From 2d1d4a189ca1a5eb3bb18b535e48d5b5114f05db Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 27 Nov 2024 07:52:33 +0100 Subject: [PATCH 1/2] feat(core): add react-hook form resolver --- .../__snapshots__/form-resolver.test.tsx.snap | 70 +++++++++++++++++++ .../src/components/form/form-provider.tsx | 5 +- .../components/form/form-resolver.test.tsx | 58 +++++++++++++++ .../src/components/form/form-resolver.ts | 44 ++++++++++++ .../src/components/form/nodes/input.tsx | 8 +-- .../theme/default/components/form/label.tsx | 14 +++- 6 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 packages/elements-react/src/components/form/__snapshots__/form-resolver.test.tsx.snap create mode 100644 packages/elements-react/src/components/form/form-resolver.test.tsx create mode 100644 packages/elements-react/src/components/form/form-resolver.ts diff --git a/packages/elements-react/src/components/form/__snapshots__/form-resolver.test.tsx.snap b/packages/elements-react/src/components/form/__snapshots__/form-resolver.test.tsx.snap new file mode 100644 index 00000000..ef96dcd8 --- /dev/null +++ b/packages/elements-react/src/components/form/__snapshots__/form-resolver.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`case={"method":"code","code":"","email":""} 1`] = ` +{ + "errors": { + "code": { + "context": { + "property": "code", + }, + "id": 4000002, + "text": "Property code is missing", + "type": "error", + }, + }, + "values": { + "code": "", + "email": "", + "method": "code", + }, +} +`; + +exports[`case={"method":"code","code":"123456","email":""} 1`] = ` +{ + "errors": {}, + "values": { + "code": "123456", + "email": "", + "method": "code", + }, +} +`; + +exports[`case={"method":"code","code":"123456","email":"some@example.com"} 1`] = ` +{ + "errors": {}, + "values": { + "code": "123456", + "email": "some@example.com", + "method": "code", + }, +} +`; + +exports[`case={"method":"code"} 1`] = ` +{ + "errors": { + "code": { + "context": { + "property": "code", + }, + "id": 4000002, + "text": "Property code is missing", + "type": "error", + }, + }, + "values": { + "method": "code", + }, +} +`; + +exports[`case={"method":"password"} 1`] = ` +{ + "errors": {}, + "values": { + "method": "password", + }, +} +`; diff --git a/packages/elements-react/src/components/form/form-provider.tsx b/packages/elements-react/src/components/form/form-provider.tsx index 78e31d88..a9e78a68 100644 --- a/packages/elements-react/src/components/form/form-provider.tsx +++ b/packages/elements-react/src/components/form/form-provider.tsx @@ -3,9 +3,10 @@ import { UiNode, UiNodeGroupEnum } from "@ory/client-fetch" import { PropsWithChildren } from "react" -import { useOryFlow } from "../../context" import { FormProvider, useForm } from "react-hook-form" +import { useOryFlow } from "../../context" import { computeDefaultValues } from "./form-helpers" +import { useOryFormResolver } from "./form-resolver" export function OryFormProvider({ children, @@ -21,6 +22,8 @@ export function OryFormProvider({ const methods = useForm({ // TODO: Generify this, so we have typesafety in the submit handler. defaultValues: computeDefaultValues(defaultNodes), + resolver: useOryFormResolver(), }) + return {children} } diff --git a/packages/elements-react/src/components/form/form-resolver.test.tsx b/packages/elements-react/src/components/form/form-resolver.test.tsx new file mode 100644 index 00000000..bf3d88b9 --- /dev/null +++ b/packages/elements-react/src/components/form/form-resolver.test.tsx @@ -0,0 +1,58 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { renderHook } from "@testing-library/react" +import { FormValues } from "../../types" +import { useOryFormResolver } from "./form-resolver" +import { OryFlowProvider } from "../../context/flow-context" +import { defaultConfiguration } from "../../tests/jest/test-utils" +import { PropsWithChildren } from "react" +import { FlowType, LoginFlow } from "@ory/client-fetch" + +const testCases = [ + { + method: "code", + code: "", + email: "", + }, + { + method: "code", + }, + { + method: "code", + code: "123456", + email: "", + }, + { + method: "code", + code: "123456", + email: "some@example.com", + }, + { + method: "password", + }, +] satisfies FormValues[] + +const wrapper = ({ children }: PropsWithChildren) => ( + + {children} + +) + +for (const testCase of testCases) { + test("case=" + JSON.stringify(testCase), () => { + const formResolver = renderHook(() => useOryFormResolver(), { + wrapper, + }) + expect(formResolver.result.current(testCase)).toMatchSnapshot() + }) +} diff --git a/packages/elements-react/src/components/form/form-resolver.ts b/packages/elements-react/src/components/form/form-resolver.ts new file mode 100644 index 00000000..822cc563 --- /dev/null +++ b/packages/elements-react/src/components/form/form-resolver.ts @@ -0,0 +1,44 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { useOryFlow } from "../../context" +import { FormValues } from "../../types" + +function isCodeResendRequest(data: FormValues) { + return data.email ?? data.resend +} + +/** + * Creates a resolver for the Ory form + * + * The resolver does form validation for missing fields in the form. + * + * @returns a react-hook-form resolver for the Ory form + */ +export function useOryFormResolver() { + const flowContainer = useOryFlow() + + return (data: FormValues) => { + if (flowContainer.formState.current === "method_active") { + if (data.method === "code" && !data.code && !isCodeResendRequest(data)) { + return { + values: data, + errors: { + code: { + id: 4000002, + context: { + property: "code", + }, + type: "error", + text: "Property code is missing", + }, + }, + } + } + } + return { + values: data, + errors: {}, + } + } +} diff --git a/packages/elements-react/src/components/form/nodes/input.tsx b/packages/elements-react/src/components/form/nodes/input.tsx index e4681a59..ff3649c0 100644 --- a/packages/elements-react/src/components/form/nodes/input.tsx +++ b/packages/elements-react/src/components/form/nodes/input.tsx @@ -30,9 +30,12 @@ export const NodeInput = ({ // ...attrs } = attributes + const isResendNode = node.meta.label?.id === 1070008 + const isScreenSelectionNode = + "name" in node.attributes && node.attributes.name === "screen" const setFormValue = () => { - if (attrs.value) { + if (attrs.value && !(isResendNode || isScreenSelectionNode)) { setValue(attrs.name, attrs.value) } } @@ -64,9 +67,6 @@ export const NodeInput = ({ const isPinCodeInput = (attrs.name === "code" && node.group === "code") || (attrs.name === "totp_code" && node.group === "totp") - const isResendNode = node.meta.label?.id === 1070008 - const isScreenSelectionNode = - "name" in node.attributes && node.attributes.name === "screen" switch (attributes.type) { case UiNodeInputAttributesTypeEnum.Submit: 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 eead86c5..a95850e4 100644 --- a/packages/elements-react/src/theme/default/components/form/label.tsx +++ b/packages/elements-react/src/theme/default/components/form/label.tsx @@ -1,7 +1,12 @@ // Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { FlowType, getNodeLabel, UiNode } from "@ory/client-fetch" +import { + FlowType, + getNodeLabel, + instanceOfUiText, + UiNode, +} from "@ory/client-fetch" import { OryNodeLabelProps, messageTestId, @@ -31,7 +36,7 @@ export function DefaultLabel({ const label = getNodeLabel(node) const { Message } = useComponents() const { config, flowType, flow } = useOryFlow() - const { setValue } = useFormContext() + const { setValue, formState } = useFormContext() const isPassword = attributes.type === "password" @@ -43,6 +48,8 @@ export function DefaultLabel({ } } + const fieldError = formState.errors[attributes.name] + return (
{label && ( @@ -86,6 +93,9 @@ export function DefaultLabel({ {node.messages.map((message) => ( ))} + {fieldError && instanceOfUiText(fieldError) && ( + + )}
) } From 78590ab6db0f6d5117cca7a412c0de68280b5eaa Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 27 Nov 2024 07:53:48 +0100 Subject: [PATCH 2/2] chore: update conventional_commits scope --- .github/conventional_commits.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/conventional_commits.json b/.github/conventional_commits.json index 0b4dc460..05f92e2c 100644 --- a/.github/conventional_commits.json +++ b/.github/conventional_commits.json @@ -1,5 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/ory/ci/master/conventional_commit_config/dist/config.schema.json", "addTypes": [], - "addScopes": ["deps-dev"] + "addScopes": ["deps-dev", "core", "theme"] }