Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-sign-in-choice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ 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: 'choice', purpose: 'wallets', options: [] }, 'choice:wallets'],
[{ kind: 'notice', purpose: 'help' }, 'notice:help'],
[
{ kind: 'code', purpose: 'reset-password', channel: 'email', identifier: 'jane@acme.com' },
Expand Down
1 change: 1 addition & 0 deletions packages/mosaic/src/features/sign-in/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { SignInView } from './sign-in.view';
export type {
SignInAccount,
SignInActionBlockedDetails,
SignInFactor,
SignInIdentifierField,
Expand Down
10 changes: 10 additions & 0 deletions packages/mosaic/src/features/sign-in/sign-in.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ 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.',
addAccount: 'Add account',
signOutAll: 'Sign out of all accounts',
},
password: {
title: 'Enter your password',
description: 'Enter the password associated with your account',
Expand Down
5 changes: 5 additions & 0 deletions packages/mosaic/src/features/sign-in/sign-in.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions packages/mosaic/src/features/sign-in/sign-in.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ 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' }
Expand All @@ -54,6 +56,13 @@ export interface SignInActionBlockedDetails {
traceId?: string;
}

export interface SignInAccount {
id: string;
name: string;
identifier: string;
imageUrl?: string;
}

export interface SignInProvider {
id: string;
name: string;
Expand Down Expand Up @@ -107,6 +116,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;
Expand Down
20 changes: 19 additions & 1 deletion packages/mosaic/src/features/sign-in/sign-in.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export function SignInView({
onEmailSupport,
pendingFactorId,
onSelectFactor,
pendingAccountId,
onSelectAccount,
onAddAccount,
onSignOutAll,
onResend,
canResend,
resendRemainingSeconds,
Expand Down Expand Up @@ -215,7 +219,15 @@ export function SignInView({
) : null}
</Flow.Step>

<Flow.Step ids={['choice:methods', 'choice:reset-methods', 'choice:enterprise-connections']}>
<Flow.Step
ids={[
'choice:methods',
'choice:reset-methods',
'choice:enterprise-connections',
'choice:accounts',
'choice:wallets',
]}
>
{current.kind === 'choice' ? (
<ChoiceStep
focusRef={focusRef}
Expand All @@ -233,9 +245,15 @@ export function SignInView({
onBack={onBack}
logo={logo}
globalErrorMessage={globalErrorMessage}
socialProviders={socialProviders}
pendingProviderId={pendingProviderId}
onSelectProvider={onSelectProvider}
onCancel={onCancel}
pendingAccountId={pendingAccountId}
isDisabled={isDisabled}
onSelectAccount={onSelectAccount}
onAddAccount={onAddAccount}
onSignOutAll={onSignOutAll}
/>
) : null}
</Flow.Step>
Expand Down
77 changes: 71 additions & 6 deletions packages/mosaic/src/features/sign-in/steps/choice-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ 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 { SignInAlternativeMethods } from './sign-in-alternative-methods';
import { SignInEnterpriseConnections } from './sign-in-enterprise-connections';
import { SignInWallets } from './sign-in-wallets';

const factorIcon = {
password: SecurityLockSquare,
Expand All @@ -32,9 +35,15 @@ interface ChoiceStepProps {
onBack: () => void;
logo?: { src: string; alt: string };
globalErrorMessage?: string;
socialProviders: readonly SignInProvider[];
pendingProviderId?: string;
onSelectProvider: (id: string) => void;
onCancel?: () => void;
pendingAccountId?: string;
isDisabled: boolean;
onSelectAccount?: (id: string) => void;
onAddAccount?: () => void;
onSignOutAll?: () => void;
focusRef?: Ref<HTMLElement>;
}

Expand All @@ -48,11 +57,45 @@ export function ChoiceStep({
onBack,
logo,
globalErrorMessage,
socialProviders,
pendingProviderId,
onSelectProvider,
onCancel,
pendingAccountId,
isDisabled,
onSelectAccount,
onAddAccount,
onSignOutAll,
focusRef,
}: ChoiceStepProps) {
if (step.purpose === 'wallets') {
return (
<SignInWallets
focusRef={focusRef}
wallets={step.options}
globalErrorMessage={globalErrorMessage}
pendingWalletId={pendingProviderId}
onSelectWallet={onSelectProvider}
onBack={onBack}
/>
);
}

if (step.purpose === 'accounts') {
return (
<SignInAccounts
focusRef={focusRef}
accounts={step.options}
globalErrorMessage={globalErrorMessage}
pendingAccountId={pendingAccountId}
isDisabled={isDisabled}
onSelectAccount={id => onSelectAccount?.(id)}
onAddAccount={onAddAccount}
onSignOutAll={onSignOutAll}
/>
);
}

if (step.purpose === 'enterprise-connections') {
return (
<SignInEnterpriseConnections
Expand All @@ -67,15 +110,37 @@ export function ChoiceStep({
);
}

const methods = step.options.map(factor => ({
id: factor.id,
label: factorLabel(factor),
icon: factorIcon[factor.strategy],
}));

if (step.purpose === 'methods') {
return (
<SignInAlternativeMethods
focusRef={focusRef}
title={messages.title}
description={messages.description}
methods={methods}
pendingMethodId={pendingFactorId}
onSelectMethod={onSelectFactor}
socialProviders={socialProviders}
pendingProviderId={pendingProviderId}
onSelectProvider={onSelectProvider}
isDisabled={isDisabled}
globalErrorMessage={globalErrorMessage}
onBack={onBack}
onShowHelp={onShowHelp}
/>
);
}

return (
<VerificationMethodPicker
focusRef={focusRef}
messages={messages}
methods={step.options.map(factor => ({
id: factor.id,
label: factorLabel(factor),
icon: factorIcon[factor.strategy],
}))}
methods={methods}
pendingMethodId={pendingFactorId}
onSelect={onSelectFactor}
onHelp={onShowHelp}
Expand Down
37 changes: 9 additions & 28 deletions packages/mosaic/src/features/sign-in/steps/identifier-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<SignInStep, { kind: 'identifier' }>;

Expand Down Expand Up @@ -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) => (
<ProviderButton
key={provider.id}
icon={
<img
src={provider.iconUrl}
alt=''
{...stylex.props(styles.providerIcon)}
/>
}
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 (
<>
<ProviderButtonGroup
disabled={isDisabled}
lastUsed={lastUsedProvider ? providerButton(lastUsedProvider, m.start.lastUsed) : undefined}
>
{otherProviders.map(provider => providerButton(provider))}
</ProviderButtonGroup>
<SignInProviderButtons
providers={providers}
pendingProviderId={pendingProviderId}
lastUsedProviderId={lastUsedProviderId}
isDisabled={isDisabled}
onSelectProvider={onSelectProvider}
/>
<div
role='separator'
{...stylex.props(styles.divider)}
Expand Down
Loading
Loading