Skip to content

Commit

Permalink
feat: enable proper account linking flows
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas committed Nov 27, 2024
1 parent e46f5fd commit 9a6e4ce
Show file tree
Hide file tree
Showing 17 changed files with 126 additions and 29 deletions.
18 changes: 10 additions & 8 deletions packages/elements-react/src/components/form/form-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Check warning on line 22 in packages/elements-react/src/components/form/form-helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/elements-react/src/components/form/form-helpers.ts#L22

Added line #L22 was not covered by tests
{
name: attrs.name,
value: attrs.value,
},
acc,
)
}

return acc
Expand All @@ -32,9 +35,8 @@ export function computeDefaultValues(nodes: UiNode[]): FormValues {
export function unrollTrait<T extends string, V>(
input: { name: T; value: V },
output: Partial<UnrollTrait<T, V>> = {},
): UnrollTrait<T, V> | undefined {
): UnrollTrait<T, V> {
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.
Expand Down
2 changes: 1 addition & 1 deletion packages/elements-react/src/components/form/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 6 additions & 3 deletions packages/elements-react/src/components/form/social.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,8 +87,10 @@ export function OryFormSocialButtonsForm() {
}

return (
<OryForm>
<OryFormOidcButtons />
</OryForm>
<OryFormProvider>
<OryForm>
<OryFormOidcButtons />
</OryForm>
</OryFormProvider>
)
}
34 changes: 33 additions & 1 deletion packages/elements-react/src/context/form-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { FlowType, UiNodeGroupEnum } from "@ory/client-fetch"
import { FlowType, UiNodeGroupEnum, UiText } from "@ory/client-fetch"
import { act, renderHook } from "@testing-library/react"
import { OryFlowContainer } from "../util"
import { useFormStateReducer } from "./form-state" // Adjust path as needed
Expand Down Expand Up @@ -384,3 +384,35 @@ for (const activeMethod of ["identifier_first", "default"]) {
expect(state).toEqual({ current: "provide_identifier" })
})
}

test(`should parse select_method from account linking flow`, () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

const mockFlow = {
flowType: FlowType.Login,
flow: {
active: "",
ui: {
nodes: [],
messages: [
{
id: 1010016,
text: "",
type: "error",
} satisfies UiText,
],
}, // Assuming nodes structure
},
} as unknown as OryFlowContainer

act(() => {
dispatch({
type: "action_flow_update",
flow: mockFlow,
})
})

const [state] = result.current
expect(state).toEqual({ current: "select_method" })
})
5 changes: 4 additions & 1 deletion packages/elements-react/src/context/form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}
Expand Down
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
3 changes: 2 additions & 1 deletion packages/elements-react/src/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Check warning on line 26 in packages/elements-react/src/theme/default/components/card/header.tsx

View check run for this annotation

Codecov / codecov/patch

packages/elements-react/src/theme/default/components/card/header.tsx#L26

Added line #L26 was not covered by tests

return <InnerCardHeader title={title} text={description} />
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`constructCardHeaderText on account linking 1`] = `
{
"description": "You tried to sign in with "{duplicateIdentifier}", but that email is already used by another account. Sign in to your account with one of the options below to add your account "{duplicateIdentifier}" at "{provider}" as another way to sign in.",
"title": "Link account",
}
`;

exports[`flowType=login refresh=false combination=code constructCardHeaderText 1`] = `
{
"description": "Sign in with a code sent to your email",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -197,3 +201,20 @@ for (const flowType of [
}
})
}

test("constructCardHeaderText on account linking", () => {
const res = renderHook(
() =>
useCardHeaderText(
{
nodes: [defaultGroup],
action: "",
method: "",
messages: [{ id: 1010016, text: "", type: "error" }],
},
{ flowType: FlowType.Login, flow: { refresh: false } },
),
{ wrapper },
)
expect(res.result.current).toMatchSnapshot()
})
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<string, string>,
),
}
}
}
}

const parts = []
Expand Down

0 comments on commit 9a6e4ce

Please sign in to comment.