-
Notifications
You must be signed in to change notification settings - Fork 475
feat(ui): add profile email verification and management flows #9737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2bf7d56
0880333
3d60bf1
6e8185d
0c18f82
1c8ee0e
99bd604
74d1866
157c133
0027bc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import type { UserProfileAccountSectionViewProps } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.view'; | ||
| import type { UserProfileAddEmailDialogProps } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-add-email.dialog'; | ||
|
|
||
| interface FixtureOptions { | ||
| failAt?: UserProfileAddEmailDialogProps['step']; | ||
| onVerified?: (emailAddress: string) => void; | ||
| } | ||
|
|
||
| export function createUserProfileAddEmailFixture({ failAt, onVerified }: FixtureOptions = {}): Pick< | ||
| UserProfileAccountSectionViewProps, | ||
| 'onSendEmailCode' | 'onVerifyEmailCode' | ||
| > { | ||
| return { | ||
| onSendEmailCode: async () => { | ||
| await new Promise(resolve => setTimeout(resolve, 700)); | ||
| if (failAt === 'email') { | ||
| throw new Error('We couldn’t send a code. Try again.'); | ||
| } | ||
| }, | ||
| onVerifyEmailCode: async (emailAddress, code) => { | ||
| await new Promise(resolve => setTimeout(resolve, 700)); | ||
| if (failAt === 'verify' || code === '000000') { | ||
| throw new Error('That code is incorrect. Try again.'); | ||
| } | ||
| onVerified?.(emailAddress); | ||
| }, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| export function useUserProfileVerifyEmailLinkFixture({ failResend = false } = {}) { | ||
| const [open, setOpen] = useState(false); | ||
| const [resendSeconds, setResendSeconds] = useState(12); | ||
| const [isResending, setIsResending] = useState(false); | ||
| const [errorMessage, setErrorMessage] = useState<string>(); | ||
|
|
||
| useEffect(() => { | ||
| if (!open) { | ||
| return; | ||
| } | ||
| if (isResending) { | ||
| const timer = setTimeout(() => { | ||
| setIsResending(false); | ||
| if (failResend) { | ||
| setErrorMessage('Unable to send the verification link. Try again.'); | ||
| } else { | ||
| setResendSeconds(12); | ||
| } | ||
| }, 700); | ||
| return () => clearTimeout(timer); | ||
| } | ||
| if (resendSeconds > 0) { | ||
| const timer = setTimeout(() => setResendSeconds(seconds => seconds - 1), 1000); | ||
| return () => clearTimeout(timer); | ||
| } | ||
| }, [open, isResending, resendSeconds, failResend]); | ||
|
|
||
| return { | ||
| open, | ||
| emailAddress: 'example@email.com', | ||
| resendSeconds, | ||
| isResending, | ||
| errorMessage, | ||
| onOpenChange: (value: boolean) => { | ||
| setOpen(value); | ||
| setResendSeconds(12); | ||
| setIsResending(false); | ||
| setErrorMessage(undefined); | ||
| }, | ||
| onResend: () => { | ||
| setErrorMessage(undefined); | ||
| setIsResending(true); | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| export function useUserProfileVerifyEmailSsoFixture({ failConnect = false } = {}) { | ||
| const [open, setOpen] = useState(false); | ||
| const [isConnecting, setIsConnecting] = useState(false); | ||
| const [errorMessage, setErrorMessage] = useState<string>(); | ||
|
|
||
| useEffect(() => { | ||
| if (!isConnecting) { | ||
| return; | ||
| } | ||
| const timer = setTimeout(() => { | ||
| setIsConnecting(false); | ||
| if (failConnect) { | ||
| setErrorMessage('Unable to connect to Okta. Try again.'); | ||
| } else { | ||
| setOpen(false); | ||
| } | ||
| }, 1200); | ||
| return () => clearTimeout(timer); | ||
| }, [isConnecting, failConnect]); | ||
|
|
||
| return { | ||
| open, | ||
| emailAddress: 'example@email.com', | ||
| connection: { | ||
| provider: 'Okta SSO', | ||
| domain: 'acme.co', | ||
| iconUrl: 'https://img.clerk.com/static/okta.svg', | ||
| }, | ||
| isConnecting, | ||
| errorMessage, | ||
| onOpenChange: (value: boolean) => { | ||
| setOpen(value); | ||
| setIsConnecting(false); | ||
| setErrorMessage(undefined); | ||
| }, | ||
| onConnect: () => { | ||
| setErrorMessage(undefined); | ||
| setIsConnecting(true); | ||
| }, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,10 @@ Entering or pasting six digits submits automatically. Use `000000` to see an inc | |
|
|
||
| ## Multiple accounts | ||
|
|
||
| **Add email** opens email entry followed by a six-digit verification code. Resend is available after | ||
| the countdown. In this preview, `000000` shows an incorrect-code error; another six-digit code adds | ||
| the verified address. | ||
|
|
||
| <Story | ||
| name='MultipleAccounts' | ||
| storyModule={Stories} | ||
|
|
@@ -30,6 +34,10 @@ Entering or pasting six digits submits automatically. Use `000000` to see an inc | |
| { name: 'Button', href: '/components/button', layer: 'Components' }, | ||
| { name: 'Badge', href: '/components/badge', layer: 'Components' }, | ||
| { name: 'Icon', href: '/components/icon', layer: 'Components' }, | ||
| { name: 'Dialog', href: '/components/dialog', layer: 'Components' }, | ||
| { name: 'Card', href: '/components/card', layer: 'Components' }, | ||
| { name: 'Flow', href: '/components/flow', layer: 'Components' }, | ||
| { name: 'Otp', href: '/components/otp', layer: 'Components' }, | ||
| ]} | ||
| /> | ||
|
|
||
|
|
@@ -38,3 +46,56 @@ Entering or pasting six digits submits automatically. Use `000000` to see an inc | |
| Add a phone number to see a failed send request while keeping the entered number. | ||
|
|
||
| <Story name='AddPhoneFails' storyModule={Stories} /> | ||
|
|
||
| ## Email verification error | ||
|
|
||
| This example rejects every verification attempt so the error remains visible and the user can retry. | ||
|
|
||
| <Story name='AddEmailFails' storyModule={Stories} /> | ||
|
|
||
| ## Email-link verification | ||
|
|
||
| The profile uses OTP. This separate view displays an email-link verification in progress, with a | ||
| resend countdown and Cancel. Its caller supplies the address, pending state, errors, and callbacks. | ||
|
|
||
| <Story | ||
| name='EmailLinkVerification' | ||
| storyModule={Stories} | ||
| composition={[ | ||
| { name: 'Dialog', href: '/components/dialog', layer: 'Components' }, | ||
| { name: 'Card', href: '/components/card', layer: 'Components' }, | ||
| { name: 'Spinner', href: '/components/spinner', layer: 'Components' }, | ||
| { name: 'Button', href: '/components/button', layer: 'Components' }, | ||
| ]} | ||
| /> | ||
|
|
||
| ### Resend error | ||
|
|
||
| After the countdown, resend to see the supplied error message. | ||
|
|
||
| <Story name='EmailLinkResendFails' storyModule={Stories} /> | ||
|
|
||
| ## Enterprise SSO verification | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Add the required documentation hierarchy. Update this page to include As per coding guidelines: “Playground / Props / Usage are mandatory and always in this order.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| When an email matches an enterprise SSO connection, this view presents the provider and a Connect | ||
| action. The option composes `Item` with the shared provider icon, label, description, and button. | ||
| The caller supplies the connection, pending state, errors, and callback. In this preview, Connect | ||
| simulates completion and closes the dialog. | ||
|
|
||
| <Story | ||
| name='EmailSsoVerification' | ||
| storyModule={Stories} | ||
| composition={[ | ||
| { name: 'Dialog', href: '/components/dialog', layer: 'Components' }, | ||
| { name: 'Card', href: '/components/card', layer: 'Components' }, | ||
| { name: 'Item', href: '/components/item', layer: 'Components' }, | ||
| { name: 'IconFrame', href: '/components/icon-frame', layer: 'Components' }, | ||
| { name: 'Button', href: '/components/button', layer: 'Components' }, | ||
| ]} | ||
| /> | ||
|
|
||
| ### Connection error | ||
|
|
||
| Connect to see the supplied error message. The user can retry or cancel. | ||
|
|
||
| <Story name='EmailSsoConnectFails' storyModule={Stories} /> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Add direct unit tests for the new fixture hook.
The
packages/swingsettesting convention requires unit tests for new functions and components. No test exercisesuseUserProfileVerifyEmailLinkFixturecountdown, failed-resend retry, or pending-resend close paths. Add co-located tests with fake timers.🤖 Prompt for AI Agents