Skip to content

Commit

Permalink
feat: feature complete settings cards
Browse files Browse the repository at this point in the history
  • Loading branch information
mszekiel committed Oct 7, 2024
1 parent b90c329 commit 7f434be
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 10 deletions.
6 changes: 5 additions & 1 deletion packages/elements-react/src/components/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function OryForm({ children, nodes }: OryFormProps) {
if (submitData.method === "code" && data.code) {
submitData.resend = ""
}
console.log(submitData)

await onSubmitLogin(flowContainer, {
onRedirect,
setFlowContainer: handleSuccess,
Expand Down Expand Up @@ -197,6 +197,10 @@ export function OryForm({ children, nodes }: OryFormProps) {
submitData.method = "webauthn"
}

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

await onSubmitSettings(flowContainer, {
onRedirect,
setFlowContainer: handleSuccess,
Expand Down
6 changes: 6 additions & 0 deletions packages/elements-react/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ export type OrySettingsWebauthnProps = {
removeButtons: UiNode[]
}

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

export type OrySettingsComponents = {
SettingsRecoveryCodes: ComponentType<OrySettingsRecoveryCodesProps>
SettingsTotp: ComponentType<OrySettingsTotpProps>
SettingsOidc: ComponentType<OrySettingsOidcProps>
SettingsWebauthn: ComponentType<OrySettingsWebauthnProps>
SettingsPasskey: ComponentType<OrySettingsPasskeyProps>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
UiNode,
UiNodeAttributes,
UiNodeInputAttributes,
} from "@ory/client-fetch"
import { useComponents, useOryFlow } from "../../context"
import { useIntl } from "react-intl"
import { triggerToWindowCall, useNodesGroups } from "../../util/ui"
import { Node } from "../form/nodes/node"

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

const getSettingsNodes = (nodes: UiNode[]): UiNode[] =>
nodes.filter(
(node) =>
"name" in node.attributes &&
(node.attributes.name === "passkey_settings_register" ||
node.attributes.name === "passkey_create_data"),
)

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

interface OrySettingsPasskeyProps {
nodes: UiNode[]
}

export function OrySettingsPasskey({ nodes }: OrySettingsPasskeyProps) {
const Components = useComponents()
const intl = useIntl()
const { flow } = useOryFlow()
const { groups } = useNodesGroups(flow.ui.nodes)

const triggerButton = getTriggerNode(nodes)
const settingsNodes = getSettingsNodes(nodes)
const removeNodes = getRemoveNodes(nodes)

if (!triggerButton) {
return null
}

const {
onclick: _onClick,
onclickTrigger,
...triggerAttributes
} = triggerButton.attributes as UiNodeInputAttributes

const onTriggerClick = () => {
triggerToWindowCall(onclickTrigger)
}

return (
<>
<Components.FormSectionContent
title={intl.formatMessage({ id: "settings.passkey.title" })}
description={intl.formatMessage({
id: "settings.passkey.description",
})}
>
{groups.default?.map((node, i) => (
<Node key={`passkey-default-nodes-${i}`} node={node} />
))}
{settingsNodes.map((node, i) => (
<Node key={`passkey-settings-nodes-${i}`} node={node} />
))}
<Components.SettingsPasskey
triggerButton={{
...triggerButton,
attributes: triggerAttributes as UiNodeAttributes,
onClick: onTriggerClick,
}}
removeButtons={removeNodes}
/>
</Components.FormSectionContent>
<Components.FormSectionFooter>
<span>{intl.formatMessage({ id: "settings.passkey.info" })}</span>
</Components.FormSectionFooter>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { OrySettingsTotp } from "./totp-settings"
import { OrySettingsRecoveryCodes } from "./recovery-codes-settings"
import { OrySettingsOidc } from "./oidc-settings"
import { OrySettingsWebauthn } from "./webauthn-settings"
import { OrySettingsPasskey } from "./passkey-settings"

interface SettingsSectionProps {
group: UiNodeGroupEnum
Expand Down Expand Up @@ -54,6 +55,14 @@ function SettingsSectionContent({ group, nodes }: SettingsSectionProps) {
)
}

if (group === UiNodeGroupEnum.Passkey) {
return (
<OryFormSection nodes={uniqueGroups.groups.passkey}>
<OrySettingsPasskey nodes={uniqueGroups.groups.passkey ?? []} />
</OryFormSection>
)
}

return (
<OryFormSection nodes={nodes}>
<Components.FormSectionContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
UiNodeAttributes,
UiNodeInputAttributes,
} from "@ory/client-fetch"
import { useComponents } from "../../context"
import { useComponents, useOryFlow } from "../../context"
import { useIntl } from "react-intl"
import { triggerToWindowCall } from "../../util/ui"
import { triggerToWindowCall, useNodesGroups } from "../../util/ui"
import { Node } from "../form/nodes/node"

const getInputNode = (nodes: UiNode[]): UiNode | undefined =>
nodes.find(
Expand All @@ -24,7 +25,19 @@ const getTriggerNode = (nodes: UiNode[]): UiNode | undefined =>
const getRemoveButtons = (nodes: UiNode[]): UiNode[] =>
nodes.filter(
(node) =>
"name" in node.attributes && node.attributes.name === "passkey_remove",
"name" in node.attributes && node.attributes.name === "webauthn_remove",
)

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

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

interface OrySettingsWebauthnProps {
Expand All @@ -34,10 +47,14 @@ interface OrySettingsWebauthnProps {
export function OrySettingsWebauthn({ nodes }: OrySettingsWebauthnProps) {
const Components = useComponents()
const intl = useIntl()
const { flow } = useOryFlow()
const { groups } = useNodesGroups(flow.ui.nodes)

const triggerButton = getTriggerNode(nodes)
const inputNode = getInputNode(nodes)
const removeButtons = getRemoveButtons(nodes)
const scriptNode = getScriptNode(nodes)
const registerNode = getRegisterNode(nodes)

if (!inputNode || !triggerButton) {
return null
Expand All @@ -61,6 +78,11 @@ export function OrySettingsWebauthn({ nodes }: OrySettingsWebauthnProps) {
id: "settings.webauthn.description",
})}
>
{groups.default?.map((node, i) => (
<Node key={`webauthn-default-${i}`} node={node} />
))}
{scriptNode && <Node node={scriptNode} />}
{registerNode && <Node node={registerNode} />}
<Components.SettingsWebauthn
nameInput={inputNode}
triggerButton={{
Expand Down
11 changes: 6 additions & 5 deletions packages/elements-react/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,11 @@
"settings.title": "Account Settings",
"settings.navigation.title": "Account Settings",
"settings.password.title": "Change Password",
"settings.profile.title": "Profile Settings",
"settings.webauthn.title": "Manage Hardware Tokens",
"settings.passkey.title": "Manage Passkeys",
"settings.password.description": "Modify your password",
"settings.profile.title": "Profile Settings",
"settings.profile.description": "Update your profile information",
"settings.webauthn.title": "Manage Hardware Tokens",
"settings.webauthn.description": "Manage your hardware token settings",
"settings.passkey.description": "Manage your passkey settings",
"verification.registration-button": "Sign up",
"verification.registration-label": "Don't have an account?",
"verification.title": "Verify your account",
Expand All @@ -191,5 +189,8 @@
"settings.oidc.title": "Connected accounts",
"settings.oidc.description": "Connect a social login provider with your account.",
"settings.oidc.info": "Connected accounts from these providers can be used to login to your account",
"settings.webauthn.info": "Hardware Tokens are used for second-factor authentication or as first-factor with Passkeys"
"settings.webauthn.info": "Hardware Tokens are used for second-factor authentication or as first-factor with Passkeys",
"settings.passkey.title": "Manage Passkeys",
"settings.passkey.description": "Manage your passkey settings",
"settings.passkey.info": "Manage your passkey settings"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { DefaultSettingsRecoveryCodes } from "./settings/settings-recovery-codes
import { DefaultSettingsTotp } from "./settings/settings-top"
import { DefaultSettingsOidc } from "./settings/settings-oidc"
import { DefaultSettingsWebauthn } from "./settings/settings-webauthn"
import { DefaultSettingsPasskey } from "./settings/settings-passkey"

export const OryDefaultComponents: OryFlowComponents = {
Card: DefaultCard,
Expand Down Expand Up @@ -75,4 +76,5 @@ export const OryDefaultComponents: OryFlowComponents = {
SettingsTotp: DefaultSettingsTotp,
SettingsOidc: DefaultSettingsOidc,
SettingsWebauthn: DefaultSettingsWebauthn,
SettingsPasskey: DefaultSettingsPasskey,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { OrySettingsPasskeyProps } from "@ory/elements-react"
import { UiNodeInputAttributes } from "@ory/client-fetch"
import { DefaultButton } from "../form/button"
import { DefaultHorizontalDivider } from "../form/horizontal-divider"
import Passkey from "../../assets/icons/passkey.svg"
import Trash from "../../assets/icons/trash.svg"

export function DefaultSettingsPasskey({
triggerButton,
removeButtons,
}: OrySettingsPasskeyProps) {
const hasRemoveButtons = removeButtons.length > 0

return (
<div className="flex flex-col gap-8">
<div className="flex gap-3 items-end max-w-[60%]">
{triggerButton ? (
<DefaultButton
node={triggerButton}
attributes={triggerButton.attributes as UiNodeInputAttributes}
onClick={triggerButton.onClick}
/>
) : null}
</div>
{hasRemoveButtons ? (
<div className="flex flex-col gap-8">
<DefaultHorizontalDivider />
<div className="flex flex-col gap-2">
{removeButtons.map((node, i) => {
const context = node.meta.label?.context ?? {}
const addedAt =
"added_at" in context ? (context.added_at as string) : null
const diaplyName =
"display_name" in context
? (context.display_name as string)
: null
const keyId =
"value" in node.attributes ? node.attributes.value : null

return (
<div
className="flex justify-between gap-6"
key={`webauthn-remove-button-${i}`}
>
<Passkey size={32} className="text-dialog-fg-default" />
<div className="flex-1 flex-col">
<p className="text-sm font-medium text-dialog-fg-subtle">
{diaplyName}
</p>
<span className="text-sm text-dialog-fg-mute">{keyId}</span>
</div>
{addedAt && (
<p className="text-sm self-center text-dialog-fg-mute">
{new Date(addedAt).toLocaleDateString()}
</p>
)}
<button
{...(node.attributes as UiNodeInputAttributes)}
type="submit"
className="cursor-pointer text-links-link-mute-default hover:text-links-link-mute-hover"
>
<Trash size={20} />
</button>
</div>
)
})}
</div>
</div>
) : null}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { UiNodeInputAttributes } from "@ory/client-fetch"
import { DefaultLabel } from "../form/label"
import { DefaultInput } from "../form/input"
import { DefaultButton } from "../form/button"
import { DefaultHorizontalDivider } from "../form/horizontal-divider"
import Key from "../../assets/icons/key.svg"
import Trash from "../../assets/icons/trash.svg"

export function DefaultSettingsWebauthn({
nameInput,
triggerButton,
removeButtons,
}: OrySettingsWebauthnProps) {
const hasRemoveButtons = removeButtons.length > 0

return (
<div className="flex flex-col gap-8">
<div className="flex gap-3 items-end max-w-[60%]">
Expand All @@ -31,6 +36,51 @@ export function DefaultSettingsWebauthn({
/>
) : null}
</div>
{hasRemoveButtons ? (
<div className="flex flex-col gap-8">
<DefaultHorizontalDivider />
<div className="flex flex-col gap-2">
{removeButtons.map((node, i) => {
const context = node.meta.label?.context ?? {}
const addedAt =
"added_at" in context ? (context.added_at as string) : null
const diaplyName =
"display_name" in context
? (context.display_name as string)
: null
const keyId =
"value" in node.attributes ? node.attributes.value : null

return (
<div
className="flex justify-between gap-6"
key={`webauthn-remove-button-${i}`}
>
<Key size={32} className="text-dialog-fg-default" />
<div className="flex-1 flex-col">
<p className="text-sm font-medium text-dialog-fg-subtle">
{diaplyName}
</p>
<span className="text-sm text-dialog-fg-mute">{keyId}</span>
</div>
{addedAt && (
<p className="text-sm self-center text-dialog-fg-mute">
{new Date(addedAt).toLocaleDateString()}
</p>
)}
<button
{...(node.attributes as UiNodeInputAttributes)}
type="submit"
className="cursor-pointer text-links-link-mute-default hover:text-links-link-mute-hover"
>
<Trash size={20} />
</button>
</div>
)
})}
</div>
</div>
) : null}
</div>
)
}
Loading

0 comments on commit 7f434be

Please sign in to comment.