Skip to content

Commit

Permalink
feat: add settings screen
Browse files Browse the repository at this point in the history
  • Loading branch information
mszekiel authored and jonas-jonas committed Oct 22, 2024
1 parent cbf29d8 commit 35c0f70
Show file tree
Hide file tree
Showing 56 changed files with 5,978 additions and 4,332 deletions.
8,720 changes: 4,544 additions & 4,176 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"tsup": "8.2.4",
"typedoc": "0.23.16",
"typescript": "5.2.2",
"typescript-eslint": "8.9.0",
"vite": "5.4.3",
"vite-plugin-dts": "4.1.0",
"vite-plugin-require": "1.2.14",
Expand Down
2 changes: 2 additions & 0 deletions packages/elements-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@ory/client-fetch": "1.15.6",
"@radix-ui/react-dropdown-menu": "2.1.2",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"input-otp": "1.2.4",
"lodash.merge": "4.6.2",
Expand All @@ -47,6 +48,7 @@
"@svgr/plugin-svgo": "8.1.0",
"@types/lodash.merge": "4.6.9",
"esbuild-plugin-svgr": "3.0.0",
"@svgr/plugin-jsx": "^8.1.0",
"eslint-plugin-react": "7.37.1",
"postcss": "8.4.47",
"tsup": "8.3.0"
Expand Down
6 changes: 5 additions & 1 deletion packages/elements-react/postcss.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

