Skip to content

Commit a541202

Browse files
authored
Merge pull request #18853 from mozilla/FXA-11511
feat(settings): Split and update design ConfirmTotpResetPassword
2 parents 766b486 + b8f47eb commit a541202

26 files changed

Lines changed: 670 additions & 185 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import ConfirmResetPasswordContainer from '../../pages/ResetPassword/ConfirmRese
5656
import CompleteResetPasswordContainer from '../../pages/ResetPassword/CompleteResetPassword/container';
5757
import AccountRecoveryConfirmKeyContainer from '../../pages/ResetPassword/AccountRecoveryConfirmKey/container';
5858
import ConfirmTotpResetPasswordContainer from '../../pages/ResetPassword/ConfirmTotpResetPassword/container';
59+
import ConfirmBackupCodeResetPasswordContainer from '../../pages/ResetPassword/ConfirmBackupCodeResetPassword/container';
5960
import ResetPasswordConfirmedContainer from '../../pages/ResetPassword/ResetPasswordConfirmed/container';
6061
import ResetPasswordWithRecoveryKeyVerifiedContainer from '../../pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/container';
6162
import CompleteSigninContainer from '../../pages/Signin/CompleteSignin/container';
@@ -373,6 +374,7 @@ const AuthAndAccountSetupRoutes = ({
373374
/>
374375
<ConfirmResetPasswordContainer path="/confirm_reset_password/*" />
375376
<ConfirmTotpResetPasswordContainer path="/confirm_totp_reset_password/*" />
377+
<ConfirmBackupCodeResetPasswordContainer path="/confirm_backup_code_reset_password/*" />
376378
<CompleteResetPasswordContainer
377379
path="/complete_reset_password/*"
378380
{...{ integration }}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
import React from 'react';
66
import { AppError } from '.';
7-
import AppLayout from '../AppLayout';
87
import {
9-
GenericData,
10-
ModelValidationErrors,
8+
119
UrlQueryData,
1210
} from '../../lib/model-data';
1311
import { withLocalization } from 'fxa-react/lib/storybooks';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import React from 'react';
6-
import { screen, within } from '@testing-library/react';
6+
import { screen } from '@testing-library/react';
77
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
88
import PasswordStrengthInline from '.';
99
// import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';

packages/fxa-settings/src/components/Settings/FlowRecoveryKeyDownload/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import React from 'react';
6-
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
6+
import { fireEvent, screen, waitFor } from '@testing-library/react';
77
import { logViewEvent } from '../../../lib/metrics';
88
import FlowRecoveryKeyDownload from './';
99
import { renderWithRouter } from '../../../models/mocks';

packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/index.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import React from 'react';
65
import { screen, fireEvent, waitFor } from '@testing-library/react';
76
import { renderWithRouter } from '../../../models/mocks';
87
import { createSubject } from './mocks';

packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localiz
66
import { Subject } from './mocks';
77
import { screen, waitFor } from '@testing-library/react';
88
import userEvent from '@testing-library/user-event';
9-
import { MOCK_EMAIL, MOCK_PASSWORD } from '../../mocks';
9+
import { MOCK_PASSWORD } from '../../mocks';
1010
import GleanMetrics from '../../../lib/glean';
1111

1212
const mockSubmitNewPassword = jest.fn((newPassword: string) =>
@@ -73,15 +73,17 @@ describe('CompleteResetPassword page', () => {
7373
expect(screen.queryByText(serviceRelayText)).not.toBeInTheDocument();
7474
});
7575

76-
it('renders expected text when service=relay', () => {
76+
it('renders expected text when service=relay', async () => {
7777
renderWithLocalizationProvider(
7878
<Subject
7979
isDesktopServiceRelay={true}
8080
estimatedSyncDeviceCount={0}
8181
recoveryKeyExists={false}
8282
/>
8383
);
84-
screen.getByText(serviceRelayText);
84+
await waitFor(() =>
85+
expect(screen.getByText(serviceRelayText)).toBeVisible()
86+
);
8587
});
8688

8789
it('renders as expected for account without sync', async () => {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 { LocationProvider } from '@reach/router';
7+
import { act } from '@testing-library/react';
8+
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
9+
import { MOCK_EMAIL, MOCK_PASSWORD_CHANGE_TOKEN, MOCK_UID } from '../../mocks';
10+
import ConfirmBackupCodeResetPasswordContainer from './container';
11+
12+
const mockConsume = jest.fn();
13+
jest.mock('../../../models', () => ({
14+
__esModule: true,
15+
useAuthClient: () => ({
16+
consumeRecoveryCodeWithPasswordForgotToken: mockConsume,
17+
}),
18+
useFtlMsgResolver: () => ({
19+
getMsg: (_id: string, fallback: string) => fallback,
20+
}),
21+
}));
22+
23+
const mockNavigate = jest.fn();
24+
jest.mock('../../../lib/hooks/useNavigateWithQuery', () => ({
25+
useNavigateWithQuery: () => mockNavigate,
26+
}));
27+
28+
let mockLocationState = {
29+
code: 'ignored',
30+
email: MOCK_EMAIL,
31+
token: MOCK_PASSWORD_CHANGE_TOKEN,
32+
emailToHashWith: MOCK_EMAIL,
33+
recoveryKeyExists: false,
34+
estimatedSyncDeviceCount: 2,
35+
uid: MOCK_UID,
36+
};
37+
jest.mock('@reach/router', () => {
38+
const actual = jest.requireActual('@reach/router');
39+
return {
40+
...actual,
41+
useLocation: () => {
42+
return {
43+
pathname: '/confirm_backup_code_reset_password',
44+
state: mockLocationState,
45+
};
46+
},
47+
};
48+
});
49+
50+
let capturedProps: any;
51+
jest.mock('.', () => (props: any) => {
52+
capturedProps = props;
53+
return null;
54+
});
55+
56+
async function render() {
57+
renderWithLocalizationProvider(
58+
<LocationProvider>
59+
<ConfirmBackupCodeResetPasswordContainer />
60+
</LocationProvider>
61+
);
62+
}
63+
64+
describe('ConfirmBackupCodeResetPasswordContainer', () => {
65+
beforeEach(() => {
66+
capturedProps = undefined;
67+
mockConsume.mockReset();
68+
mockNavigate.mockReset();
69+
});
70+
71+
it('calls authClient then navigates on success', async () => {
72+
mockConsume.mockResolvedValueOnce(undefined);
73+
74+
render();
75+
76+
await act(async () => {
77+
await capturedProps.verifyBackupCode('BACKUPCODE');
78+
});
79+
80+
expect(mockConsume).toHaveBeenCalledWith(
81+
MOCK_PASSWORD_CHANGE_TOKEN,
82+
'BACKUPCODE'
83+
);
84+
85+
expect(mockNavigate).toHaveBeenCalledWith('/complete_reset_password', {
86+
state: expect.objectContaining({
87+
token: MOCK_PASSWORD_CHANGE_TOKEN,
88+
email: MOCK_EMAIL,
89+
uid: MOCK_UID,
90+
}),
91+
replace: true,
92+
});
93+
});
94+
95+
it('sets localized error message when authClient throws', async () => {
96+
mockConsume.mockRejectedValueOnce('Some error');
97+
98+
render();
99+
100+
await act(async () => {
101+
await capturedProps.verifyBackupCode('INVALDCODE');
102+
});
103+
104+
expect(capturedProps.codeErrorMessage).toBe('Unexpected error');
105+
});
106+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { RouteComponentProps, useLocation } from '@reach/router';
7+
import { useAuthClient, useFtlMsgResolver } from '../../../models';
8+
import ConfirmBackupCodeResetPassword from '.';
9+
import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery';
10+
import { CompleteResetPasswordLocationState } from '../CompleteResetPassword/interfaces';
11+
import { getLocalizedErrorMessage } from '../../../lib/error-utils';
12+
13+
const ConfirmBackupCodeResetPasswordContainer = (_: RouteComponentProps) => {
14+
const authClient = useAuthClient();
15+
const location = useLocation();
16+
17+
const {
18+
code,
19+
email,
20+
token,
21+
emailToHashWith,
22+
recoveryKeyExists,
23+
estimatedSyncDeviceCount,
24+
uid,
25+
} = location.state as CompleteResetPasswordLocationState;
26+
27+
const ftlMsgResolver = useFtlMsgResolver();
28+
const navigateWithQuery = useNavigateWithQuery();
29+
30+
const [codeErrorMessage, setCodeErrorMessage] = useState<string>('');
31+
32+
const onSuccess = () => {
33+
navigateWithQuery('/complete_reset_password', {
34+
state: {
35+
code,
36+
email,
37+
emailToHashWith,
38+
estimatedSyncDeviceCount,
39+
recoveryKeyExists,
40+
token,
41+
uid,
42+
},
43+
replace: true,
44+
});
45+
};
46+
47+
const verifyBackupCode = async (backupCode: string) => {
48+
setCodeErrorMessage('');
49+
try {
50+
await authClient.consumeRecoveryCodeWithPasswordForgotToken(
51+
token,
52+
backupCode
53+
);
54+
onSuccess();
55+
} catch (error) {
56+
setCodeErrorMessage(getLocalizedErrorMessage(ftlMsgResolver, error));
57+
}
58+
};
59+
60+
return (
61+
<ConfirmBackupCodeResetPassword
62+
{...{ verifyBackupCode, codeErrorMessage, setCodeErrorMessage }}
63+
/>
64+
);
65+
};
66+
67+
export default ConfirmBackupCodeResetPasswordContainer;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# ConfirmBackupCodeResetPassword page
2+
3+
confirm-backup-code-reset-password-input-label = Enter 10-character code
4+
confirm-backup-code-reset-password-confirm-button = Confirm
5+
confirm-backup-code-reset-password-subheader = Enter backup authentication code
6+
confirm-backup-code-reset-password-instruction = Enter one of the one-time-use codes you saved when you set up two-step authentication.
7+
# Link out to support article: https://support.mozilla.org/kb/what-if-im-locked-out-two-step-authentication
8+
confirm-backup-code-reset-password-locked-out-link = Are you locked out?
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 ConfirmBackupCodeResetPassword from '.';
7+
import { Meta } from '@storybook/react';
8+
import { withLocalization } from 'fxa-react/lib/storybooks';
9+
import { Subject } from './mocks';
10+
11+
export default {
12+
title: 'Pages/ResetPassword/ConfirmBackupCodeResetPassword',
13+
component: ConfirmBackupCodeResetPassword,
14+
decorators: [withLocalization],
15+
} as Meta;
16+
17+
export const Default = () => <Subject />;
18+
19+
export const WithErrorBanner = () => (
20+
<Subject errorMessage="Unexpected error" />
21+
);

0 commit comments

Comments
 (0)