Skip to content

Commit d72d088

Browse files
Merge pull request #18941 from mozilla/FXA-11624
feat(settings): new FlowSetup2faBackupCodeDownload component
2 parents 20a17f9 + 85960ef commit d72d088

8 files changed

Lines changed: 297 additions & 13 deletions

File tree

packages/fxa-settings/src/components/DataBlock/index.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default {
1616

1717
export const SingleCode = () => <Subject />;
1818

19-
export const SingleCodeOnIOS = () => <Subject isIOS />;
19+
export const SingleCodeOnIOS = () => <Subject isMobile />;
2020

2121
export const MultipleCodes = () => (
2222
<Subject

packages/fxa-settings/src/components/DataBlock/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ it('can render multiple values', () => {
3838
});
3939

4040
it('displays only Copy icon in iOS', () => {
41-
renderWithLocalizationProvider(<Subject value={multiValue} isIOS />);
41+
renderWithLocalizationProvider(<Subject value={multiValue} isMobile />);
4242

4343
expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument();
4444
expect(

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type DataBlockProps = {
2727
prefixDataTestId?: string;
2828
onCopy?: (event: React.ClipboardEvent<HTMLElement>) => void;
2929
onAction?: actionFn;
30-
isIOS?: boolean;
30+
isMobile?: boolean;
3131
email: string;
3232
gleanDataAttrs: {
3333
copy?: GetDataTrioGleanData;
@@ -42,7 +42,7 @@ export const DataBlock = ({
4242
prefixDataTestId,
4343
onCopy,
4444
onAction = () => {},
45-
isIOS = false,
45+
isMobile = false,
4646
email,
4747
gleanDataAttrs,
4848
}: DataBlockProps) => {
@@ -61,10 +61,10 @@ export const DataBlock = ({
6161
};
6262

6363
return (
64-
<div className="w-full flex flex-col gap-3 items-center bg-white rounded-xl border-2 border-grey-100 px-5 pt-5 pb-3">
64+
<div className="w-full flex flex-col items-center bg-white rounded-xl border-2 border-grey-100 p-5">
6565
<ul
6666
className={classNames(
67-
'relative gap-2 w-full text-black text-sm font-mono font-bold',
67+
'relative gap-2 mobileLandscape:gap-4 w-full mb-5 text-black text-sm font-mono font-bold',
6868
valueIsArray ? 'grid grid-cols-2 max-w-sm' : 'flex flex-col max-w-lg'
6969
)}
7070
{...{ onCopy }}
@@ -88,13 +88,13 @@ export const DataBlock = ({
8888
prefixDataTestId={`datablock-${performedAction}`}
8989
message={actionTypeToNotification[performedAction]}
9090
anchorPosition="middle"
91-
position="bottom"
91+
position="top"
9292
className="mt-1"
9393
></Tooltip>
9494
</FtlMsg>
9595
)}
9696
</ul>
97-
{isIOS ? (
97+
{isMobile ? (
9898
<GetDataCopySingleton
9999
{...{
100100
value,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export type GetDataTrioProps = {
4747
};
4848

4949
const trioButtonClassName =
50-
'w-24 h-20 shrink p-1 relative text-grey-600 text-sm rounded-xl flex flex-col items-center justify-center hover:text-blue-600 active:text-blue-500 focus-visible-default outline-offset-2 hover:bg-gradient-to-tr hover:from-blue-600/10 hover:to-purple-500/10 active:bg-gradient-to-tr active:from-blue-600/10 active:to-purple-500/10 focus-visible:bg-gradient-to-tr focus-visible:from-blue-600/10 focus-visible:to-purple-500/10';
50+
'w-12 h-12 p-1 relative text-grey-600 text-sm rounded flex flex-col items-center justify-center hover:text-blue-600 active:text-blue-500 focus-visible-default outline-offset-2 hover:bg-gradient-to-tr hover:from-blue-600/10 hover:to-purple-500/10 active:bg-gradient-to-tr active:from-blue-600/10 active:to-purple-500/10 focus-visible:bg-gradient-to-tr focus-visible:from-blue-600/10 focus-visible:to-purple-500/10';
5151

5252
export const GetDataCopySingleton = ({
5353
value,
@@ -76,7 +76,7 @@ export const GetDataCopySingleton = ({
7676
data-glean-id={gleanDataAttrs.copy?.id}
7777
data-glean-type={gleanDataAttrs.copy?.type}
7878
>
79-
<CopyIcon aria-hidden className="w-10 h-10 fill-current" />
79+
<CopyIcon aria-hidden className="w-8 h-8 fill-current" />
8080
</button>
8181
</FtlMsg>
8282
);
@@ -169,7 +169,7 @@ export const GetDataTrio = ({
169169
}, [value, pageTitle]);
170170

171171
return (
172-
<div className="flex justify-center w-full">
172+
<div className="flex justify-between max-w-52 w-4/5">
173173
<FtlMsg
174174
id="get-data-trio-download-2"
175175
attrs={{ title: true, 'aria-label': true }}
@@ -195,7 +195,7 @@ export const GetDataTrio = ({
195195
data-glean-id={gleanDataAttrs.download?.id}
196196
data-glean-type={gleanDataAttrs.download?.type}
197197
>
198-
<DownloadIcon aria-hidden className="w-10 h-10 fill-current" />
198+
<DownloadIcon aria-hidden className="w-8 h-8 fill-current" />
199199
</a>
200200
</FtlMsg>
201201

@@ -225,7 +225,7 @@ export const GetDataTrio = ({
225225
data-glean-id={gleanDataAttrs.print?.id}
226226
data-glean-type={gleanDataAttrs.print?.type}
227227
>
228-
<PrintIcon aria-hidden className="w-10 h-10 fill-current" />
228+
<PrintIcon aria-hidden className="w-7 h-7 fill-current" />
229229
</button>
230230
</FtlMsg>
231231
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## The backup codes download step of the setup 2 factor authentication flow
2+
3+
flow-setup-2fa-backup-code-dl-heading = Save backup authentication codes
4+
5+
flow-setup-2fa-backup-code-dl-save-these-codes = Keep these in a place you’ll remember. If you don’t have access to your authenticator app you’ll need to enter one to sign in.
6+
7+
flow-setup-2fa-backup-code-dl-button-continue = Continue
8+
9+
##
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 { 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 { FlowSetup2faBackupCodeDownload } from '.';
11+
12+
export default {
13+
title: 'Components/Settings/FlowSetup2faBackupCodeDownload',
14+
component: FlowSetup2faBackupCodeDownload,
15+
decorators: [withLocalization],
16+
} as Meta;
17+
18+
const navigateBackward = async () => {
19+
action('navigateBackward')();
20+
};
21+
22+
const onContinue = () => {
23+
action('onContinue')();
24+
};
25+
26+
const dummyRecoveryCodes = [
27+
'code111111',
28+
'code222222',
29+
'code333333',
30+
'code444444',
31+
'code555555',
32+
'code666666',
33+
'code777777',
34+
'code888888',
35+
];
36+
37+
export const Default = () => (
38+
<SettingsLayout>
39+
<FlowSetup2faBackupCodeDownload
40+
currentStep={2}
41+
numberOfSteps={3}
42+
localizedFlowTitle="Two-step authentication"
43+
onBackButtonClick={navigateBackward}
44+
showProgressBar
45+
46+
recoveryCodes={dummyRecoveryCodes}
47+
onContinue={onContinue}
48+
/>
49+
</SettingsLayout>
50+
);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 { FlowSetup2faBackupCodeDownload } 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 recoveryCodes = ['3594s0tbsq', '0zrg82sdzm', 'wx88yxenfc'];
15+
16+
const renderFlowSetup2faBackupCodeDownload = () => {
17+
const onBackButtonClick = jest.fn();
18+
const onContinue = jest.fn();
19+
return {
20+
onBackButtonClick,
21+
onContinue,
22+
...renderWithLocalizationProvider(
23+
<FlowSetup2faBackupCodeDownload
24+
currentStep={1}
25+
numberOfSteps={3}
26+
localizedFlowTitle="Two-step authentication"
27+
28+
showProgressBar
29+
{...{ recoveryCodes, onBackButtonClick, onContinue }}
30+
/>
31+
),
32+
};
33+
};
34+
35+
describe('FlowSetup2faBackupCodeDownload', () => {
36+
beforeEach(() => {
37+
window.URL.createObjectURL = jest.fn(() => 'blob:mock-url');
38+
// set UA to a desktop browser as the default for the tests
39+
// using a hack to work around userAgent being read-only
40+
Object.defineProperty(window.navigator, 'userAgent', {
41+
value:
42+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0',
43+
configurable: true,
44+
});
45+
});
46+
it('renders correctly', () => {
47+
renderFlowSetup2faBackupCodeDownload();
48+
expect(screen.getByRole('progressbar')).toBeVisible();
49+
screen
50+
.getByTestId('datablock')
51+
.querySelectorAll('li')
52+
.forEach((li, i) => expect(li).toHaveTextContent(recoveryCodes[i]));
53+
expect(screen.getByRole('link', { name: 'Download' })).toHaveAttribute(
54+
'download',
55+
'[email protected] Backup authentication codes.txt'
56+
);
57+
});
58+
59+
it('sets up Glean metrics correctly', () => {
60+
const gleanSpy = jest.spyOn(
61+
GleanMetrics.accountPref,
62+
'twoStepAuthCodesView'
63+
);
64+
renderFlowSetup2faBackupCodeDownload();
65+
expect(gleanSpy).toBeCalled();
66+
67+
const downloadButton = screen.getByRole('link', { name: 'Download' });
68+
const copyButton = screen.getByRole('button', { name: 'Copy' });
69+
const printButton = screen.getByRole('button', { name: 'Print' });
70+
71+
expect(downloadButton).toHaveAttribute(
72+
'data-glean-id',
73+
'two_step_auth_codes_download'
74+
);
75+
expect(copyButton).toHaveAttribute(
76+
'data-glean-id',
77+
'two_step_auth_codes_copy'
78+
);
79+
expect(printButton).toHaveAttribute(
80+
'data-glean-id',
81+
'two_step_auth_codes_print'
82+
);
83+
for (const button of [downloadButton, copyButton, printButton]) {
84+
expect(button).toHaveAttribute(
85+
'data-glean-type',
86+
GleanClickEventType2FA.setup
87+
);
88+
}
89+
});
90+
91+
it('does not display the download button or the print button on mobile', () => {
92+
// Set the user agent to a mobile browser for this test case
93+
Object.defineProperty(window.navigator, 'userAgent', {
94+
value:
95+
'Mozilla/5.0 (Android 15; Mobile; rv:139.0) Gecko/139.0 Firefox/139.0',
96+
configurable: true,
97+
});
98+
renderFlowSetup2faBackupCodeDownload();
99+
expect(
100+
screen.queryByRole('link', { name: 'Download' })
101+
).not.toBeInTheDocument();
102+
expect(
103+
screen.queryByRole('button', { name: 'Print' })
104+
).not.toBeInTheDocument();
105+
});
106+
107+
it('calls onBackButtonClick when the back button is clicked', async () => {
108+
const { onBackButtonClick } = renderFlowSetup2faBackupCodeDownload();
109+
const cancelButton = screen.getByRole('button', { name: 'Back' });
110+
await userEvent.click(cancelButton);
111+
expect(onBackButtonClick).toHaveBeenCalled();
112+
});
113+
114+
it('calls onContinue when the Continue button is clicked', async () => {
115+
const { onContinue } = renderFlowSetup2faBackupCodeDownload();
116+
const continueButton = screen.getByRole('button', { name: 'Continue' });
117+
await userEvent.click(continueButton);
118+
expect(onContinue).toHaveBeenCalled();
119+
});
120+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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, { 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 DataBlock from '../../DataBlock';
11+
import GleanMetrics from '../../../lib/glean';
12+
import { UAParser } from 'ua-parser-js';
13+
14+
type FlowSetup2faBackupCodeDownloadProps = {
15+
currentStep?: number;
16+
numberOfSteps?: number;
17+
hideBackButton?: boolean;
18+
localizedFlowTitle: string;
19+
onBackButtonClick?: () => void;
20+
showProgressBar?: boolean;
21+
recoveryCodes: string[];
22+
email: string;
23+
onContinue: () => void;
24+
reason?: GleanClickEventType2FA;
25+
};
26+
27+
export const FlowSetup2faBackupCodeDownload = ({
28+
currentStep,
29+
numberOfSteps,
30+
hideBackButton = false,
31+
localizedFlowTitle,
32+
onBackButtonClick,
33+
showProgressBar = true,
34+
recoveryCodes,
35+
email,
36+
onContinue,
37+
reason = GleanClickEventType2FA.setup,
38+
}: FlowSetup2faBackupCodeDownloadProps) => {
39+
const [isMobile, setIsMobile] = React.useState(false);
40+
useEffect(() => {
41+
// undefined means desktop
42+
setIsMobile(new UAParser().getDevice().type !== undefined);
43+
}, []);
44+
45+
useEffect(() => {
46+
GleanMetrics.accountPref.twoStepAuthCodesView({
47+
event: { reason },
48+
});
49+
}, [reason]);
50+
51+
return (
52+
<FlowContainer
53+
title={localizedFlowTitle}
54+
{...{ hideBackButton, onBackButtonClick }}
55+
>
56+
{showProgressBar && currentStep != null && numberOfSteps != null && (
57+
<ProgressBar {...{ currentStep, numberOfSteps }} />
58+
)}
59+
<FtlMsg id="flow-setup-2fa-backup-code-dl-heading">
60+
<h2 className="font-bold text-xl my-2">
61+
Save backup authentication codes
62+
</h2>
63+
</FtlMsg>
64+
65+
<div className="my-2" data-testid="2fa-recovery-codes">
66+
<FtlMsg id="flow-setup-2fa-backup-code-dl-save-these-codes">
67+
Keep these in a place you’ll remember. If you don’t have access to
68+
your authenticator app you’ll need to enter one to sign in.
69+
</FtlMsg>
70+
<div className="mt-6 flex flex-col items-center justify-between">
71+
<DataBlock
72+
value={recoveryCodes}
73+
contentType="Backup authentication codes"
74+
email={email}
75+
isMobile={isMobile}
76+
gleanDataAttrs={{
77+
download: {
78+
id: 'two_step_auth_codes_download',
79+
type: reason,
80+
},
81+
copy: {
82+
id: 'two_step_auth_codes_copy',
83+
type: reason,
84+
},
85+
print: {
86+
id: 'two_step_auth_codes_print',
87+
type: reason,
88+
},
89+
}}
90+
/>
91+
</div>
92+
</div>
93+
<FtlMsg id="flow-setup-2fa-backup-code-dl-button-continue">
94+
<button
95+
type="submit"
96+
className="cta-primary cta-xl mt-3"
97+
onClick={onContinue}
98+
data-glean-id="two_step_auth_codes_submit"
99+
>
100+
Continue
101+
</button>
102+
</FtlMsg>
103+
</FlowContainer>
104+
);
105+
};

0 commit comments

Comments
 (0)