From 83c426aff6891ef4901ef0f3989ed02a88bd0082 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Thu, 12 Nov 2026 17:27:11 -0700 Subject: [PATCH 1/4] feat(mosaic): add the sign-in account chooser --- .changeset/mosaic-sign-in-choice.md | 2 + .../sign-in/__tests__/sign-in.utils.test.ts | 1 + packages/mosaic/src/features/sign-in/index.ts | 1 + .../src/features/sign-in/sign-in.messages.ts | 6 + .../src/features/sign-in/sign-in.types.ts | 12 ++ .../src/features/sign-in/sign-in.view.tsx | 13 +- .../features/sign-in/steps/choice-step.tsx | 26 ++++ .../sign-in/steps/sign-in-accounts.tsx | 135 ++++++++++++++++++ .../swingset/src/stories/sign-in.stories.tsx | 25 ++++ 9 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 .changeset/mosaic-sign-in-choice.md create mode 100644 packages/mosaic/src/features/sign-in/steps/sign-in-accounts.tsx diff --git a/.changeset/mosaic-sign-in-choice.md b/.changeset/mosaic-sign-in-choice.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-sign-in-choice.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts b/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts index 044f3a35b7b..99ca4e65b5a 100644 --- a/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts +++ b/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts @@ -34,6 +34,7 @@ describe('stepKey', () => { [{ kind: 'code', purpose: 'verify-email', channel: 'email', identifier: 'new@acme.com' }, 'code:verify-email'], [{ kind: 'choice', purpose: 'methods', options: [] }, 'choice:methods'], [{ kind: 'choice', purpose: 'reset-methods', options: [] }, 'choice:reset-methods'], + [{ kind: 'choice', purpose: 'accounts', options: [] }, 'choice:accounts'], [{ kind: 'notice', purpose: 'help' }, 'notice:help'], [ { kind: 'code', purpose: 'reset-password', channel: 'email', identifier: 'jane@acme.com' }, diff --git a/packages/mosaic/src/features/sign-in/index.ts b/packages/mosaic/src/features/sign-in/index.ts index a28cda2df2d..59bfc7b4b8d 100644 --- a/packages/mosaic/src/features/sign-in/index.ts +++ b/packages/mosaic/src/features/sign-in/index.ts @@ -1,5 +1,6 @@ export { SignInView } from './sign-in.view'; export type { + SignInAccount, SignInActionBlockedDetails, SignInFactor, SignInIdentifierField, diff --git a/packages/mosaic/src/features/sign-in/sign-in.messages.ts b/packages/mosaic/src/features/sign-in/sign-in.messages.ts index 197abc20d67..255accac921 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.messages.ts +++ b/packages/mosaic/src/features/sign-in/sign-in.messages.ts @@ -33,6 +33,12 @@ export const signInMessages = { description: 'Enter your phone number to get a verification code on {provider}.', label: '{provider} phone number', }, + accounts: { + title: 'Choose an account', + description: 'Select the account with which you wish to continue.', + addAccount: 'Add account', + signOutAll: 'Sign out of all accounts', + }, password: { title: 'Enter your password', description: 'Enter the password associated with your account', diff --git a/packages/mosaic/src/features/sign-in/sign-in.types.ts b/packages/mosaic/src/features/sign-in/sign-in.types.ts index a86e640c2f0..1c3bec2b942 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.types.ts +++ b/packages/mosaic/src/features/sign-in/sign-in.types.ts @@ -38,6 +38,7 @@ export type SignInStep = } | { kind: 'choice'; purpose: 'methods' | 'reset-methods'; options: readonly SignInFactor[] } | { kind: 'choice'; purpose: 'enterprise-connections'; options: readonly SignInProvider[] } + | { kind: 'choice'; purpose: 'accounts'; options: readonly SignInAccount[] } | { kind: 'notice'; purpose: 'help' } | { kind: 'notice'; purpose: 'forgot-password'; identifier: string } | { kind: 'notice'; purpose: 'no-methods' | 'password-compromised' | 'enterprise-sso' } @@ -54,6 +55,13 @@ export interface SignInActionBlockedDetails { traceId?: string; } +export interface SignInAccount { + id: string; + name: string; + identifier: string; + imageUrl?: string; +} + export interface SignInProvider { id: string; name: string; @@ -107,6 +115,10 @@ export interface SignInViewProps { onEmailSupport: () => void; pendingFactorId?: string; onSelectFactor: (id: string) => void; + pendingAccountId?: string; + onSelectAccount?: (id: string) => void; + onAddAccount?: () => void; + onSignOutAll?: () => void; onResend?: () => void; canResend: boolean; resendRemainingSeconds?: number; diff --git a/packages/mosaic/src/features/sign-in/sign-in.view.tsx b/packages/mosaic/src/features/sign-in/sign-in.view.tsx index 1b25f76cbd9..5a6f47e3dc0 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.view.tsx +++ b/packages/mosaic/src/features/sign-in/sign-in.view.tsx @@ -80,6 +80,10 @@ export function SignInView({ onEmailSupport, pendingFactorId, onSelectFactor, + pendingAccountId, + onSelectAccount, + onAddAccount, + onSignOutAll, onResend, canResend, resendRemainingSeconds, @@ -215,7 +219,9 @@ export function SignInView({ ) : null} - + {current.kind === 'choice' ? ( ) : null} diff --git a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx index 33c29754495..5ba04c88a8f 100644 --- a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx +++ b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx @@ -9,6 +9,7 @@ import { SecurityLockSquare } from '../../../icons/glyphs/security-lock-square'; import { SecurityPasskey } from '../../../icons/glyphs/security-passkey'; import { SecurityPhone } from '../../../icons/glyphs/security-phone'; import type { SignInFactor, SignInStep } from '../sign-in.types'; +import { SignInAccounts } from './sign-in-accounts'; import { SignInEnterpriseConnections } from './sign-in-enterprise-connections'; const factorIcon = { @@ -35,6 +36,11 @@ interface ChoiceStepProps { pendingProviderId?: string; onSelectProvider: (id: string) => void; onCancel?: () => void; + pendingAccountId?: string; + isDisabled: boolean; + onSelectAccount?: (id: string) => void; + onAddAccount?: () => void; + onSignOutAll?: () => void; focusRef?: Ref; } @@ -51,8 +57,28 @@ export function ChoiceStep({ pendingProviderId, onSelectProvider, onCancel, + pendingAccountId, + isDisabled, + onSelectAccount, + onAddAccount, + onSignOutAll, focusRef, }: ChoiceStepProps) { + if (step.purpose === 'accounts') { + return ( + onSelectAccount?.(id)} + onAddAccount={onAddAccount} + onSignOutAll={onSignOutAll} + /> + ); + } + if (step.purpose === 'enterprise-connections') { return ( void; + onAddAccount?: () => void; + onSignOutAll?: () => void; + focusRef?: Ref; +} + +export function SignInAccounts({ + accounts, + globalErrorMessage, + pendingAccountId, + isDisabled, + onSelectAccount, + onAddAccount, + onSignOutAll, + focusRef, +}: SignInAccountsProps) { + const m = useMessages('signIn'); + const firstRowRef = useMergeRefs([focusRef, useFlowAutoFocus()]); + + return ( + <> + + {m.accounts.title} + {m.accounts.description} + + + {globalErrorMessage} + + + + {accounts.map((account, index) => ( + onSelectAccount(account.id)} + /> + } + > + + + {account.imageUrl ? ( + + ) : null} + + + + + {account.name} + {account.identifier} + + + {pendingAccountId === account.id ? ( + + ) : ( + + )} + + + ))} + {onAddAccount ? ( + + } + > + + + + + + + {m.accounts.addAccount} + + + ) : null} + {onSignOutAll ? ( + + ) : null} + + + + ); +} diff --git a/packages/swingset/src/stories/sign-in.stories.tsx b/packages/swingset/src/stories/sign-in.stories.tsx index 9007176b0fc..c82d4f50096 100644 --- a/packages/swingset/src/stories/sign-in.stories.tsx +++ b/packages/swingset/src/stories/sign-in.stories.tsx @@ -2,6 +2,7 @@ import { Button } from '@clerk/mosaic/components/button'; import type { FieldsConfig } from '@clerk/mosaic/components/form'; import { useForm } from '@clerk/mosaic/components/form'; import type { + SignInAccount, SignInFactor, SignInIdentifierField, SignInProvider, @@ -471,6 +472,7 @@ function StepPanel({ onCancel, lastUsedProviderId, onJoinWaitlist, + onSelectAccount, }: { step: SignInStep; combined?: boolean; @@ -487,6 +489,7 @@ function StepPanel({ onCancel?: () => void; lastUsedProviderId?: string; onJoinWaitlist?: () => void; + onSelectAccount?: (id: string) => void; }) { const attempted = errorMessage && step.kind !== 'identifier' ? 'incorrect' : ''; const form = useForm({ @@ -522,6 +525,9 @@ function StepPanel({ onEditIdentifier={() => undefined} onSignUp={combined ? undefined : () => undefined} onJoinWaitlist={onJoinWaitlist} + onSelectAccount={onSelectAccount} + onAddAccount={() => undefined} + onSignOutAll={() => undefined} onForgotPassword={() => undefined} onCancel={ onCancel ?? @@ -817,6 +823,25 @@ export function ChoiceEnterpriseConnections() { ); } +const accounts: SignInAccount[] = [ + { + id: 'sess_1', + name: 'Cameron Walker', + identifier: 'cameron@acme.com', + imageUrl: 'https://randomuser.me/api/portraits/men/32.jpg', + }, + { id: 'sess_2', name: 'Jane Doe', identifier: 'jane@acme.com' }, +]; + +export function ChoiceAccounts() { + return ( + undefined} + /> + ); +} + export function ChoiceResetMethods() { return ; } From a80b8dfb857551a9c06f30ffb5571bd13e72b35a Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Thu, 12 Nov 2026 17:45:13 -0700 Subject: [PATCH 2/4] feat(mosaic): add the sign-in Solana wallet chooser --- .../sign-in/__tests__/sign-in.utils.test.ts | 1 + .../src/features/sign-in/sign-in.messages.ts | 4 + .../src/features/sign-in/sign-in.types.ts | 1 + .../src/features/sign-in/sign-in.view.tsx | 8 +- .../features/sign-in/steps/choice-step.tsx | 14 +++ .../sign-in/steps/sign-in-wallets.tsx | 86 +++++++++++++++++++ .../swingset/src/stories/sign-in.stories.tsx | 9 ++ 7 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 packages/mosaic/src/features/sign-in/steps/sign-in-wallets.tsx diff --git a/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts b/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts index 99ca4e65b5a..9941421b022 100644 --- a/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts +++ b/packages/mosaic/src/features/sign-in/__tests__/sign-in.utils.test.ts @@ -35,6 +35,7 @@ describe('stepKey', () => { [{ kind: 'choice', purpose: 'methods', options: [] }, 'choice:methods'], [{ kind: 'choice', purpose: 'reset-methods', options: [] }, 'choice:reset-methods'], [{ kind: 'choice', purpose: 'accounts', options: [] }, 'choice:accounts'], + [{ kind: 'choice', purpose: 'wallets', options: [] }, 'choice:wallets'], [{ kind: 'notice', purpose: 'help' }, 'notice:help'], [ { kind: 'code', purpose: 'reset-password', channel: 'email', identifier: 'jane@acme.com' }, diff --git a/packages/mosaic/src/features/sign-in/sign-in.messages.ts b/packages/mosaic/src/features/sign-in/sign-in.messages.ts index 255accac921..37c2a73d7cf 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.messages.ts +++ b/packages/mosaic/src/features/sign-in/sign-in.messages.ts @@ -33,6 +33,10 @@ export const signInMessages = { description: 'Enter your phone number to get a verification code on {provider}.', label: '{provider} phone number', }, + wallets: { + title: 'Sign in with Solana', + description: 'Select a wallet below to sign in', + }, accounts: { title: 'Choose an account', description: 'Select the account with which you wish to continue.', diff --git a/packages/mosaic/src/features/sign-in/sign-in.types.ts b/packages/mosaic/src/features/sign-in/sign-in.types.ts index 1c3bec2b942..7fbd5fd0dca 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.types.ts +++ b/packages/mosaic/src/features/sign-in/sign-in.types.ts @@ -39,6 +39,7 @@ export type SignInStep = | { kind: 'choice'; purpose: 'methods' | 'reset-methods'; options: readonly SignInFactor[] } | { kind: 'choice'; purpose: 'enterprise-connections'; options: readonly SignInProvider[] } | { kind: 'choice'; purpose: 'accounts'; options: readonly SignInAccount[] } + | { kind: 'choice'; purpose: 'wallets'; options: readonly SignInProvider[] } | { kind: 'notice'; purpose: 'help' } | { kind: 'notice'; purpose: 'forgot-password'; identifier: string } | { kind: 'notice'; purpose: 'no-methods' | 'password-compromised' | 'enterprise-sso' } diff --git a/packages/mosaic/src/features/sign-in/sign-in.view.tsx b/packages/mosaic/src/features/sign-in/sign-in.view.tsx index 5a6f47e3dc0..1de7a27db5c 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.view.tsx +++ b/packages/mosaic/src/features/sign-in/sign-in.view.tsx @@ -220,7 +220,13 @@ export function SignInView({ {current.kind === 'choice' ? ( + ); + } + if (step.purpose === 'accounts') { return ( void; + onBack: () => void; + focusRef?: Ref; +} + +export function SignInWallets({ + wallets, + globalErrorMessage, + pendingWalletId, + onSelectWallet, + onBack, + focusRef, +}: SignInWalletsProps) { + const m = useMessages('signIn'); + const firstRef = useMergeRefs([focusRef, useFlowAutoFocus()]); + const pending = Boolean(pendingWalletId); + + return ( + <> + + {m.wallets.title} + {m.wallets.description} + + + {globalErrorMessage} + + + + {wallets.map((wallet, index) => ( + + } + longLabel={wallet.name} + shortLabel={wallet.name} + isPending={pendingWalletId === wallet.id} + pendingLabel={m.verifying} + onClick={() => onSelectWallet(wallet.id)} + /> + ))} + + + + + + + ); +} diff --git a/packages/swingset/src/stories/sign-in.stories.tsx b/packages/swingset/src/stories/sign-in.stories.tsx index c82d4f50096..6a5567379c5 100644 --- a/packages/swingset/src/stories/sign-in.stories.tsx +++ b/packages/swingset/src/stories/sign-in.stories.tsx @@ -842,6 +842,15 @@ export function ChoiceAccounts() { ); } +const wallets: SignInProvider[] = [ + { id: 'phantom', name: 'Phantom', iconUrl: 'https://img.clerk.com/static/phantom.svg' }, + { id: 'metamask', name: 'MetaMask', iconUrl: 'https://img.clerk.com/static/metamask.svg' }, +]; + +export function ChoiceWallets() { + return ; +} + export function ChoiceResetMethods() { return ; } From 4f9255a5f97d85fe3068cf4647f009bc08bacc3b Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Thu, 12 Nov 2026 17:54:36 -0700 Subject: [PATCH 3/4] feat(mosaic): show social providers above the sign-in alternative methods --- .../verification-method-picker.tsx | 6 ++- .../src/features/sign-in/sign-in.view.tsx | 1 + .../features/sign-in/steps/choice-step.tsx | 15 +++++- .../sign-in/steps/identifier-step.tsx | 37 ++++--------- .../steps/sign-in-provider-buttons.tsx | 53 +++++++++++++++++++ packages/swingset/src/stories/sign-in.mdx | 2 +- .../swingset/src/stories/sign-in.stories.tsx | 19 ++++++- 7 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 packages/mosaic/src/features/sign-in/steps/sign-in-provider-buttons.tsx diff --git a/packages/mosaic/src/blocks/verification/verification-method-picker.tsx b/packages/mosaic/src/blocks/verification/verification-method-picker.tsx index 54c2c7141b9..dc426683d04 100644 --- a/packages/mosaic/src/blocks/verification/verification-method-picker.tsx +++ b/packages/mosaic/src/blocks/verification/verification-method-picker.tsx @@ -1,5 +1,5 @@ import { useMergeRefs } from '@floating-ui/react'; -import type { Ref } from 'react'; +import type { ReactNode, Ref } from 'react'; import { Button } from '../../components/button'; import { Card } from '../../components/card'; @@ -35,6 +35,8 @@ interface VerificationMethodPickerProps { onSelect: (methodId: string) => void; onHelp: () => void; onBack?: () => void; + /** Rendered above the methods, such as a group of social sign-in buttons. */ + providers?: ReactNode; focusRef?: Ref; } @@ -45,6 +47,7 @@ export function VerificationMethodPicker({ onSelect, onHelp, onBack, + providers, focusRef, }: VerificationMethodPickerProps) { const firstMethodRef = useMergeRefs([focusRef, useFlowAutoFocus()]); @@ -56,6 +59,7 @@ export function VerificationMethodPicker({ {messages.description} + {providers} {methods.map((method, index) => { const isPending = pendingMethodId === method.id; diff --git a/packages/mosaic/src/features/sign-in/sign-in.view.tsx b/packages/mosaic/src/features/sign-in/sign-in.view.tsx index 1de7a27db5c..c03b2280861 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.view.tsx +++ b/packages/mosaic/src/features/sign-in/sign-in.view.tsx @@ -245,6 +245,7 @@ export function SignInView({ onBack={onBack} logo={logo} globalErrorMessage={globalErrorMessage} + socialProviders={socialProviders} pendingProviderId={pendingProviderId} onSelectProvider={onSelectProvider} onCancel={onCancel} diff --git a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx index 89fc4416ca2..1e8140d8c25 100644 --- a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx +++ b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx @@ -8,9 +8,10 @@ import { Key } from '../../../icons/glyphs/key'; import { SecurityLockSquare } from '../../../icons/glyphs/security-lock-square'; import { SecurityPasskey } from '../../../icons/glyphs/security-passkey'; import { SecurityPhone } from '../../../icons/glyphs/security-phone'; -import type { SignInFactor, SignInStep } from '../sign-in.types'; +import type { SignInFactor, SignInProvider, SignInStep } from '../sign-in.types'; import { SignInAccounts } from './sign-in-accounts'; import { SignInEnterpriseConnections } from './sign-in-enterprise-connections'; +import { SignInProviderButtons } from './sign-in-provider-buttons'; import { SignInWallets } from './sign-in-wallets'; const factorIcon = { @@ -34,6 +35,7 @@ interface ChoiceStepProps { onBack: () => void; logo?: { src: string; alt: string }; globalErrorMessage?: string; + socialProviders: readonly SignInProvider[]; pendingProviderId?: string; onSelectProvider: (id: string) => void; onCancel?: () => void; @@ -55,6 +57,7 @@ export function ChoiceStep({ onBack, logo, globalErrorMessage, + socialProviders, pendingProviderId, onSelectProvider, onCancel, @@ -120,6 +123,16 @@ export function ChoiceStep({ onSelect={onSelectFactor} onHelp={onShowHelp} onBack={onBack} + providers={ + step.purpose === 'methods' && socialProviders.length > 0 ? ( + + ) : undefined + } /> ); } diff --git a/packages/mosaic/src/features/sign-in/steps/identifier-step.tsx b/packages/mosaic/src/features/sign-in/steps/identifier-step.tsx index 2b117cfc6d6..94250260cb7 100644 --- a/packages/mosaic/src/features/sign-in/steps/identifier-step.tsx +++ b/packages/mosaic/src/features/sign-in/steps/identifier-step.tsx @@ -11,7 +11,6 @@ import { useFlowAutoFocus } from '../../../components/flow'; import type { FieldFeedback, UseFormResult } from '../../../components/form'; import { Input } from '../../../components/input'; import { PhoneInput } from '../../../components/phone-input'; -import { ProviderButton, ProviderButtonGroup } from '../../../components/provider-button'; import { Text } from '../../../components/text'; import { fill, useMessages } from '../../../localization'; import { Form } from '../../../primitives/form'; @@ -23,9 +22,10 @@ import type { SignInSubmitEvent, SignInValues, } from '../sign-in.types'; -import { identifierFieldCopy, splitLastUsedProvider, startPrompt } from '../sign-in.utils'; +import { identifierFieldCopy, startPrompt } from '../sign-in.utils'; import { SignInAlternativeChannel } from './sign-in-alternative-channel'; import { SignInCaptcha } from './sign-in-captcha'; +import { SignInProviderButtons } from './sign-in-provider-buttons'; type IdentifierStepData = Extract; @@ -268,34 +268,15 @@ function SignInProviders({ if (providers.length === 0) { return null; } - const { lastUsed: lastUsedProvider, others: otherProviders } = splitLastUsedProvider(providers, lastUsedProviderId); - const providerButton = (provider: SignInProvider, badge?: string) => ( - - } - longLabel={fill(m.start.socialButton, { provider: provider.name })} - shortLabel={provider.name} - badge={badge} - isPending={pendingProviderId === provider.id} - pendingLabel={m.verifying} - onClick={() => onSelectProvider(provider.id)} - /> - ); - return ( <> - - {otherProviders.map(provider => providerButton(provider))} - +
void; +} + +export function SignInProviderButtons({ + providers, + pendingProviderId, + lastUsedProviderId, + isDisabled, + onSelectProvider, +}: SignInProviderButtonsProps) { + const m = useMessages('signIn'); + const { lastUsed, others } = splitLastUsedProvider(providers, lastUsedProviderId); + const providerButton = (provider: SignInProvider, badge?: string) => ( + + } + longLabel={fill(m.start.socialButton, { provider: provider.name })} + shortLabel={provider.name} + badge={badge} + isPending={pendingProviderId === provider.id} + pendingLabel={m.verifying} + onClick={() => onSelectProvider(provider.id)} + /> + ); + + return ( + + {others.map(provider => providerButton(provider))} + + ); +} diff --git a/packages/swingset/src/stories/sign-in.mdx b/packages/swingset/src/stories/sign-in.mdx index 106f4e5fe52..753cd3d342f 100644 --- a/packages/swingset/src/stories/sign-in.mdx +++ b/packages/swingset/src/stories/sign-in.mdx @@ -198,7 +198,7 @@ Each section is one component in `features/sign-in/steps/`, and each example und ### Choice step -`choice-step.tsx` draws `{ kind: 'choice' }` with `VerificationMethodPicker`: other sign-in methods, or where to send a password reset code. +`choice-step.tsx` draws `{ kind: 'choice' }`. Other sign-in methods and where to send a password reset code use `VerificationMethodPicker`, with the social providers in its `providers` slot when there are any. Enterprise connections, accounts and wallets each have their own screen. #### Methods diff --git a/packages/swingset/src/stories/sign-in.stories.tsx b/packages/swingset/src/stories/sign-in.stories.tsx index 6a5567379c5..3b4e73a714a 100644 --- a/packages/swingset/src/stories/sign-in.stories.tsx +++ b/packages/swingset/src/stories/sign-in.stories.tsx @@ -804,7 +804,24 @@ export function CodeVerifyEmail() { } export function ChoiceMethods() { - return ; + return ( + + ); +} + +export function ChoiceMethodsWithProviders() { + return ( + + ); } export function ChoiceEnterpriseConnections() { From 959e707d96012284a74f52e60652e4e9e9dcd227 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Thu, 12 Nov 2026 18:21:59 -0700 Subject: [PATCH 4/4] refactor(mosaic): draw the sign-in alternative methods as provider buttons, as legacy does --- .../verification-method-picker.tsx | 6 +- .../src/features/sign-in/sign-in.styles.ts | 5 + .../features/sign-in/steps/choice-step.tsx | 44 +++--- .../steps/sign-in-alternative-methods.tsx | 133 ++++++++++++++++++ packages/swingset/src/stories/sign-in.mdx | 2 +- 5 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 packages/mosaic/src/features/sign-in/steps/sign-in-alternative-methods.tsx diff --git a/packages/mosaic/src/blocks/verification/verification-method-picker.tsx b/packages/mosaic/src/blocks/verification/verification-method-picker.tsx index dc426683d04..54c2c7141b9 100644 --- a/packages/mosaic/src/blocks/verification/verification-method-picker.tsx +++ b/packages/mosaic/src/blocks/verification/verification-method-picker.tsx @@ -1,5 +1,5 @@ import { useMergeRefs } from '@floating-ui/react'; -import type { ReactNode, Ref } from 'react'; +import type { Ref } from 'react'; import { Button } from '../../components/button'; import { Card } from '../../components/card'; @@ -35,8 +35,6 @@ interface VerificationMethodPickerProps { onSelect: (methodId: string) => void; onHelp: () => void; onBack?: () => void; - /** Rendered above the methods, such as a group of social sign-in buttons. */ - providers?: ReactNode; focusRef?: Ref; } @@ -47,7 +45,6 @@ export function VerificationMethodPicker({ onSelect, onHelp, onBack, - providers, focusRef, }: VerificationMethodPickerProps) { const firstMethodRef = useMergeRefs([focusRef, useFlowAutoFocus()]); @@ -59,7 +56,6 @@ export function VerificationMethodPicker({ {messages.description} - {providers} {methods.map((method, index) => { const isPending = pendingMethodId === method.id; diff --git a/packages/mosaic/src/features/sign-in/sign-in.styles.ts b/packages/mosaic/src/features/sign-in/sign-in.styles.ts index 8b01564f1ef..c7b74e95d94 100644 --- a/packages/mosaic/src/features/sign-in/sign-in.styles.ts +++ b/packages/mosaic/src/features/sign-in/sign-in.styles.ts @@ -15,6 +15,11 @@ export const styles = stylex.create({ height: space['4'], width: space['4'], }, + buttonStack: { + gap: space['2'], + display: 'flex', + flexDirection: 'column', + }, divider: { gap: space['4'], alignItems: 'center', diff --git a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx index 1e8140d8c25..b36637d2d35 100644 --- a/packages/mosaic/src/features/sign-in/steps/choice-step.tsx +++ b/packages/mosaic/src/features/sign-in/steps/choice-step.tsx @@ -10,8 +10,8 @@ import { SecurityPasskey } from '../../../icons/glyphs/security-passkey'; import { SecurityPhone } from '../../../icons/glyphs/security-phone'; import type { SignInFactor, SignInProvider, SignInStep } from '../sign-in.types'; import { SignInAccounts } from './sign-in-accounts'; +import { SignInAlternativeMethods } from './sign-in-alternative-methods'; import { SignInEnterpriseConnections } from './sign-in-enterprise-connections'; -import { SignInProviderButtons } from './sign-in-provider-buttons'; import { SignInWallets } from './sign-in-wallets'; const factorIcon = { @@ -110,29 +110,41 @@ export function ChoiceStep({ ); } + const methods = step.options.map(factor => ({ + id: factor.id, + label: factorLabel(factor), + icon: factorIcon[factor.strategy], + })); + + if (step.purpose === 'methods') { + return ( + + ); + } + return ( ({ - id: factor.id, - label: factorLabel(factor), - icon: factorIcon[factor.strategy], - }))} + methods={methods} pendingMethodId={pendingFactorId} onSelect={onSelectFactor} onHelp={onShowHelp} onBack={onBack} - providers={ - step.purpose === 'methods' && socialProviders.length > 0 ? ( - - ) : undefined - } /> ); } diff --git a/packages/mosaic/src/features/sign-in/steps/sign-in-alternative-methods.tsx b/packages/mosaic/src/features/sign-in/steps/sign-in-alternative-methods.tsx new file mode 100644 index 00000000000..8a681412b2f --- /dev/null +++ b/packages/mosaic/src/features/sign-in/steps/sign-in-alternative-methods.tsx @@ -0,0 +1,133 @@ +import { useMergeRefs } from '@floating-ui/react'; +import * as stylex from '@stylexjs/stylex'; +import type { Ref } from 'react'; + +import { Button } from '../../../components/button'; +import { Card } from '../../../components/card'; +import { useFlowAutoFocus } from '../../../components/flow'; +import { Icon } from '../../../components/icon'; +import { ProviderButton, ProviderButtonGroup } from '../../../components/provider-button'; +import { Text } from '../../../components/text'; +import type { IconComponent } from '../../../icons/glyph'; +import { useMessages } from '../../../localization'; +import { styles } from '../sign-in.styles'; +import type { SignInProvider } from '../sign-in.types'; +import { SignInProviderButtons } from './sign-in-provider-buttons'; + +export interface SignInMethodOption { + id: string; + label: string; + icon: IconComponent; +} + +interface SignInAlternativeMethodsProps { + title: string; + description: string; + methods: readonly SignInMethodOption[]; + pendingMethodId?: string; + onSelectMethod: (id: string) => void; + socialProviders: readonly SignInProvider[]; + pendingProviderId?: string; + onSelectProvider: (id: string) => void; + isDisabled: boolean; + globalErrorMessage?: string; + onBack: () => void; + onShowHelp: () => void; + focusRef?: Ref; +} + +export function SignInAlternativeMethods({ + title, + description, + methods, + pendingMethodId, + onSelectMethod, + socialProviders, + pendingProviderId, + onSelectProvider, + isDisabled, + globalErrorMessage, + onBack, + onShowHelp, + focusRef, +}: SignInAlternativeMethodsProps) { + const m = useMessages('signIn'); + const firstMethodRef = useMergeRefs([focusRef, useFlowAutoFocus()]); + const busy = isDisabled || Boolean(pendingMethodId); + + return ( + <> + + {title} + {description} + + + {globalErrorMessage} + + +
+ {socialProviders.length > 0 ? ( + + ) : null} + + {methods.map((method, index) => ( + } + longLabel={method.label} + shortLabel={method.label} + isPending={pendingMethodId === method.id} + pendingLabel={m.verifying} + onClick={() => onSelectMethod(method.id)} + /> + ))} + +
+ + + + + {m.alternativeMethods.helpPrompt}{' '} + + +
+ + ); +} diff --git a/packages/swingset/src/stories/sign-in.mdx b/packages/swingset/src/stories/sign-in.mdx index 753cd3d342f..088adb8649b 100644 --- a/packages/swingset/src/stories/sign-in.mdx +++ b/packages/swingset/src/stories/sign-in.mdx @@ -198,7 +198,7 @@ Each section is one component in `features/sign-in/steps/`, and each example und ### Choice step -`choice-step.tsx` draws `{ kind: 'choice' }`. Other sign-in methods and where to send a password reset code use `VerificationMethodPicker`, with the social providers in its `providers` slot when there are any. Enterprise connections, accounts and wallets each have their own screen. +`choice-step.tsx` draws `{ kind: 'choice' }`. Other sign-in methods are full-width buttons under the social providers, as in legacy; where to send a password reset code uses `VerificationMethodPicker`. Enterprise connections, accounts and wallets each have their own screen. #### Methods