Repository navigation
feat(mosaic): add the remaining sign-in code step views #10765
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
Changes from all commits
9640b7e
01db710
fc25ceb
87e7a05
b545287
78d36be
474582d
c2237f2
035aa75
4f51369
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 |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { useMergeRefs } from '@floating-ui/react'; | |
| import type { FormEvent, ReactNode, Ref } from 'react'; | ||
| import { useId } from 'react'; | ||
|
|
||
| import { Banner } from '../../components/banner'; | ||
| import { Button, SubmitButton } from '../../components/button'; | ||
| import { Card } from '../../components/card'; | ||
| import { Field } from '../../components/field'; | ||
|
|
@@ -34,6 +35,8 @@ interface VerificationCodeProps { | |
| globalErrorMessage?: string; | ||
| isPending?: boolean; | ||
| resend?: VerificationCodeResend; | ||
| identity?: ReactNode; | ||
| notice?: string; | ||
|
Member
Author
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. |
||
| onValueChange: (value: string) => void; | ||
| onComplete?: (value: string) => void; | ||
| onSubmit: () => void; | ||
|
|
@@ -49,6 +52,8 @@ export function VerificationCode({ | |
| globalErrorMessage, | ||
| isPending = false, | ||
| resend, | ||
| identity, | ||
| notice, | ||
| onValueChange, | ||
| onComplete, | ||
| onSubmit, | ||
|
|
@@ -71,6 +76,7 @@ export function VerificationCode({ | |
| <Card.Header> | ||
| <Card.Title>{messages.title}</Card.Title> | ||
| <Card.Description>{messages.description}</Card.Description> | ||
| {identity} | ||
| </Card.Header> | ||
| <Card.Banner | ||
| role='alert' | ||
|
|
@@ -122,6 +128,11 @@ export function VerificationCode({ | |
| ) : null} | ||
| </Field.Root> | ||
| </Form> | ||
| {notice ? ( | ||
| <Banner.Root color='warning'> | ||
|
Member
Author
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. In legacy appears to always be a 'warning' variant. |
||
| <Banner.Label>{notice}</Banner.Label> | ||
| </Banner.Root> | ||
| ) : null} | ||
| </Card.Content> | ||
| <Card.Footer> | ||
| {onCancel ? ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import type { MosaicMessages } from '../../localization'; | ||
| import { fill } from '../../localization'; | ||
| import type { SignInStep } from './sign-in.types'; | ||
|
|
||
| export function stepKey(step: SignInStep): string { | ||
|
|
@@ -26,3 +28,126 @@ export function httpsUrl(url: string | undefined): string | undefined { | |
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| type Messages = MosaicMessages['signIn']; | ||
| type CodeStepData = Extract<SignInStep, { kind: 'code' }>; | ||
| type CodeResendCopy = Pick<Messages['emailCode'], 'resendText' | 'resend' | 'resendCountdown'>; | ||
|
|
||
| export function codeCopy( | ||
| step: CodeStepData, | ||
| m: Messages, | ||
| applicationName: string, | ||
| ): { title: string; description: string; fieldLabel: string } { | ||
| const identifier = step.identifier ?? ''; | ||
| if (step.purpose === 'reset-password') { | ||
| const description = | ||
| step.channel === 'phone' ? m.resetPasswordCode.descriptionPhone : m.resetPasswordCode.descriptionEmail; | ||
| return { | ||
| title: m.resetPasswordCode.title, | ||
| description: fill(description, { identifier }), | ||
| fieldLabel: m.resetPasswordCode.label, | ||
| }; | ||
| } | ||
| if (step.purpose === 'verify-email') { | ||
| return { | ||
| title: m.verifyEmail.title, | ||
| description: fill(m.verifyEmail.description, { identifier }), | ||
| fieldLabel: m.verifyEmail.label, | ||
| }; | ||
| } | ||
| if (step.purpose === 'verify-phone') { | ||
| const provider = step.provider; | ||
| return provider | ||
| ? { | ||
| title: fill(m.verifyPhone.titleProvider, { provider }), | ||
| description: fill(m.verifyPhone.descriptionProvider, { provider }), | ||
| fieldLabel: m.verifyPhone.label, | ||
| } | ||
| : { title: m.verifyPhone.title, description: m.verifyPhone.description, fieldLabel: m.verifyPhone.label }; | ||
| } | ||
| if (step.channel === 'totp') { | ||
| return { title: m.totpMfa.title, description: m.totpMfa.description, fieldLabel: m.totpMfa.label }; | ||
| } | ||
| const copy = channelCopy(step, m); | ||
| const description = sentToIdentifier(step) | ||
| ? fill(copy.descriptionSentTo, { identifier }) | ||
| : fill(copy.description, { applicationName }); | ||
| const title = | ||
| step.channel === 'phone' && step.provider | ||
| ? fill(m.alternativePhoneCode.title, { provider: step.provider }) | ||
| : copy.title; | ||
| return { title, description, fieldLabel: copy.label }; | ||
| } | ||
|
|
||
| export function codeResendCopy(step: CodeStepData, m: Messages): CodeResendCopy { | ||
| if (step.purpose === 'reset-password') { | ||
| return m.resetPasswordCode; | ||
| } | ||
| if (step.purpose === 'verify-email') { | ||
| return m.verifyEmail; | ||
| } | ||
| if (step.purpose === 'verify-phone') { | ||
| return m.verifyPhone; | ||
| } | ||
| return channelCopy(step, m); | ||
| } | ||
|
|
||
| function channelCopy(step: CodeStepData, m: Messages) { | ||
| if (step.channel === 'phone' && step.provider) { | ||
| return m.alternativePhoneCode; | ||
| } | ||
| if (step.channel === 'phone') { | ||
| return step.purpose === 'first-factor' ? m.phoneCode : m.phoneCodeMfa; | ||
| } | ||
| return m.emailCode; | ||
| } | ||
|
|
||
| function identifierEditable(step: CodeStepData): boolean { | ||
| return step.purpose === 'first-factor' || step.purpose === 'sso-bypass' || step.purpose === 'verify-phone'; | ||
| } | ||
|
|
||
| function sentToIdentifier(step: CodeStepData): boolean { | ||
| return Boolean(step.identifier) && !identifierEditable(step); | ||
| } | ||
|
|
||
| export function codeIdentity(step: CodeStepData, m: Messages): { identifier?: string; editLabel: string } { | ||
| return { | ||
| identifier: step.channel !== 'totp' && identifierEditable(step) ? step.identifier : undefined, | ||
| editLabel: step.channel === 'phone' ? m.editPhoneNumber : m.editIdentifier, | ||
| }; | ||
| } | ||
|
|
||
| export function codeNotice(step: CodeStepData, m: Messages): string | undefined { | ||
| switch (step.purpose) { | ||
| case 'client-trust': | ||
| return m.clientTrust.notice; | ||
| case 'sso-bypass': | ||
| return m.ssoBypass.notice; | ||
| case 'first-factor': | ||
| case 'second-factor': | ||
| case 'reset-password': | ||
| case 'verify-email': | ||
| case 'verify-phone': | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| export function codeSecondaryAction( | ||
| step: CodeStepData, | ||
| m: Messages, | ||
| { onCancel, onBack }: { onCancel?: () => void; onBack: () => void }, | ||
| ): { label: string; onClick?: () => void } { | ||
| if (step.purpose === 'reset-password') { | ||
| return { label: m.forgotPassword.back, onClick: onCancel }; | ||
| } | ||
| if (step.channel === 'phone' && step.provider) { | ||
| return { label: m.alternativePhoneCode.useSms, onClick: onCancel }; | ||
|
Comment on lines
+143
to
+144
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. Preserve Back when an alternative-channel step has no For a second-factor or client-trust phone step with Use the provider-specific action only when its callback exists. Otherwise, preserve the Back fallback.
|
||
| } | ||
| if (step.purpose === 'verify-phone') { | ||
| return { label: m.useAnotherMethod, onClick: undefined }; | ||
| } | ||
| if ((step.purpose === 'second-factor' || step.purpose === 'client-trust') && !onCancel) { | ||
| return { label: m.back, onClick: onBack }; | ||
| } | ||
| return { label: m.useAnotherMethod, onClick: onCancel }; | ||
| } | ||

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.
Needed to support identity preview: