Skip to content

Commit 75cac36

Browse files
feat(settings): new FlowSetup2faBackupCodeConfirm component
Because: * The design for 2FA setup has been updated * We want the UI component to be reusable * We want to document the component and states in storybook This commit: * Creates a new UI component with l10n, storybook and unit tests * Updates FormVerifyTotp to allow custom styling * Does not hook up the component in the setup flow - that will be done in a later ticket Closes #FXA-11625
1 parent 92d6cab commit 75cac36

14 files changed

Lines changed: 291 additions & 3 deletions

File tree

packages/fxa-settings/src/components/FormVerifyTotp/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getLocalizedErrorMessage } from '../../lib/error-utils';
99
import { useFtlMsgResolver } from '../../models';
1010
import { AuthUiErrors } from '../../lib/auth-errors/auth-errors';
1111
import { FormVerifyTotpProps, VerifyTotpFormData } from './interfaces';
12+
import classNames from 'classnames';
1213

1314
// Split inputs are not recommended for accesibility
1415
// Code inputs should be a single input field
@@ -25,6 +26,7 @@ const FormVerifyTotp = ({
2526
setErrorMessage,
2627
verifyCode,
2728
gleanDataAttrs,
29+
className = '',
2830
}: FormVerifyTotpProps) => {
2931
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
3032

@@ -87,7 +89,7 @@ const FormVerifyTotp = ({
8789
return (
8890
<form
8991
noValidate
90-
className="flex flex-col gap-4 my-6"
92+
className={classNames('flex flex-col gap-4', className)}
9193
onSubmit={handleSubmit(onSubmit)}
9294
>
9395
{/* Using `type="text" inputmode="numeric"` shows the numeric keyboard on mobile
@@ -105,7 +107,7 @@ const FormVerifyTotp = ({
105107
spellCheck={false}
106108
inputRef={register({ required: true })}
107109
hasErrors={!!errorMessage}
108-
aria-describedby={errorBannerId}
110+
ariaDescribedBy={errorBannerId}
109111
/>
110112
<button
111113
type="submit"

packages/fxa-settings/src/components/FormVerifyTotp/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type FormVerifyTotpProps = {
1515
setErrorMessage: React.Dispatch<React.SetStateAction<string>>;
1616
verifyCode: (code: string) => void;
1717
gleanDataAttrs?: GleanClickEventDataAttrs;
18+
className?: string;
1819
};
1920

2021
export type VerifyTotpFormData = {

packages/fxa-settings/src/components/InputText/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type InputTextProps = {
4848
required?: boolean;
4949
tooltipPosition?: 'top' | 'bottom';
5050
isPasswordInput?: boolean;
51+
ariaDescribedBy?: string;
5152
};
5253

5354
export const InputText = ({
@@ -80,6 +81,7 @@ export const InputText = ({
8081
required,
8182
tooltipPosition,
8283
isPasswordInput = false,
84+
ariaDescribedBy,
8385
}: InputTextProps) => {
8486
const [focused, setFocused] = useState<boolean>(false);
8587
const [hasContent, setHasContent] = useState<boolean>(defaultValue != null);
@@ -176,6 +178,7 @@ export const InputText = ({
176178
data-testid={formatDataTestId('input-field')}
177179
onChange={textFieldChange}
178180
ref={combinedRef}
181+
aria-describedby={ariaDescribedBy}
179182
{...{
180183
name,
181184
disabled,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## The backup code confirm step of the setup 2 factor authentication flow,
2+
## where the user confirm that they have saved their backup authentication codes
3+
## by entering one of them.
4+
5+
flow-setup-2fa-backup-code-confirm-heading = Enter backup authentication code
6+
7+
# codes here refers to backup authentication codes
8+
flow-setup-2fa-backup-code-confirm-confirm-saved = Confirm you saved your codes by entering one. Without these codes, you might not be able to sign in if you don’t have your authenticator app.
9+
10+
flow-setup-2fa-backup-code-confirm-code-input = Enter 10-character code
11+
12+
# Clicking on this button finishes the whole flow upon success.
13+
flow-setup-2fa-backup-code-confirm-button-finish = Finish
14+
15+
##
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React, { useState } from 'react';
6+
import { Meta } from '@storybook/react';
7+
import { withLocalization } from 'fxa-react/lib/storybooks';
8+
import SettingsLayout from '../SettingsLayout';
9+
import { action } from '@storybook/addon-actions';
10+
import { FlowSetup2faBackupCodeConfirm } from '.';
11+
12+
export default {
13+
title: 'Components/Settings/FlowSetup2faBackupCodeConfirm',
14+
component: FlowSetup2faBackupCodeConfirm,
15+
decorators: [withLocalization],
16+
} as Meta;
17+
18+
const navigateBackward = async () => {
19+
action('navigateBackward')();
20+
};
21+
22+
const verifyCode = (code: string) => {
23+
action('verifyCode')(code);
24+
return Promise.resolve();
25+
};
26+
27+
export const Default = () => (
28+
<SettingsLayout>
29+
<FlowSetup2faBackupCodeConfirm
30+
currentStep={3}
31+
numberOfSteps={3}
32+
localizedFlowTitle="Two-step authentication"
33+
onBackButtonClick={navigateBackward}
34+
showProgressBar
35+
errorMessage=""
36+
setErrorMessage={() => {}}
37+
verifyCode={verifyCode}
38+
/>
39+
</SettingsLayout>
40+
);
41+
42+
export const WithError = () => {
43+
const [errorMessage, setErrorMessage] = useState('');
44+
const verifyCodeError = (code: string) => {
45+
action('verifyCode')(code);
46+
setErrorMessage('Invalid recovery code');
47+
return Promise.resolve();
48+
};
49+
return (
50+
<SettingsLayout>
51+
<FlowSetup2faBackupCodeConfirm
52+
currentStep={3}
53+
numberOfSteps={3}
54+
localizedFlowTitle="Two-step authentication"
55+
onBackButtonClick={navigateBackward}
56+
showProgressBar
57+
{...{ errorMessage, setErrorMessage }}
58+
verifyCode={verifyCodeError}
59+
/>
60+
</SettingsLayout>
61+
);
62+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React from 'react';
6+
import { screen } from '@testing-library/react';
7+
import userEvent from '@testing-library/user-event';
8+
9+
import { FlowSetup2faBackupCodeConfirm } from '.';
10+
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
11+
import GleanMetrics from '../../../lib/glean';
12+
import { GleanClickEventType2FA } from '../../../lib/types';
13+
14+
const renderFlowSetup2faBackupCodeConfirm = (error?: string) => {
15+
const onBackButtonClick = jest.fn();
16+
const verifyCode = jest.fn();
17+
const setErrorMessage = jest.fn();
18+
return {
19+
onBackButtonClick,
20+
verifyCode,
21+
setErrorMessage,
22+
...renderWithLocalizationProvider(
23+
<FlowSetup2faBackupCodeConfirm
24+
currentStep={3}
25+
numberOfSteps={3}
26+
localizedFlowTitle="Two-step authentication"
27+
showProgressBar
28+
errorMessage={error || ''}
29+
{...{ onBackButtonClick, setErrorMessage, verifyCode }}
30+
/>
31+
),
32+
};
33+
};
34+
35+
describe('FlowSetup2faBackupCodeDownload', () => {
36+
it('renders correctly', () => {
37+
renderFlowSetup2faBackupCodeConfirm();
38+
expect(screen.getByRole('progressbar')).toBeVisible();
39+
expect(
40+
screen.getByRole('textbox', { name: 'Enter 10-character code' })
41+
).toBeVisible();
42+
expect(screen.getByRole('button', { name: 'Finish' })).toBeVisible();
43+
});
44+
45+
it('shows error correctly and clears error on input', async () => {
46+
const errorMsg = 'Invalid recovery code';
47+
const { setErrorMessage } = renderFlowSetup2faBackupCodeConfirm(errorMsg);
48+
expect(screen.getByText(errorMsg)).toBeVisible();
49+
await userEvent.type(
50+
screen.getByRole('textbox', { name: /Enter 10-character code/ }),
51+
'12345'
52+
);
53+
expect(setErrorMessage).toHaveBeenCalledWith('');
54+
});
55+
56+
it('sets up Glean metrics correctly', () => {
57+
const gleanSpy = jest.spyOn(
58+
GleanMetrics.accountPref,
59+
'twoStepAuthEnterCodeView'
60+
);
61+
renderFlowSetup2faBackupCodeConfirm();
62+
expect(gleanSpy).toBeCalled();
63+
64+
const finishButton = screen.getByRole('button', { name: 'Finish' });
65+
66+
expect(finishButton).toHaveAttribute(
67+
'data-glean-id',
68+
'two_step_auth_enter_code_submit'
69+
);
70+
expect(finishButton).toHaveAttribute(
71+
'data-glean-type',
72+
GleanClickEventType2FA.setup
73+
);
74+
});
75+
76+
it('calls onBackButtonClick when the back button is clicked', async () => {
77+
const { onBackButtonClick } = renderFlowSetup2faBackupCodeConfirm();
78+
const cancelButton = screen.getByRole('button', { name: 'Back' });
79+
await userEvent.click(cancelButton);
80+
expect(onBackButtonClick).toHaveBeenCalled();
81+
});
82+
83+
it('calls onRecoveryCodeSubmit when the input code is of valid format and the finish button is clicked', async () => {
84+
const { verifyCode } = renderFlowSetup2faBackupCodeConfirm();
85+
const finishButton = screen.getByRole('button', { name: 'Finish' });
86+
const recoveryCodeInput = screen.getByRole('textbox', {
87+
name: 'Enter 10-character code',
88+
});
89+
expect(finishButton).toBeDisabled();
90+
await userEvent.type(recoveryCodeInput, '12345');
91+
expect(finishButton).toBeDisabled();
92+
await userEvent.type(recoveryCodeInput, 'abcde');
93+
// After entering 10 characters, the button should now be enabled
94+
expect(finishButton).toBeEnabled();
95+
await userEvent.click(finishButton);
96+
expect(verifyCode).toHaveBeenCalledWith('12345abcde');
97+
});
98+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React, { Dispatch, SetStateAction, useEffect } from 'react';
6+
import { FtlMsg } from 'fxa-react/lib/utils';
7+
import FlowContainer from '../FlowContainer';
8+
import ProgressBar from '../ProgressBar';
9+
import { GleanClickEventType2FA } from '../../../lib/types';
10+
import GleanMetrics from '../../../lib/glean';
11+
import { BackupCodesImage } from '../../images';
12+
import FormVerifyTotp from '../../FormVerifyTotp';
13+
import { useFtlMsgResolver } from '../../../models';
14+
import Banner from '../../Banner';
15+
16+
type FlowSetup2faBackupCodeConfirmProps = {
17+
currentStep?: number;
18+
numberOfSteps?: number;
19+
hideBackButton?: boolean;
20+
localizedFlowTitle: string;
21+
onBackButtonClick?: () => void;
22+
showProgressBar?: boolean;
23+
verifyCode: (code: string) => Promise<void>;
24+
errorMessage: string;
25+
setErrorMessage: Dispatch<SetStateAction<string>>;
26+
reason?: GleanClickEventType2FA;
27+
};
28+
29+
export const FlowSetup2faBackupCodeConfirm = ({
30+
currentStep,
31+
numberOfSteps,
32+
hideBackButton = false,
33+
localizedFlowTitle,
34+
onBackButtonClick,
35+
showProgressBar = true,
36+
verifyCode,
37+
setErrorMessage,
38+
errorMessage,
39+
reason = GleanClickEventType2FA.setup,
40+
}: FlowSetup2faBackupCodeConfirmProps) => {
41+
useEffect(() => {
42+
GleanMetrics.accountPref.twoStepAuthEnterCodeView({
43+
event: { reason },
44+
});
45+
}, [reason]);
46+
47+
const ftlMsgResolver = useFtlMsgResolver();
48+
49+
return (
50+
<FlowContainer
51+
title={localizedFlowTitle}
52+
{...{ hideBackButton, onBackButtonClick }}
53+
>
54+
{showProgressBar && currentStep != null && numberOfSteps != null && (
55+
<ProgressBar {...{ currentStep, numberOfSteps }} />
56+
)}
57+
{errorMessage && (
58+
<Banner
59+
type="error"
60+
bannerId="backup-code-confirm-error"
61+
content={{ localizedDescription: errorMessage }}
62+
/>
63+
)}
64+
<BackupCodesImage />
65+
<FtlMsg id="flow-setup-2fa-backup-code-confirm-heading">
66+
<h2 className="font-bold text-xl my-2">
67+
Enter backup authentication code
68+
</h2>
69+
</FtlMsg>
70+
<FtlMsg id="flow-setup-2fa-backup-code-confirm-confirm-saved">
71+
<p>
72+
Confirm you saved your codes by entering one. Without these codes, you
73+
might not be able to sign in if you don’t have your authenticator app.
74+
</p>
75+
</FtlMsg>
76+
<FormVerifyTotp
77+
codeType="alphanumeric"
78+
codeLength={10}
79+
{...{ verifyCode, errorMessage, setErrorMessage }}
80+
localizedSubmitButtonText={ftlMsgResolver.getMsg(
81+
'flow-setup-2fa-backup-code-confirm-button-finish',
82+
'Finish'
83+
)}
84+
localizedInputLabel={ftlMsgResolver.getMsg(
85+
'flow-setup-2fa-backup-code-confirm-code-input',
86+
'Enter 10-character code'
87+
)}
88+
errorBannerId="backup-code-confirm-error"
89+
clearBanners={() => setErrorMessage('')}
90+
gleanDataAttrs={{
91+
id: 'two_step_auth_enter_code_submit',
92+
type: reason,
93+
}}
94+
className="mt-6"
95+
/>
96+
</FlowContainer>
97+
);
98+
};

packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export const FlowSetupRecoveryPhoneConfirmCode = ({
164164
id: 'two_step_auth_phone_verify_submit',
165165
type: reason,
166166
}}
167+
className="my-6"
167168
/>
168169
<div className="flex flex-wrap gap-2 mt-6 justify-center text-center">
169170
<FtlMsg id="flow-setup-phone-confirm-code-expired">

packages/fxa-settings/src/pages/ResetPassword/ConfirmBackupCodeResetPassword/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const ConfirmBackupCodeResetPassword = ({
7979
id: 'password_reset_backup_code_submit',
8080
}}
8181
errorBannerId="confirm-backup-code-reset-password-error-banner"
82+
className="my-6"
8283
/>
8384

8485
<div className="mt-5 link-blue text-sm flex justify-end">

packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const ConfirmResetPassword = ({
8989
setErrorMessage,
9090
verifyCode,
9191
}}
92+
className="my-6"
9293
/>
9394
<LinkRememberPassword {...{ email }} clickHandler={signinClickHandler} />
9495
<div className="flex justify-between mt-5 text-sm">

0 commit comments

Comments
 (0)