Skip to content

Commit

Permalink
fix: password validation error form state (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas authored Nov 26, 2024
1 parent 26f2087 commit cfebb19
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
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

0 comments on commit cfebb19

Please sign in to comment.