Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: password validation error form state #292

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/elements-react/src/components/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ export function OryForm({ children, onAfterSubmit }: OryFormProps) {
break
}
}
if ("password" in data) {
methods.setValue("password", "")
}
if ("code" in data) {
methods.setValue("code", "")
}
onAfterSubmit?.(data.method)
}

Expand Down
143 changes: 140 additions & 3 deletions packages/elements-react/src/context/form-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,121 @@ test(`should parse state from flow on "action_flow_update" provide_identifier`,
expect(state).toEqual({ current: "provide_identifier" })
})

test(`should parse state from flow on "action_flow_update" if password has validation error`, () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

const mockFlow = {
flowType: FlowType.Login,
flow: {
active: "",
ui: {
nodes: [
{
attributes: {
autocomplete: "new-password",
disabled: false,
name: "password",
node_type: "input",
required: true,
type: "password",
},
group: "password",
messages: [
{
context: {
actual_length: 2,
min_length: 8,
},
id: 4000032,
text: "The password must be at least 8 characters long, but got 2.",
type: "error",
},
],
meta: {
label: {
id: 1070001,
text: "Password",
type: "info",
},
},
type: "input",
},
],
}, // Assuming nodes structure
},
} as unknown as OryFlowContainer

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

const [state] = result.current
expect(state).toEqual({ current: "method_active", method: "password" })
})

test(`should ignore validation message on default group nodes`, () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

const mockFlow = {
flowType: FlowType.Login,
flow: {
active: "",
ui: {
nodes: [
{
attributes: {
autocomplete: "email",
disabled: false,
name: "traits.email",
node_type: "input",
required: true,
type: "email",
value: "fdghj",
},
group: "default",
messages: [
{
context: {
reason: '"fdghj" is not valid "email"',
},
id: 4000001,
text: '"fdghj" is not valid "email"',
type: "error",
},
],
meta: {
label: {
context: {
title: "E-Mail",
},
id: 1070002,
text: "E-Mail",
type: "info",
},
},
type: "input",
},
],
}, // Assuming nodes structure
},
} as unknown as OryFlowContainer

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

const [state] = result.current
expect(state).toEqual({ current: "provide_identifier" })
})

test(`should parse state from flow on "action_flow_update" when choosing method on registration`, () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current
Expand Down Expand Up @@ -210,12 +325,12 @@ test(`should parse state from flow on "action_flow_update" when choosing method
})
})

test('should fallback to "impossible_unknown" for unknown recovery state', () => {
test("should throw error for unknown recovery state", () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

const mockFlow = {
flowType: FlowType.Recovery, // Intentional to simulate an unexpected flow
flowType: FlowType.Recovery,
flow: { id: "unknown", active: null, ui: { nodes: [] } },
} as unknown as OryFlowContainer

Expand All @@ -229,7 +344,7 @@ test('should fallback to "impossible_unknown" for unknown recovery state', () =>
).toThrow("Unknown form state")
})

test('should fallback to "impossible_unknown" for unrecognized flow', () => {
test("should throw error for unrecognized flow", () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

Expand All @@ -247,3 +362,25 @@ test('should fallback to "impossible_unknown" for unrecognized flow', () => {
}),
).toThrow("Unknown form state")
})

for (const activeMethod of ["identifier_first", "default"]) {
test(`should ignore ${activeMethod} group in active field`, () => {
const { result } = renderHook(() => useFormStateReducer(init))
const [, dispatch] = result.current

const mockFlow = {
flowType: FlowType.Login,
flow: { id: "unknown", active: activeMethod, ui: { nodes: [] } },
} as unknown as OryFlowContainer

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

const [state] = result.current
expect(state).toEqual({ current: "provide_identifier" })
})
}
21 changes: 17 additions & 4 deletions packages/elements-react/src/context/form-state.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, UiNode, UiNodeGroupEnum } from "@ory/client-fetch"
import { useReducer } from "react"
import { isChoosingMethod } from "../components/card/card-two-step.utils"
import { OryFlowContainer } from "../util"
Expand All @@ -23,20 +23,33 @@ export type FormStateAction =
method: UiNodeGroupEnum
}

function findMethodWithMessage(nodes?: UiNode[]) {
return nodes
?.filter((n) => !["default", "identifier_first"].includes(n.group))
?.find((node) => node.messages?.length > 0)
}

function parseStateFromFlow(flow: OryFlowContainer): FormState {
switch (flow.flowType) {
case FlowType.Registration:
case FlowType.Login:
case FlowType.Login: {
const methodWithMessage = findMethodWithMessage(flow.flow.ui.nodes)
if (flow.flow.active == "link_recovery") {
return { current: "method_active", method: "link" }
} else if (flow.flow.active == "code_recovery") {
return { current: "method_active", method: "code" }
} else if (methodWithMessage) {
return { current: "method_active", method: methodWithMessage.group }
} else if (
flow.flow.active &&
!["default", "identifier_first"].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.active) {
return { current: "method_active", method: flow.flow.active }
}
return { current: "provide_identifier" }
}
case FlowType.Recovery:
case FlowType.Verification:
// The API does not provide types for the active field of the recovery flow
Expand Down
Loading