From 8ab23aee8ee43bd090c35e152cdd3b5092918601 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 26 Nov 2024 19:34:22 +0100 Subject: [PATCH] feat: enable proper account linking flows --- .../src/components/form/form-helpers.ts | 18 ++++++------ .../src/components/form/messages.tsx | 2 +- .../src/components/form/social.tsx | 9 ++++-- .../elements-react/src/context/form-state.ts | 5 +++- packages/elements-react/src/locales/de.json | 3 +- packages/elements-react/src/locales/en.json | 3 +- packages/elements-react/src/locales/es.json | 3 +- packages/elements-react/src/locales/fr.json | 3 +- packages/elements-react/src/locales/nl.json | 3 +- packages/elements-react/src/locales/pl.json | 3 +- packages/elements-react/src/locales/pt.json | 3 +- packages/elements-react/src/locales/sv.json | 3 +- .../theme/default/components/card/header.tsx | 5 +--- .../__tests__/constructCardHeader.spec.tsx | 6 +++- .../default/utils/constructCardHeader.ts | 28 +++++++++++++++++-- 15 files changed, 69 insertions(+), 28 deletions(-) diff --git a/packages/elements-react/src/components/form/form-helpers.ts b/packages/elements-react/src/components/form/form-helpers.ts index c6b78d6b9..95ec5a6ac 100644 --- a/packages/elements-react/src/components/form/form-helpers.ts +++ b/packages/elements-react/src/components/form/form-helpers.ts @@ -14,15 +14,18 @@ export function computeDefaultValues(nodes: UiNode[]): FormValues { attrs.name === "method" || attrs.type === "submit" || typeof attrs.value === "undefined" - ) + ) { return acc + } // Unroll nested traits or assign default values - const unrolled = unrollTrait({ - name: attrs.name, - value: attrs.value, - }) - Object.assign(acc, unrolled ?? { [attrs.name]: attrs.value }) + return unrollTrait( + { + name: attrs.name, + value: attrs.value, + }, + acc, + ) } return acc @@ -32,9 +35,8 @@ export function computeDefaultValues(nodes: UiNode[]): FormValues { export function unrollTrait( input: { name: T; value: V }, output: Partial> = {}, -): UnrollTrait | undefined { +): UnrollTrait { const keys = input.name.split(".") - if (!keys.length) return undefined // It's challenging to type this for deeply nested structures because the shape // of current changes dynamically as we navigate through levels. diff --git a/packages/elements-react/src/components/form/messages.tsx b/packages/elements-react/src/components/form/messages.tsx index d09797654..8d6afa648 100644 --- a/packages/elements-react/src/components/form/messages.tsx +++ b/packages/elements-react/src/components/form/messages.tsx @@ -16,7 +16,7 @@ export type OryMessageRootProps = DetailedHTMLProps< // This is a list of message IDs that should not be shown to the user. // They're returned by the API, but they don't work well in the two step flows. -const messageIdsToHide = [1040009, 1060003, 1080003, 1010014, 1040005] +const messageIdsToHide = [1040009, 1060003, 1080003, 1010014, 1040005, 1010016] export function OryCardValidationMessages({ ...props }: OryMessageRootProps) { const { flow } = useOryFlow() diff --git a/packages/elements-react/src/components/form/social.tsx b/packages/elements-react/src/components/form/social.tsx index 1313f2fff..37c25418f 100644 --- a/packages/elements-react/src/components/form/social.tsx +++ b/packages/elements-react/src/components/form/social.tsx @@ -7,6 +7,7 @@ import { UiNode, UiNodeInputAttributes } from "@ory/client-fetch" import { PropsWithChildren } from "react" import { OryForm } from "./form" import { useFormContext } from "react-hook-form" +import { OryFormProvider } from "./form-provider" export type OryFormOidcButtonsProps = PropsWithChildren<{ hideDivider?: boolean @@ -86,8 +87,10 @@ export function OryFormSocialButtonsForm() { } return ( - - - + + + + + ) } diff --git a/packages/elements-react/src/context/form-state.ts b/packages/elements-react/src/context/form-state.ts index b958cdcd7..aeaea0d72 100644 --- a/packages/elements-react/src/context/form-state.ts +++ b/packages/elements-react/src/context/form-state.ts @@ -42,11 +42,14 @@ function parseStateFromFlow(flow: OryFlowContainer): FormState { return { current: "method_active", method: methodWithMessage.group } } else if ( flow.flow.active && - !["default", "identifier_first"].includes(flow.flow.active) + !["default", "identifier_first", "oidc"].includes(flow.flow.active) ) { return { current: "method_active", method: flow.flow.active } } else if (isChoosingMethod(flow.flow.ui.nodes)) { return { current: "select_method" } + } else if (flow.flow.ui.messages?.some((m) => m.id === 1010016)) { + // Account linking edge case + return { current: "select_method" } } return { current: "provide_identifier" } } diff --git a/packages/elements-react/src/locales/de.json b/packages/elements-react/src/locales/de.json index 29e9874bc..ee57eeccb 100644 --- a/packages/elements-react/src/locales/de.json +++ b/packages/elements-react/src/locales/de.json @@ -240,5 +240,6 @@ "settings.title-totp": "Verwalten Sie die 2FA TOTP Authenticator-App", "settings.title-webauthn": "Hardware-Token verwalten", "settings.webauthn.info": "Hardware-Tokens werden für die Zweitfaktor-Authentifizierung oder als Erstfaktor-Authentifizierung mit Passkeys verwendet", - "card.footer.select-another-method": "Eine andere Methode verwenden" + "card.footer.select-another-method": "Eine andere Methode verwenden", + "account-linking.title": "Account Verbinden" } diff --git a/packages/elements-react/src/locales/en.json b/packages/elements-react/src/locales/en.json index bb2c5144a..a6ec0a81c 100644 --- a/packages/elements-react/src/locales/en.json +++ b/packages/elements-react/src/locales/en.json @@ -240,5 +240,6 @@ "settings.passkey.title": "Manage Passkeys", "settings.passkey.description": "Manage your passkey settings", "settings.passkey.info": "Manage your passkey settings", - "card.footer.select-another-method": "Select another method" + "card.footer.select-another-method": "Select another method", + "account-linking.title": "Link account" } diff --git a/packages/elements-react/src/locales/es.json b/packages/elements-react/src/locales/es.json index 5f15a9a2a..846bc6234 100644 --- a/packages/elements-react/src/locales/es.json +++ b/packages/elements-react/src/locales/es.json @@ -240,5 +240,6 @@ "settings.title-totp": "", "settings.title-webauthn": "", "settings.webauthn.info": "", - "card.footer.select-another-method": "" + "card.footer.select-another-method": "", + "account-linking.title": "" } diff --git a/packages/elements-react/src/locales/fr.json b/packages/elements-react/src/locales/fr.json index f8d650875..55e4eb5c7 100644 --- a/packages/elements-react/src/locales/fr.json +++ b/packages/elements-react/src/locales/fr.json @@ -240,5 +240,6 @@ "settings.webauthn.description": "", "settings.webauthn.info": "", "settings.webauthn.title": "", - "card.footer.select-another-method": "" + "card.footer.select-another-method": "", + "account-linking.title": "" } diff --git a/packages/elements-react/src/locales/nl.json b/packages/elements-react/src/locales/nl.json index c8b299864..338375c9f 100644 --- a/packages/elements-react/src/locales/nl.json +++ b/packages/elements-react/src/locales/nl.json @@ -240,5 +240,6 @@ "settings.webauthn.description": "", "settings.webauthn.info": "", "settings.webauthn.title": "", - "card.footer.select-another-method": "" + "card.footer.select-another-method": "", + "account-linking.title": "" } diff --git a/packages/elements-react/src/locales/pl.json b/packages/elements-react/src/locales/pl.json index 4709be665..2297fc077 100644 --- a/packages/elements-react/src/locales/pl.json +++ b/packages/elements-react/src/locales/pl.json @@ -240,5 +240,6 @@ "settings.webauthn.description": "", "settings.webauthn.info": "", "settings.webauthn.title": "", - "card.footer.select-another-method": "" + "card.footer.select-another-method": "", + "account-linking.title": "" } diff --git a/packages/elements-react/src/locales/pt.json b/packages/elements-react/src/locales/pt.json index f8d719b14..b0c1a035c 100644 --- a/packages/elements-react/src/locales/pt.json +++ b/packages/elements-react/src/locales/pt.json @@ -240,5 +240,6 @@ "settings.webauthn.description": "", "settings.webauthn.info": "", "settings.webauthn.title": "", - "card.footer.select-another-method": "" + "card.footer.select-another-method": "", + "account-linking.title": "" } diff --git a/packages/elements-react/src/locales/sv.json b/packages/elements-react/src/locales/sv.json index 6d31bd164..27cdc55f3 100644 --- a/packages/elements-react/src/locales/sv.json +++ b/packages/elements-react/src/locales/sv.json @@ -240,5 +240,6 @@ "settings.webauthn.description": "Hantera inställningarna för din maskinvarutoken", "settings.webauthn.info": "Hårdvarutokens används för andrafaktorsautentisering eller som förstafaktor med lösenordsnycklar", "settings.webauthn.title": "Hantera maskinvarutokens", - "card.footer.select-another-method": "Välj en annan metod" + "card.footer.select-another-method": "Välj en annan metod", + "account-linking.title": "Länka ditt konto" } diff --git a/packages/elements-react/src/theme/default/components/card/header.tsx b/packages/elements-react/src/theme/default/components/card/header.tsx index d08148ea8..1686c2ace 100644 --- a/packages/elements-react/src/theme/default/components/card/header.tsx +++ b/packages/elements-react/src/theme/default/components/card/header.tsx @@ -23,10 +23,7 @@ function InnerCardHeader({ title, text }: { title: string; text?: string }) { export function DefaultCardHeader() { const context = useOryFlow() - const { title, description } = useCardHeaderText( - context.flow.ui.nodes, - context, - ) + const { title, description } = useCardHeaderText(context.flow.ui, context) return } diff --git a/packages/elements-react/src/theme/default/utils/__tests__/constructCardHeader.spec.tsx b/packages/elements-react/src/theme/default/utils/__tests__/constructCardHeader.spec.tsx index 1348e9c82..34d9a8f6e 100644 --- a/packages/elements-react/src/theme/default/utils/__tests__/constructCardHeader.spec.tsx +++ b/packages/elements-react/src/theme/default/utils/__tests__/constructCardHeader.spec.tsx @@ -184,7 +184,11 @@ for (const flowType of [ const res = renderHook( () => useCardHeaderText( - Array.isArray(value) ? value : [value], + { + nodes: Array.isArray(value) ? value : [value], + action: "", + method: "", + }, opts, ), { wrapper }, diff --git a/packages/elements-react/src/theme/default/utils/constructCardHeader.ts b/packages/elements-react/src/theme/default/utils/constructCardHeader.ts index 772a280e6..1fed28b6a 100644 --- a/packages/elements-react/src/theme/default/utils/constructCardHeader.ts +++ b/packages/elements-react/src/theme/default/utils/constructCardHeader.ts @@ -1,7 +1,11 @@ // Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { FlowType, isUiNodeInputAttributes, UiNode } from "@ory/client-fetch" +import { + FlowType, + isUiNodeInputAttributes, + UiContainer, +} from "@ory/client-fetch" import { useIntl } from "react-intl" function joinWithCommaOr(list: string[], orText = "or"): string { @@ -48,9 +52,10 @@ type opts = * @returns a title and a description for the card header */ export function useCardHeaderText( - nodes: UiNode[], + container: UiContainer, opts: opts, ): { title: string; description: string } { + const nodes = container.nodes const intl = useIntl() switch (opts.flowType) { case FlowType.Recovery: @@ -110,6 +115,25 @@ export function useCardHeaderText( id: "verification.subtitle", }), } + case FlowType.Login: { + // account linking + const accountLinkingMessage = container.messages?.find( + (m) => m.id === 1010016, + ) + if (accountLinkingMessage) { + return { + title: intl.formatMessage({ + id: "account-linking.title", + }), + description: intl.formatMessage( + { + id: "identities.messages.1010016", + }, + accountLinkingMessage.context as Record, + ), + } + } + } } const parts = []