Skip to content
Merged
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/chubby-donkeys-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const reverificationMessages = {
emailCode: {
description: 'Enter the code sent to your email to continue',
formTitle: 'Verification code',
resendButton: 'Didn’t receive a code? Resend',
resendButton: 'Resend',
resendCountdown: 'Resend ({seconds})',
resendText: 'Didn’t receive a code?',
title: 'Verification required',
},
passkey: {
Expand All @@ -44,13 +46,17 @@ export const reverificationMessages = {
phoneCode: {
description: 'Enter the code sent to your phone to continue',
formTitle: 'Verification code',
resendButton: 'Didn’t receive a code? Resend',
resendButton: 'Resend',
resendCountdown: 'Resend ({seconds})',
resendText: 'Didn’t receive a code?',
title: 'Verification required',
},
phoneCodeMfa: {
description: 'Enter the code sent to your phone to continue',
formTitle: 'Verification code',
resendButton: 'Didn’t receive a code? Resend',
resendButton: 'Resend',
resendCountdown: 'Resend ({seconds})',
resendText: 'Didn’t receive a code?',
title: 'Verification required',
},
error: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as stylex from '@stylexjs/stylex';
import type { Ref } from 'react';

import { Button } from '../../components/button';
Expand All @@ -10,7 +11,8 @@ import { SecurityLockSquare } from '../../icons/glyphs/security-lock-square';
import { SecurityPasskey } from '../../icons/glyphs/security-passkey';
import { SecurityPhone } from '../../icons/glyphs/security-phone';
import type { MosaicMessages } from '../../localization';
import { fill, useMessages } from '../../localization';
import { fill, rich, useMessages } from '../../localization';
import { tabularNumbersStyle } from '../../styles/typography.styles';
import { styles } from './reverification.styles';
import type {
ReverificationErrorReason,
Expand Down Expand Up @@ -150,11 +152,22 @@ export function ReverificationView({
pendingLabel: m.verifying,
};
const hasAlternatives = methods.length > 0;
const resendLabel = otpChannel === 'phone' ? m.phoneCode.resendButton : m.emailCode.resendButton;
const resendCopy = otpChannel === 'phone' ? m.phoneCode : m.emailCode;
const resend =
otpChannel === 'email' || otpChannel === 'phone'
? {
label: resendRemainingSeconds ? `${resendLabel} (${resendRemainingSeconds})` : resendLabel,
text: resendCopy.resendText,
label: resendRemainingSeconds ? (
<span>
{rich(resendCopy.resendCountdown, {
values: {
seconds: <span {...stylex.props(tabularNumbersStyle.enabled)}>{resendRemainingSeconds}</span>,
},
})}
</span>
) : (
resendCopy.resendButton
),
disabled: !canResend || isPending,
onClick: onResend,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,17 @@ export function ReverificationMethodPicker({
</Item.Root>
);
})}
{onBack ? (
<Button
type='button'
variant='ghost'
disabled={Boolean(pendingMethodId)}
onClick={onBack}
>
{messages.backButton}
</Button>
) : null}
</Item.Group>
{onBack ? (
<Button
type='button'
variant='ghost'
fullWidth
disabled={Boolean(pendingMethodId)}
onClick={onBack}
>
{messages.backButton}
</Button>
) : null}
<Text
size='xs'
xstyle={styles.helpText}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMergeRefs } from '@floating-ui/react';
import type { FormEvent, Ref } from 'react';
import type { FormEvent, ReactNode, Ref } from 'react';
import { useId } from 'react';

import { Button, SubmitButton } from '../../../components/button';
Expand All @@ -18,7 +18,8 @@ export interface ReverificationOtpMessages {
}

export interface ReverificationOtpResend {
label: string;
text: string;
label: ReactNode;
disabled?: boolean;
onClick: () => void;
}
Expand Down Expand Up @@ -93,15 +94,18 @@ export function ReverificationOTP({
<Field.Error>{errorMessage}</Field.Error>
</Field.Message>
{resend ? (
<Button
type='button'
size='sm'
variant='link'
disabled={resend.disabled || isPending}
onClick={resend.onClick}
>
{resend.label}
</Button>
<Field.Description>
{resend.text}{' '}
<Button
type='button'
size='sm'
variant='link'
disabled={resend.disabled || isPending}
onClick={resend.onClick}
>
{resend.label}
</Button>
</Field.Description>
) : null}
</Field.Root>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('UserProfileAddEmailDialog', () => {
const verify = screen.getByRole('button', { name: 'Verify', exact: true });
expect(verify).toHaveAttribute('aria-busy', 'true');
await user.click(verify);
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend' }));
await user.click(screen.getByRole('button', { name: 'Resend' }));

expect(props.onSubmit).not.toHaveBeenCalled();
expect(props.onResend).not.toHaveBeenCalled();
Expand All @@ -159,13 +159,15 @@ describe('UserProfileAddEmailDialog', () => {
const field = screen.getByRole('textbox', { name: step === 'email' ? 'Email' : 'Verification code' });
expect(field).toHaveAttribute('aria-invalid', 'true');
const describedControl = step === 'verify' ? screen.getByRole('group', { name: 'Verification code' }) : field;
expect(describedControl).toHaveAccessibleDescription('Please try again.');
expect(describedControl).toHaveAccessibleDescription(
step === 'verify' ? 'Please try again. Didn’t receive a code? Resend' : 'Please try again.',
);
});

it('allows resending only after the countdown and the current request finish', async () => {
const user = userEvent.setup();
const { props, rerender } = renderView({ step: 'verify', resendSeconds: 12 });
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend (12)' }));
await user.click(screen.getByRole('button', { name: 'Resend (12)' }));
expect(props.onResend).not.toHaveBeenCalled();

rerender(
Expand All @@ -176,7 +178,7 @@ describe('UserProfileAddEmailDialog', () => {
/>
</MosaicProvider>,
);
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend' }));
await user.click(screen.getByRole('button', { name: 'Resend' }));
expect(props.onResend).toHaveBeenCalledOnce();

rerender(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import { Otp } from '../../../components/otp';
import { Text } from '../../../components/text';
import { ArrowUpRight } from '../../../icons/glyphs/arrow-up-right';
import { fill, rich, useMessages } from '../../../localization';
import { tabularNumbersStyle } from '../../../styles/typography.styles';
import type { ReverificationController } from '../../reverification';
import { Reverification } from '../../reverification';
import { styles as panelStyles } from '../user-profile-profile-panel.styles';

const styles = stylex.create({
ssoConnection: {
Expand Down Expand Up @@ -253,13 +253,16 @@ function VerifyEmailStep(props: VerifyEmailStepProps) {
<Field.Message>
<Field.Error>{props.errorMessage}</Field.Error>
</Field.Message>
<ResendButton
labels={m.verify}
onResend={props.onResend}
disabled={props.isPending}
isResending={props.isResending}
resendSeconds={props.resendSeconds}
/>
<Field.Description>
{m.verify.resendText}{' '}
<ResendButton
labels={m.verify}
onResend={props.onResend}
disabled={props.isPending}
isResending={props.isResending}
resendSeconds={props.resendSeconds}
/>
</Field.Description>
</Field.Root>
</Card.Content>
<Card.Footer>
Expand Down Expand Up @@ -435,7 +438,7 @@ function ResendButton(props: ResendButtonProps) {
) : resendSeconds > 0 ? (
<span>
{rich(props.labels.resendCountdown, {
values: { seconds: <span {...stylex.props(panelStyles.countdown)}>{resendSeconds}</span> },
values: { seconds: <span {...stylex.props(tabularNumbersStyle.enabled)}>{resendSeconds}</span> },
})}
</span>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export const userProfileAddEmailMessages = {
submit: 'Verify',
pending: 'Verifying',
cancel: 'Cancel',
resend: 'Didn’t receive a code? Resend',
resend: 'Resend',
resendText: 'Didn’t receive a code?',
resending: 'Sending code…',
resendCountdown: 'Didn’t receive a code? Resend ({seconds})',
resendCountdown: 'Resend ({seconds})',
},
link: {
title: 'Verify your email',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('User profile MFA', () => {
expect(screen.getByText('You cannot set your last identification as second factor.')).toBeVisible(),
);
expect(screen.getByRole('combobox', { name: /Phone number/ })).toHaveAccessibleDescription(
'You cannot set your last identification as second factor.',
'Add a new phone number You cannot set your last identification as second factor.',
);
expect(fapi.mfa.phoneUpdates).toHaveLength(0);
expect(screen.getByRole('button', { name: 'Back' })).toBeVisible();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,19 @@ function SelectPhoneStep(props: UserProfileAddSmsViewProps & { inputRef?: Ref<HT
<Field.Message>
<Field.Error>{props.errorMessage}</Field.Error>
</Field.Message>
<Field.Description>
<Button
type='button'
variant='link'
color='neutral'
size='sm'
disabled={props.isPending}
onClick={props.onAddPhone}
>
{m.addPhone}
</Button>
</Field.Description>
</Field.Root>
<Button
type='button'
variant='link'
color='neutral'
size='sm'
disabled={props.isPending}
onClick={props.onAddPhone}
>
{m.addPhone}
</Button>
</Card.Content>
<Card.Footer>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('UserProfileAddPhoneDialog', () => {
const verify = screen.getByRole('button', { name: 'Verify', exact: true });
expect(verify).toHaveAttribute('aria-busy', 'true');
await user.click(verify);
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend' }));
await user.click(screen.getByRole('button', { name: 'Resend' }));

expect(props.onSubmit).not.toHaveBeenCalled();
expect(props.onResend).not.toHaveBeenCalled();
Expand All @@ -145,13 +145,15 @@ describe('UserProfileAddPhoneDialog', () => {
const field = screen.getByRole('textbox', { name: step === 'phone' ? 'Phone' : 'Verification code' });
expect(field).toHaveAttribute('aria-invalid', 'true');
const describedControl = step === 'verify' ? screen.getByRole('group', { name: 'Verification code' }) : field;
expect(describedControl).toHaveAccessibleDescription('Please try again.');
expect(describedControl).toHaveAccessibleDescription(
step === 'verify' ? 'Please try again. Didn’t receive a code? Resend' : 'Please try again.',
);
});

it('allows resending only after the countdown and the current request finish', async () => {
const user = userEvent.setup();
const { props, rerender } = renderView({ step: 'verify', resendSeconds: 12 });
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend (12)' }));
await user.click(screen.getByRole('button', { name: 'Resend (12)' }));
expect(props.onResend).not.toHaveBeenCalled();

rerender(
Expand All @@ -162,7 +164,7 @@ describe('UserProfileAddPhoneDialog', () => {
/>
</MosaicProvider>,
);
await user.click(screen.getByRole('button', { name: 'Didn’t receive a code? Resend' }));
await user.click(screen.getByRole('button', { name: 'Resend' }));
expect(props.onResend).toHaveBeenCalledOnce();

rerender(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('the user profile phone section', () => {

await addPhone(actor, '8015550100');

expect(await screen.findByRole('button', { name: /Didn’t receive a code\? Resend \(\d+\)/ })).toBeDisabled();
expect(await screen.findByRole('button', { name: /Resend \(\d+\)/ })).toBeDisabled();
});

it('verifies a number that was left unverified', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export const userProfileAddPhoneMessages = {
submit: 'Verify',
pending: 'Verifying',
cancel: 'Cancel',
resend: 'Didn’t receive a code? Resend',
resend: 'Resend',
resendText: 'Didn’t receive a code?',
resending: 'Sending a new code…',
resendCountdown: 'Didn’t receive a code? Resend ({seconds})',
resendCountdown: 'Resend ({seconds})',
},
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Otp } from '../../components/otp';
import type { CountryIso } from '../../components/phone-input';
import { PhoneInput } from '../../components/phone-input';
import { fill, rich, useMessages } from '../../localization';
import { styles } from './user-profile-profile-panel.styles';
import { tabularNumbersStyle } from '../../styles/typography.styles';

interface EnterPhoneStepProps {
inputRef?: Ref<HTMLInputElement>;
Expand Down Expand Up @@ -147,26 +147,31 @@ export function VerifyPhoneStep(props: VerifyPhoneStepProps) {
<Field.Message>
<Field.Error>{props.errorMessage}</Field.Error>
</Field.Message>
<Button
type='button'
size='sm'
variant='link'
color='neutral'
disabled={props.isPending || props.isResending || (props.resendSeconds ?? 0) > 0}
onClick={props.onResend}
>
{props.isResending ? (
m.verify.resending
) : (props.resendSeconds ?? 0) > 0 ? (
<span>
{rich(m.verify.resendCountdown, {
values: { seconds: <span {...stylex.props(styles.countdown)}>{props.resendSeconds}</span> },
})}
</span>
) : (
m.verify.resend
)}
</Button>
<Field.Description>
{m.verify.resendText}{' '}
<Button
type='button'
size='sm'
variant='link'
color='neutral'
disabled={props.isPending || props.isResending || (props.resendSeconds ?? 0) > 0}
onClick={props.onResend}
>
{props.isResending ? (
m.verify.resending
) : (props.resendSeconds ?? 0) > 0 ? (
<span>
{rich(m.verify.resendCountdown, {
values: {
seconds: <span {...stylex.props(tabularNumbersStyle.enabled)}>{props.resendSeconds}</span>,
},
})}
</span>
) : (
m.verify.resend
)}
</Button>
</Field.Description>
</Field.Root>
</Card.Content>
<Card.Footer>
Expand Down
Loading
Loading