From 0a8712b46315a1b3a949d21aea7248dfe1c18ab7 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 18 Nov 2024 11:06:16 +0100 Subject: [PATCH] chore: add settings flow to form state --- .../api-report/elements-react.api.json | 2 +- .../api-report/elements-react.api.md | 2 +- .../src/components/form/form-helpers.ts | 11 ++++-- .../src/components/form/nodes/input.tsx | 6 ++-- .../src/context/form-state.test.ts | 34 +++++++++---------- .../elements-react/src/context/form-state.ts | 6 ++-- packages/elements-react/src/locales/de.json | 2 +- .../card/current-identifier-button.tsx | 11 ++++-- .../theme/default/components/form/input.tsx | 9 ++++- .../theme/default/components/form/label.tsx | 8 ++--- .../src/theme/default/utils/attributes.ts | 13 +++++++ .../src/util/ui/__test__/ui.spec.ts | 2 +- packages/elements-react/src/util/ui/index.ts | 3 +- 13 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 packages/elements-react/src/theme/default/utils/attributes.ts diff --git a/packages/elements-react/api-report/elements-react.api.json b/packages/elements-react/api-report/elements-react.api.json index 6497ffcc..fa0ab7e0 100644 --- a/packages/elements-react/api-report/elements-react.api.json +++ b/packages/elements-react/api-report/elements-react.api.json @@ -348,7 +348,7 @@ }, { "kind": "Content", - "text": ";\n} | {\n current: \"success_screen\";\n} | {\n current: \"impossible_unknown\";\n}" + "text": ";\n} | {\n current: \"success_screen\";\n} | {\n current: \"settings\";\n}" }, { "kind": "Content", diff --git a/packages/elements-react/api-report/elements-react.api.md b/packages/elements-react/api-report/elements-react.api.md index 8884a6fe..152ab6cc 100644 --- a/packages/elements-react/api-report/elements-react.api.md +++ b/packages/elements-react/api-report/elements-react.api.md @@ -64,7 +64,7 @@ export type FormState = { } | { current: "success_screen"; } | { - current: "impossible_unknown"; + current: "settings"; }; // @public (undocumented) diff --git a/packages/elements-react/src/components/form/form-helpers.ts b/packages/elements-react/src/components/form/form-helpers.ts index dda41c66..c6b78d6b 100644 --- a/packages/elements-react/src/components/form/form-helpers.ts +++ b/packages/elements-react/src/components/form/form-helpers.ts @@ -10,14 +10,19 @@ export function computeDefaultValues(nodes: UiNode[]): FormValues { if (isUiNodeInputAttributes(attrs)) { // Skip the "method" field and "submit" button - if (attrs.name === "method" || attrs.type === "submit") return acc + if ( + 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 ?? "", + value: attrs.value, }) - Object.assign(acc, unrolled ?? { [attrs.name]: attrs.value ?? "" }) + Object.assign(acc, unrolled ?? { [attrs.name]: attrs.value }) } return acc diff --git a/packages/elements-react/src/components/form/nodes/input.tsx b/packages/elements-react/src/components/form/nodes/input.tsx index e8b19e49..e4681a59 100644 --- a/packages/elements-react/src/components/form/nodes/input.tsx +++ b/packages/elements-react/src/components/form/nodes/input.tsx @@ -64,8 +64,8 @@ export const NodeInput = ({ const isPinCodeInput = (attrs.name === "code" && node.group === "code") || (attrs.name === "totp_code" && node.group === "totp") - const isResend = node.meta.label?.id === 1070008 - const isScreenSelection = + const isResendNode = node.meta.label?.id === 1070008 + const isScreenSelectionNode = "name" in node.attributes && node.attributes.name === "screen" switch (attributes.type) { @@ -74,7 +74,7 @@ export const NodeInput = ({ if (isSocial) { return } - if (isResend || isScreenSelection) { + if (isResendNode || isScreenSelectionNode) { return null } diff --git a/packages/elements-react/src/context/form-state.test.ts b/packages/elements-react/src/context/form-state.test.ts index 52a78d06..d029fad0 100644 --- a/packages/elements-react/src/context/form-state.test.ts +++ b/packages/elements-react/src/context/form-state.test.ts @@ -208,15 +208,14 @@ test('should fallback to "impossible_unknown" for unknown recovery state', () => flow: { id: "unknown", active: null, ui: { nodes: [] } }, } as unknown as OryFlowContainer - act(() => { - dispatch({ - type: "action_flow_update", - flow: mockFlow, - }) - }) - - const [state] = result.current - expect(state).toEqual({ current: "impossible_unknown" }) + expect(() => + act(() => { + dispatch({ + type: "action_flow_update", + flow: mockFlow, + }) + }), + ).toThrow("Unknown form state") }) test('should fallback to "impossible_unknown" for unrecognized flow', () => { @@ -228,13 +227,12 @@ test('should fallback to "impossible_unknown" for unrecognized flow', () => { flow: { id: "unknown", active: null, ui: { nodes: [] } }, } as unknown as OryFlowContainer - act(() => { - dispatch({ - type: "action_flow_update", - flow: mockFlow, - }) - }) - - const [state] = result.current - expect(state).toEqual({ current: "impossible_unknown" }) + expect(() => + act(() => { + dispatch({ + type: "action_flow_update", + flow: mockFlow, + }) + }), + ).toThrow("Unknown form state") }) diff --git a/packages/elements-react/src/context/form-state.ts b/packages/elements-react/src/context/form-state.ts index 16329ff8..44257277 100644 --- a/packages/elements-react/src/context/form-state.ts +++ b/packages/elements-react/src/context/form-state.ts @@ -11,7 +11,7 @@ export type FormState = | { current: "select_method" } | { current: "method_active"; method: UiNodeGroupEnum } | { current: "success_screen" } - | { current: "impossible_unknown" } + | { current: "settings" } export type FormStateAction = | { @@ -48,11 +48,13 @@ function parseStateFromFlow(flow: OryFlowContainer): FormState { return { current: "method_active", method: flow.flow.active } } break + case FlowType.Settings: + return { current: "settings" } } console.warn( `[Ory/Elements React] Encountered an unknown form state on ${flow.flowType} flow with ID ${flow.flow.id}`, ) - return { current: "impossible_unknown" } + throw new Error("Unknown form state") } export function formStateReducer( diff --git a/packages/elements-react/src/locales/de.json b/packages/elements-react/src/locales/de.json index d32c98b1..29e9874b 100644 --- a/packages/elements-react/src/locales/de.json +++ b/packages/elements-react/src/locales/de.json @@ -240,5 +240,5 @@ "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 versuchen" + "card.footer.select-another-method": "Eine andere Methode verwenden" } diff --git a/packages/elements-react/src/theme/default/components/card/current-identifier-button.tsx b/packages/elements-react/src/theme/default/components/card/current-identifier-button.tsx index 7982cfbe..ea600bb5 100644 --- a/packages/elements-react/src/theme/default/components/card/current-identifier-button.tsx +++ b/packages/elements-react/src/theme/default/components/card/current-identifier-button.tsx @@ -4,6 +4,7 @@ import { FlowType, UiNode } from "@ory/client-fetch" import { useOryFlow } from "@ory/elements-react" import IconArrowLeft from "../../assets/icons/arrow-left.svg" +import { omit } from "../../utils/attributes" export function DefaultCurrentIdentifierButton() { const { @@ -26,17 +27,21 @@ export function DefaultCurrentIdentifierButton() { return null } const initFlowUrl = `${config.sdk.url}/self-service/${flowType}/browser` + const attributes = omit(nodeBackButton.attributes, [ + "autocomplete", + "node_type", + ]) return (
- + {nodeBackButton?.attributes.value} diff --git a/packages/elements-react/src/theme/default/components/form/input.tsx b/packages/elements-react/src/theme/default/components/form/input.tsx index 5204670a..9d6aeb45 100644 --- a/packages/elements-react/src/theme/default/components/form/input.tsx +++ b/packages/elements-react/src/theme/default/components/form/input.tsx @@ -18,7 +18,14 @@ export const DefaultInput = ({ }: OryNodeInputProps) => { const label = getNodeLabel(node) const { register } = useFormContext() - const { value, autocomplete, name, maxlength, ...rest } = attributes + const { + value, + autocomplete, + name, + maxlength, + node_type: _, + ...rest + } = attributes const intl = useIntl() const { flowType } = useOryFlow() diff --git a/packages/elements-react/src/theme/default/components/form/label.tsx b/packages/elements-react/src/theme/default/components/form/label.tsx index cfcd94bd..2c4a873e 100644 --- a/packages/elements-react/src/theme/default/components/form/label.tsx +++ b/packages/elements-react/src/theme/default/components/form/label.tsx @@ -23,7 +23,7 @@ export function DefaultLabel({ const isPassword = attributes.type === "password" - const hasResend = flow.ui.nodes.some( + const hasResendNode = flow.ui.nodes.some( (n) => "name" in n.attributes && n.attributes.name === "email" && @@ -31,7 +31,7 @@ export function DefaultLabel({ ) return ( - +
{label && (
) } diff --git a/packages/elements-react/src/theme/default/utils/attributes.ts b/packages/elements-react/src/theme/default/utils/attributes.ts new file mode 100644 index 00000000..ee2ea28a --- /dev/null +++ b/packages/elements-react/src/theme/default/utils/attributes.ts @@ -0,0 +1,13 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export function omit( + obj: OBJ, + keys: (keyof OBJ)[], +): Omit { + const ret = { ...obj } + for (const key of keys) { + delete ret[key] + } + return ret +} diff --git a/packages/elements-react/src/util/ui/__test__/ui.spec.ts b/packages/elements-react/src/util/ui/__test__/ui.spec.ts index 49bf215c..cbd67db7 100644 --- a/packages/elements-react/src/util/ui/__test__/ui.spec.ts +++ b/packages/elements-react/src/util/ui/__test__/ui.spec.ts @@ -16,7 +16,7 @@ describe("utils/ui", () => { expect(result.current.groups.oidc).toHaveLength(2) expect(result.current.groups.default).toHaveLength(2) - expect(result.current.groups.webauthn).toHaveLength(2) + expect(result.current.groups.webauthn).toHaveLength(1) expect(result.current.groups.passkey).toHaveLength(3) expect(result.current.groups.password).toHaveLength(2) expect(result.current.groups.code).toHaveLength(1) diff --git a/packages/elements-react/src/util/ui/index.ts b/packages/elements-react/src/util/ui/index.ts index 6e106e84..a60b8b71 100644 --- a/packages/elements-react/src/util/ui/index.ts +++ b/packages/elements-react/src/util/ui/index.ts @@ -108,7 +108,8 @@ export function useNodesGroups(nodes: UiNode[]) { for (const node of nodes) { if (node.type === "script") { - // WebAuthn scripts are part of the nodes, for passkey flows + // We always render all scripts, because the scripts for passkeys are part of the webauthn group, + // which leads to this hook returning a webauthn group on passkey flows (which it should not - webauthn is the "legacy" passkey implementation). continue } const groupNodes = groups[node.group] ?? []