Skip to content

Commit

Permalink
chore: add settings flow to form state
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas committed Nov 18, 2024
1 parent aeb715f commit 6f01bc1
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 37 deletions.
11 changes: 8 additions & 3 deletions packages/elements-react/src/components/form/form-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/elements-react/src/components/form/nodes/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -74,7 +74,7 @@ export const NodeInput = ({
if (isSocial) {
return <Node.OidcButton attributes={attrs} node={node} />
}
if (isResend || isScreenSelection) {
if (isResendNode || isScreenSelectionNode) {
return null
}

Expand Down
34 changes: 16 additions & 18 deletions packages/elements-react/src/context/form-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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")
})
6 changes: 4 additions & 2 deletions packages/elements-react/src/context/form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/elements-react/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
<div>
<a
className="cursor-pointer py-1.5 px-3 rounded-full border border-button-identifier-border-default bg-button-identifier-bg-default hover:border-button-identifier-border-hover hover:bg-button-identifier-bg-hover transition-colors inline-flex gap-1 items-center"
{...nodeBackButton.attributes}
className="cursor-pointer py-[5px] px-3 rounded-full border border-button-identifier-border-default bg-button-identifier-bg-default hover:border-button-identifier-border-hover hover:bg-button-identifier-bg-hover transition-colors inline-flex gap-1 items-center"
{...attributes}
href={initFlowUrl}
title={`Adjust ${nodeBackButton?.attributes.value}`}
>
<IconArrowLeft size={16} className="text-button-identifier-fg-subtle" />
<span className="text-sm font-medium leading-none text-button-identifier-fg-default text-ellipsis overflow-hidden text-nowrap">
<span className="text-sm font-medium text-button-identifier-fg-default text-ellipsis overflow-hidden text-nowrap">
{nodeBackButton?.attributes.value}
</span>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ 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" &&
n.attributes.type === "submit",
)

return (
<span className="flex flex-col antialiased gap-1">
<div className="flex flex-col antialiased gap-1">
{label && (
<span className="inline-flex justify-between">
<label
Expand All @@ -56,7 +56,7 @@ export function DefaultLabel({
})}
</a>
)}
{hasResend && (
{hasResendNode && (
<button
type="submit"
name="method"
Expand All @@ -82,6 +82,6 @@ export function DefaultLabel({
{uiTextToFormattedMessage(message, intl)}
</span>
))}
</span>
</div>
)
}
10 changes: 10 additions & 0 deletions packages/elements-react/src/theme/default/utils/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function omit<OBJ extends object>(
obj: OBJ,
keys: (keyof OBJ)[],
): Omit<typeof obj, (typeof keys)[number]> {
const ret = { ...obj }
for (const key of keys) {
delete ret[key]
}
return ret
}
2 changes: 1 addition & 1 deletion packages/elements-react/src/util/ui/__test__/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/elements-react/src/util/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? []
Expand Down

0 comments on commit 6f01bc1

Please sign in to comment.