Skip to content

Commit

Permalink
feat: additional setting nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
mszekiel committed Oct 7, 2024
1 parent f03ac0c commit b90c329
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 68 deletions.
7 changes: 3 additions & 4 deletions packages/elements-react/src/components/form/form-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { isUiNodeInputAttributes } from "@ory/client-fetch"
import { isUiNodeInputAttributes, UiNode } from "@ory/client-fetch"
import { FormValues } from "../../types"
import { FlowContainer } from "../../util"

export function computeDefaultValues(flowContainer: FlowContainer): FormValues {
return flowContainer.flow.ui.nodes.reduce<FormValues>((acc, node) => {
export function computeDefaultValues(nodes: UiNode[]): FormValues {
return nodes.reduce<FormValues>((acc, node) => {
if (isUiNodeInputAttributes(node.attributes)) {
if (node.attributes.name === "method") {
// Do not set the default values for this.
Expand Down
33 changes: 25 additions & 8 deletions packages/elements-react/src/components/form/form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
UiNode,
UiNodeGroupEnum,
UpdateLoginFlowBody,
UpdateRecoveryFlowBody,
UpdateRegistrationFlowBody,
Expand Down Expand Up @@ -79,14 +81,23 @@ export type OryFormComponents = {
FormSectionFooter: ComponentType<HeadlessFormSectionContentProps>
}

export type OryFormProps = PropsWithChildren
export type OryFormProps = PropsWithChildren<{
nodes?: UiNode[]
}>

export function OryForm({ children }: OryFormProps) {
export function OryForm({ children, nodes }: OryFormProps) {
const { FormContainer } = useComponents()
const flowContainer = useOryFlow()

const defaultNodes = nodes
? flowContainer.flow.ui.nodes
.filter((node) => node.group === UiNodeGroupEnum.Default)
.concat(nodes)
: flowContainer.flow.ui.nodes

const methods = useForm({
// TODO: Generify this, so we have typesafety in the submit handler.
defaultValues: computeDefaultValues(flowContainer),
defaultValues: computeDefaultValues(defaultNodes),
})

const intl = useIntl()
Expand All @@ -103,7 +114,7 @@ export function OryForm({ children }: OryFormProps) {

const handleSuccess = (flow: FlowContainer) => {
flowContainer.setFlowContainer(flow)
methods.reset(computeDefaultValues(flow))
methods.reset(computeDefaultValues(flow.flow.ui.nodes))
}

const onSubmit: SubmitHandler<FormValues> = async (data) => {
Expand Down Expand Up @@ -171,14 +182,20 @@ export function OryForm({ children }: OryFormProps) {
}

if (
("lookup_secret_regenerate" in submitData ||
("lookup_secret_confirm" in submitData &&
submitData.lookup_secret_confirm)) ??
"lookup_secret_regenerate" in submitData ||
"lookup_secret_confirm" in submitData ||
"lookup_secret_reveal" in submitData
) {
submitData.method = "lookup_secret"
}
console.log(submitData)

if ("link" in submitData || "unlink" in submitData) {
submitData.method = "oidc"
}

if ("webauthn_remove" in submitData) {
submitData.method = "webauthn"
}

await onSubmitSettings(flowContainer, {
onRedirect,
Expand Down
10 changes: 10 additions & 0 deletions packages/elements-react/src/components/form/nodes/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
UiNodeInputAttributesTypeEnum,
} from "@ory/client-fetch"
import { MouseEventHandler, ReactNode, useEffect, useRef } from "react"
import { useFormContext } from "react-hook-form"

export const NodeInput = ({
node,
Expand All @@ -14,6 +15,7 @@ export const NodeInput = ({
attributes: UiNodeInputAttributes
onClick?: MouseEventHandler
}): ReactNode => {
const { setValue } = useFormContext()
const Components = useComponents()
const nodeType = attributes.type
const {
Expand All @@ -26,9 +28,16 @@ export const NodeInput = ({
...attrs
} = attributes

const setFormValue = () => {
if (attrs.value) {
setValue(attrs.name, attrs.value)
}
}

const hasRun = useRef(false)
useEffect(
() => {
setFormValue()
if (!hasRun.current && onloadTrigger) {
hasRun.current = true
triggerToWindowCall(onloadTrigger)
Expand All @@ -40,6 +49,7 @@ export const NodeInput = ({
)

const handleClick: MouseEventHandler = () => {
setFormValue()
if (onclickTrigger) {
triggerToWindowCall(onclickTrigger)
}
Expand Down
10 changes: 6 additions & 4 deletions packages/elements-react/src/components/form/section.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useComponents } from "../../context/component"
import { PropsWithChildren } from "react"
import { HeadlessFormSectionProps } from "../../types"
import { OryForm } from "./form"
import { UiNode } from "@ory/client-fetch"

export type HeadlessGroupContainerProps = PropsWithChildren
export type OryFormSectionProps = HeadlessFormSectionProps & {
nodes?: UiNode[]
}

export function OryFormSection({ children }: HeadlessFormSectionProps) {
export function OryFormSection({ children, nodes }: OryFormSectionProps) {
const { FormSection } = useComponents()

return (
<OryForm>
<OryForm nodes={nodes}>
<FormSection>{children}</FormSection>
</OryForm>
)
Expand Down
15 changes: 14 additions & 1 deletion packages/elements-react/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ComponentType } from "react"

export * from "./settings-card"

export interface OrySettingsRecoveryCodesProps {
export type OrySettingsRecoveryCodesProps = {
codes: string[]
regnerateButton?: UiNode
revealButton?: UiNode
Expand All @@ -19,7 +19,20 @@ export type OrySettingsTotpProps =
totpUnlink: UiNode
}

export type OrySettingsOidcProps = {
linkButtons: UiNode[]
unlinkButtons: UiNode[]
}

export type OrySettingsWebauthnProps = {
nameInput: UiNode
triggerButton: UiNode & { onClick: () => void }
removeButtons: UiNode[]
}

export type OrySettingsComponents = {
SettingsRecoveryCodes: ComponentType<OrySettingsRecoveryCodesProps>
SettingsTotp: ComponentType<OrySettingsTotpProps>
SettingsOidc: ComponentType<OrySettingsOidcProps>
SettingsWebauthn: ComponentType<OrySettingsWebauthnProps>
}
42 changes: 42 additions & 0 deletions packages/elements-react/src/components/settings/oidc-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { UiNode } from "@ory/client-fetch"
import { useComponents } from "../../context"
import { useIntl } from "react-intl"

const getLinkButtons = (nodes: UiNode[]): UiNode[] =>
nodes.filter(
(node) => "name" in node.attributes && node.attributes.name === "link",
)

const getUnlinkButtons = (nodes: UiNode[]): UiNode[] =>
nodes.filter(
(node) => "name" in node.attributes && node.attributes.name === "unlink",
)

interface OrySettingsOidcProps {
nodes: UiNode[]
}

export function OrySettingsOidc({ nodes }: OrySettingsOidcProps) {
const Components = useComponents()
const intl = useIntl()

const linkButtons = getLinkButtons(nodes)
const unlinkButtons = getUnlinkButtons(nodes)

return (
<>
<Components.FormSectionContent
title={intl.formatMessage({ id: "settings.oidc.title" })}
description={intl.formatMessage({ id: "settings.oidc.description" })}
>
<Components.SettingsOidc
linkButtons={linkButtons}
unlinkButtons={unlinkButtons}
/>
</Components.FormSectionContent>
<Components.FormSectionFooter>
<span>{intl.formatMessage({ id: "settings.oidc.info" })}</span>
</Components.FormSectionFooter>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,6 @@ import { useComponents } from "../../context"
import { useIntl } from "react-intl"
import { Node } from "../form/nodes/node"

// const getQrCodeNode = (nodes: UiNode[]): UiNode | undefined =>
// nodes.find(
// (node) => "id" in node.attributes && node.attributes.id === "totp_qr",
// )

// const getTotpSecretNode = (nodes: UiNode[]): UiNode | undefined =>
// nodes.find(
// (node) =>
// "id" in node.attributes && node.attributes.id === "totp_secret_key",
// )

// const getTotpInputNode = (nodes: UiNode[]): UiNode | undefined =>
// nodes.find(
// (node) => "name" in node.attributes && node.attributes.name === "totp_code",
// )

// const getTotpUnlinkInput = (nodes: UiNode[]): UiNode | undefined =>
// nodes.find(
// (node) =>
// "name" in node.attributes && node.attributes.name === "totp_unlink",
// )

const getRegenerateNode = (nodes: UiNode[]): UiNode | undefined =>
nodes.find(
(node) =>
Expand Down Expand Up @@ -54,7 +32,6 @@ export function OrySettingsRecoveryCodes({
}: OrySettingsRecoveryCodesProps) {
const Components = useComponents()
const intl = useIntl()
console.log(nodes)

const codesNode = getRecoveryCodes(nodes)
const revealNode = getRevealNode(nodes)
Expand Down Expand Up @@ -89,7 +66,8 @@ export function OrySettingsRecoveryCodes({
"type" in node.attributes &&
node.attributes.type === "submit" &&
"name" in node.attributes &&
node.attributes.name !== "lookup_secret_reveal",
node.attributes.name !== "lookup_secret_reveal" &&
node.attributes.name !== "lookup_secret_regenerate",
)
.map((node, k) => (
<Node key={k} node={node} />
Expand Down
49 changes: 33 additions & 16 deletions packages/elements-react/src/components/settings/settings-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { OryFormSection } from "../form/section"
import { useIntl } from "react-intl"
import { OrySettingsTotp } from "./totp-settings"
import { OrySettingsRecoveryCodes } from "./recovery-codes-settings"
import { OrySettingsOidc } from "./oidc-settings"
import { OrySettingsWebauthn } from "./webauthn-settings"

interface SettingsSectionProps {
group: UiNodeGroupEnum
Expand All @@ -19,19 +21,41 @@ function SettingsSectionContent({ group, nodes }: SettingsSectionProps) {
const uniqueGroups = useNodesGroups(flow.ui.nodes)

if (group === UiNodeGroupEnum.Totp) {
return <OrySettingsTotp nodes={uniqueGroups.groups.totp ?? []} />
return (
<OryFormSection nodes={uniqueGroups.groups.totp}>
<OrySettingsTotp nodes={uniqueGroups.groups.totp ?? []} />
</OryFormSection>
)
}

if (group === UiNodeGroupEnum.LookupSecret) {
return (
<OrySettingsRecoveryCodes
nodes={uniqueGroups.groups.lookup_secret ?? []}
/>
<OryFormSection nodes={uniqueGroups.groups.lookup_secret}>
<OrySettingsRecoveryCodes
nodes={uniqueGroups.groups.lookup_secret ?? []}
/>
</OryFormSection>
)
}

if (group === UiNodeGroupEnum.Oidc) {
return (
<OryFormSection nodes={uniqueGroups.groups.oidc}>
<OrySettingsOidc nodes={uniqueGroups.groups.oidc ?? []} />
</OryFormSection>
)
}

if (group === UiNodeGroupEnum.Webauthn) {
return (
<OryFormSection nodes={uniqueGroups.groups.webauthn}>
<OrySettingsWebauthn nodes={uniqueGroups.groups.webauthn ?? []} />
</OryFormSection>
)
}

return (
<>
<OryFormSection nodes={nodes}>
<Components.FormSectionContent
title={intl.formatMessage({
id: `settings.${group}.title`,
Expand All @@ -46,8 +70,7 @@ function SettingsSectionContent({ group, nodes }: SettingsSectionProps) {
{nodes
.filter(
(node) =>
group === UiNodeGroupEnum.Oidc ||
("type" in node.attributes && node.attributes.type !== "submit"),
"type" in node.attributes && node.attributes.type !== "submit",
)
.map((node, k) => (
<Node key={k} node={node} />
Expand All @@ -57,15 +80,13 @@ function SettingsSectionContent({ group, nodes }: SettingsSectionProps) {
{nodes
.filter(
(node) =>
"type" in node.attributes &&
group !== UiNodeGroupEnum.Oidc &&
node.attributes.type === "submit",
"type" in node.attributes && node.attributes.type === "submit",
)
.map((node, k) => (
<Node key={k} node={node} />
))}
</Components.FormSectionFooter>
</>
</OryFormSection>
)
}

Expand All @@ -78,10 +99,6 @@ export function OrySettingsCard() {
return null
}

return (
<OryFormSection key={group}>
<SettingsSectionContent group={group} nodes={nodes} />
</OryFormSection>
)
return <SettingsSectionContent key={group} group={group} nodes={nodes} />
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ const getTotpLinkButton = (nodes: UiNode[]): UiNode | undefined =>
(node) => "name" in node.attributes && node.attributes.name === "method",
)

interface OrySettingsTotpProps {
interface HeadlessSettingsTotpProps {
nodes: UiNode[]
}

export function OrySettingsTotp({ nodes }: OrySettingsTotpProps) {
export function OrySettingsTotp({ nodes }: HeadlessSettingsTotpProps) {
const Components = useComponents()
const intl = useIntl()

Expand Down
Loading

0 comments on commit b90c329

Please sign in to comment.