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) && (
+
+ )}
)
}