module.exports = {
plugins: [require("tailwindcss")(), require("autoprefixer")()],
plugins: [
require("tailwindcss")(),
require("autoprefixer")(),
// require("postcss-prefix-selector")({ prefix: ".ory-default-theme" }),
],
}
6 changes: 3 additions & 3 deletions packages/elements-react/src/components/card/card-two-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function OryTwoStepCard() {
const uniqueGroups = useNodesGroups(ui.nodes)

const options: UiNodeGroupEnum[] = Object.values(UiNodeGroupEnum)
.filter((group) => uniqueGroups[group]?.length)
.filter((group) => uniqueGroups.groups[group]?.length)
.filter(
(group) =>
!(
Expand All @@ -61,10 +61,10 @@ export function OryTwoStepCard() {
).includes(group),
)

const hasOidc = Boolean(uniqueGroups.oidc?.length)
const hasOidc = Boolean(uniqueGroups.groups[UiNodeGroupEnum.Oidc]?.length)

const zeroStepGroups = filterZeroStepGroups(ui.nodes)
const finalNodes = getFinalNodes(uniqueGroups, selectedGroup)
const finalNodes = getFinalNodes(uniqueGroups.groups, selectedGroup)

const step = selectedGroup
? ProcessStep.ExecuteAuthMethod
Expand Down
9 changes: 3 additions & 6 deletions packages/elements-react/src/components/form/form-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +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 { OryFlowContainer } from "../../util"

export function computeDefaultValues(
flowContainer: OryFlowContainer,
): 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
81 changes: 75 additions & 6 deletions packages/elements-react/src/components/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import {
UiNode,
UiNodeGroupEnum,
UpdateLoginFlowBody,
UpdateRecoveryFlowBody,
UpdateRegistrationFlowBody,
Expand All @@ -24,6 +26,7 @@ import {
OryNodeTextProps,
OryCurrentIdentifierProps,
OryCardLogoProps,
OryFormSectionContentProps,
} from "../../types"
import { OryCardDividerProps } from "../generic/divider"
import { OryFormGroupProps, OryFormGroups } from "./groups"
Expand Down Expand Up @@ -52,6 +55,14 @@ import { onSubmitVerification } from "../../util/onSubmitVerification"
import { onSubmitRecovery } from "../../util/onSubmitRecovery"
import { onSubmitSettings } from "../../util/onSubmitSettings"
import { OryPageHeaderProps } from "../generic"
import { OryFormSectionProps } from "./section"
import {
OrySettingsOidcProps,
OrySettingsPasskeyProps,
OrySettingsRecoveryCodesProps,
OrySettingsTotpProps,
OrySettingsWebauthnProps,
} from "../settings"

/**
* A record of all the components that are used in the OryForm component.
Expand Down Expand Up @@ -174,6 +185,26 @@ export type OryFlowComponents = {
Page: {
Header: ComponentType<OryPageHeaderProps>
}
Settings: {
/**
* The SettingsSection component is rendered around each section of the settings.
*/
Section: ComponentType<OryFormSectionProps>
/**
* The SettingsSectionContent component is rendered around the content of each section of the settings.
*/
SectionContent: ComponentType<OryFormSectionContentProps>
/**
* The SettingsSectionFooter component is rendered around the footer of each section of the settings.
*/
SectionFooter: ComponentType<OryFormSectionContentProps>

Oidc: ComponentType<OrySettingsOidcProps>
Webauthn: ComponentType<OrySettingsWebauthnProps>
Passkey: ComponentType<OrySettingsPasskeyProps>
Totp: ComponentType<OrySettingsTotpProps>
RecoveryCodes: ComponentType<OrySettingsRecoveryCodesProps>
}
}

type DeepPartialTwoLevels<T> = {
Expand All @@ -182,14 +213,23 @@ type DeepPartialTwoLevels<T> = {

export type OryFlowComponentOverrides = DeepPartialTwoLevels<OryFlowComponents>

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

export function OryForm({ children }: OryFormProps) {
export function OryForm({ children, nodes }: OryFormProps) {
const { Form } = 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 @@ -206,7 +246,7 @@ export function OryForm({ children }: OryFormProps) {

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

const onSubmit: SubmitHandler<FormValues> = async (data) => {
Expand Down Expand Up @@ -264,13 +304,42 @@ export function OryForm({ children }: OryFormProps) {
})
break
}
case FlowType.Settings:
case FlowType.Settings: {
const submitData: UpdateSettingsFlowBody = {
...(data as unknown as UpdateSettingsFlowBody),
}

if ("totp_unlink" in submitData) {
submitData.method = "totp"
}

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

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

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

if ("passkey_remove" in submitData) {
submitData.method = "passkey"
}

await onSubmitSettings(flowContainer, {
onRedirect,
setFlowContainer: handleSuccess,
body: data as unknown as UpdateSettingsFlowBody,
body: submitData,
})
break
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/elements-react/src/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./form"
export * from "./groups"
export * from "./messages"
export * from "./social"
export * from "./section"
11 changes: 11 additions & 0 deletions packages/elements-react/src/components/form/nodes/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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 @@ -18,6 +19,8 @@ export const NodeInput = ({
onClick?: MouseEventHandler
}): ReactNode => {
const { Node } = useComponents()
const { setValue } = useFormContext()

const nodeType = attributes.type
const {
onloadTrigger: onloadTrigger,
Expand All @@ -29,9 +32,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 @@ -43,6 +53,7 @@ export const NodeInput = ({
)

const handleClick: MouseEventHandler = () => {
setFormValue()
if (onclickTrigger) {
triggerToWindowCall(onclickTrigger)
}
Expand Down
21 changes: 21 additions & 0 deletions packages/elements-react/src/components/form/section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { PropsWithChildren } from "react"
import { useComponents } from "../../context/component"
import { OryForm } from "./form"
import { UiNode } from "@ory/client-fetch"

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

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

return (
<OryForm nodes={nodes}>
<Settings.Section>{children}</Settings.Section>
</OryForm>
)
}
1 change: 1 addition & 0 deletions packages/elements-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
export * from "./card"
export * from "./form"
export * from "./generic"
export * from "./settings"
38 changes: 38 additions & 0 deletions packages/elements-react/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { UiNode } from "@ory/client-fetch"

export * from "./settings-card"

export type OrySettingsRecoveryCodesProps = {
codes: string[]
regnerateButton?: UiNode
revealButton?: UiNode
}

export type OrySettingsTotpProps =
| {
totpImage: UiNode
totpSecret: UiNode
totpInput: UiNode
}
| {
totpUnlink: UiNode
}

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

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

export type OrySettingsPasskeyProps = {
triggerButton: UiNode & { onClick: () => void }
removeButtons: UiNode[]
}
45 changes: 45 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,45 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

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",
)

export interface HeadlessSettingsOidcProps {
nodes: UiNode[]
}

export function OrySettingsOidc({ nodes }: HeadlessSettingsOidcProps) {
const { Settings } = useComponents()
const intl = useIntl()

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

return (
<>
<Settings.SectionContent
title={intl.formatMessage({ id: "settings.oidc.title" })}
description={intl.formatMessage({ id: "settings.oidc.description" })}
>
<Settings.Oidc
linkButtons={linkButtons}
unlinkButtons={unlinkButtons}
/>
</Settings.SectionContent>
<Settings.SectionFooter>
<span>{intl.formatMessage({ id: "settings.oidc.info" })}</span>
</Settings.SectionFooter>
</>
)
}
Loading

0 comments on commit 35c0f70

Please sign in to comment.