feat(fxa-settings): Modify styling of DataBlock component

Because:

* We are updating the styling of the DataBlock component as part of the redesign of the Recovery Key updates

This commit:

* Change the styling of the data block to use a gradient background
* Add an inline copy button option to the DataBlock
* Update stories for RecoveryKeyAdd and TwoStepAuthenticationReplaceCodes to display generated codes
* Update tests
* Correct l10n id mismatch

Closes #FXA-7222
This commit is contained in:
Valerie Pomerleau 2023-04-13 10:45:29 -07:00
Родитель f782016516
Коммит 56702f86c4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 33A451F0BB2180B4
13 изменённых файлов: 189 добавлений и 100 удалений

Просмотреть файл

@ -50,7 +50,7 @@
/* Max-height is a likely temp "hack" for .spinner until it's converted to TW */
/* font-color is also a hack until all buttons are TWified */
.cta-xl {
@apply flex-1 font-bold text-base p-4 border-0 max-h-14 rounded-md;
@apply flex-1 font-bold text-base p-4 max-h-14 rounded-md;
}
.cta-caution {

Просмотреть файл

@ -13,6 +13,10 @@ export default {
decorators: [withLocalization],
} as Meta;
export const SingleCodeInlineCopy = () => (
<DataBlock value="ANMD 1S09 7Y2Y 4EES 02CW BJ6Z PYKP H69F" isInline />
);
export const SingleCode = () => (
<DataBlock value="ANMD 1S09 7Y2Y 4EES 02CW BJ6Z PYKP H69F" />
);

Просмотреть файл

@ -7,6 +7,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { Account, AppContext } from '../../models';
import DataBlock from './index';
import { act } from 'react-dom/test-utils';
import { MOCK_ACCOUNT, mockAppContext } from '../../models/mocks';
const singleValue = 'ANMD 1S09 7Y2Y 4EES 02CW BJ6Z PYKP H69F';
@ -22,9 +23,7 @@ const multiValue = [
];
const account = {
primaryEmail: {
email: 'pbooth@mozilla.com',
},
...MOCK_ACCOUNT,
} as unknown as Account;
Object.defineProperty(window.navigator, 'clipboard', {
@ -34,7 +33,7 @@ window.URL.createObjectURL = jest.fn();
it('can render single values', () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={singleValue} />
</AppContext.Provider>
);
@ -43,7 +42,7 @@ it('can render single values', () => {
it('can render multiple values', () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={multiValue} />
</AppContext.Provider>
);
@ -54,7 +53,7 @@ it('can render multiple values', () => {
it('can apply spacing to multiple values', () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={multiValue} separator=" " />
</AppContext.Provider>
);
@ -66,7 +65,7 @@ it('can apply spacing to multiple values', () => {
it('displays only Copy icon in iOS', () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={multiValue} separator=" " isIOS />
</AppContext.Provider>
);
@ -82,7 +81,7 @@ it('displays only Copy icon in iOS', () => {
it('displays a tooltip on action', async () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={multiValue} />
</AppContext.Provider>
);
@ -96,7 +95,7 @@ it('displays a tooltip on action', async () => {
it('sets download file name', async () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock
value={multiValue}
contentType="Firefox account recovery key"
@ -112,7 +111,7 @@ it('sets download file name', async () => {
it('sets has fallback download file name', async () => {
render(
<AppContext.Provider value={{ account }}>
<AppContext.Provider value={mockAppContext({ account })}>
<DataBlock value={multiValue} />
</AppContext.Provider>
);

Просмотреть файл

@ -2,13 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Localized } from '@fluent/react';
import React, { useState } from 'react';
import GetDataTrio, {
DownloadContentType,
GetDataCopySingleton,
GetDataCopySingletonInline,
} from '../GetDataTrio';
import { Tooltip } from '../Tooltip';
import { FtlMsg } from 'fxa-react/lib/utils';
const actionTypeToNotification = {
download: 'Downloaded',
copy: 'Copied',
@ -26,6 +27,7 @@ export type DataBlockProps = {
onCopy?: (event: React.ClipboardEvent<HTMLDivElement>) => void;
onAction?: actionFn;
isIOS?: boolean;
isInline?: boolean;
};
export const DataBlock = ({
@ -36,6 +38,7 @@ export const DataBlock = ({
onCopy,
onAction = () => {},
isIOS = false,
isInline = false,
}: DataBlockProps) => {
const valueIsArray = Array.isArray(value);
const [performedAction, setPerformedAction] = useState<actions>();
@ -53,8 +56,9 @@ export const DataBlock = ({
return (
<div className="flex flex-col items-center">
<div
className={`flex rounded-xl px-7 font-mono text-center text-sm text-green-900 bg-green-800/10 flex-wrap relative mb-6 ${
className={`flex rounded-xl px-7 font-mono text-center text-sm font-bold text-black bg-gradient-to-tr from-blue-600/10 to-purple-500/10 flex-wrap relative mb-6 ${
valueIsArray ? 'max-w-sm py-4' : 'max-w-lg py-5'
} ${isInline ? 'gap-6 items-center' : ''}
}`}
data-testid={dataTestId}
{...{ onCopy }}
@ -70,22 +74,23 @@ export const DataBlock = ({
<span>{value}</span>
)}
{performedAction && (
<Localized
id={`datablock-${performedAction}`}
attrs={{ message: true }}
>
<FtlMsg id={`datablock-${performedAction}`} attrs={{ message: true }}>
<Tooltip
prefixDataTestId={`datablock-${performedAction}`}
message={actionTypeToNotification[performedAction]}
position="bottom"
className="mt-1"
></Tooltip>
</Localized>
</FtlMsg>
)}
{isInline && (
<GetDataCopySingletonInline {...{ value, onAction: actionCb }} />
)}
</div>
{isIOS ? (
{isIOS && !isInline && (
<GetDataCopySingleton {...{ value, onAction: actionCb }} />
) : (
)}
{!isIOS && !isInline && (
<GetDataTrio {...{ value, contentType, onAction: actionCb }} />
)}
</div>

Просмотреть файл

@ -0,0 +1,3 @@
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.54736 12.4453H2.71191C1.7891 12.4453 1.04102 11.6972 1.04102 10.7744V3.25537C1.04102 2.33256 1.7891 1.58447 2.71191 1.58447H10.231C11.1538 1.58447 11.9019 2.33256 11.9019 3.25537V4.09082M8.56006 18.2935H16.0791C17.0019 18.2935 17.75 17.5454 17.75 16.6226V9.10352C17.75 8.1807 17.0019 7.43262 16.0791 7.43262H8.56006C7.63725 7.43262 6.88916 8.1807 6.88916 9.10352V16.6226C6.88916 17.5454 7.63725 18.2935 8.56006 18.2935Z" stroke="current" stroke-width="1.6709" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 624 B

Просмотреть файл

@ -4,7 +4,10 @@
import React from 'react';
import { Meta } from '@storybook/react';
import GetDataTrio, { GetDataCopySingleton } from './index';
import GetDataTrio, {
GetDataCopySingleton,
GetDataCopySingletonInline,
} from './index';
import { withLocalization } from '../../../.storybook/decorators';
export default {
@ -24,3 +27,9 @@ export const SingleCopyButton = () => (
<GetDataCopySingleton value="Copy that" />
</div>
);
export const SingleCopyButtonInline = () => (
<div className="p-10 max-w-xs">
<GetDataCopySingletonInline value="Copy that" />
</div>
);

Просмотреть файл

@ -6,16 +6,13 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import { Account, AppContext } from '../../models';
import GetDataTrio from './index';
import { MOCK_ACCOUNT } from '../../models/mocks';
const contentType = 'Firefox account recovery key';
const value = 'Sun Tea';
const url = 'https://mozilla.org';
const account = {
primaryEmail: {
email: 'pbooth@mozilla.com',
},
} as unknown as Account;
const account = { ...MOCK_ACCOUNT } as unknown as Account;
it('renders Trio as expected', () => {
window.URL.createObjectURL = jest.fn();

Просмотреть файл

@ -3,12 +3,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React, { useCallback } from 'react';
import { Localized, useLocalization } from '@fluent/react';
import { copy } from '../../lib/clipboard';
import { ReactComponent as CopyIcon } from './copy.svg';
import { ReactComponent as InlineCopyIcon } from './copy-inline.svg';
import { ReactComponent as DownloadIcon } from './download.svg';
import { ReactComponent as PrintIcon } from './print.svg';
import { useAccount } from '../../models';
import { useAccount, useFtlMsgResolver } from '../../models';
import { FtlMsg } from 'fxa-react/lib/utils';
export type DownloadContentType =
| 'Firefox account recovery key'
@ -18,9 +19,8 @@ export type DownloadContentType =
const DownloadContentTypeL10nMapping: Record<DownloadContentType, string> = {
Firefox: 'get-data-trio-title-firefox',
'Firefox backup authentication codes':
'get-data-trio-title-firefox-backup-authentication-codes',
'Firefox account recovery key':
'get-data-trio-title-firefox-account-recovery-key',
'get-data-trio-title-firefox-backup-verification-codes',
'Firefox account recovery key': 'get-data-trio-title-firefox-recovery-key',
};
export type GetDataTrioProps = {
@ -31,7 +31,7 @@ export type GetDataTrioProps = {
export const GetDataCopySingleton = ({ value, onAction }: GetDataTrioProps) => {
return (
<Localized id="get-data-trio-copy-2" attrs={{ title: true, ariaLabel: true }}>
<FtlMsg id="get-data-trio-copy-2" attrs={{ title: true, ariaLabel: true }}>
<button
title="Copy"
type="button"
@ -50,7 +50,33 @@ export const GetDataCopySingleton = ({ value, onAction }: GetDataTrioProps) => {
className="absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2 fill-current"
/>
</button>
</Localized>
</FtlMsg>
);
};
export const GetDataCopySingletonInline = ({
value,
onAction,
}: GetDataTrioProps) => {
return (
<FtlMsg id="get-data-trio-copy-2" attrs={{ title: true, ariaLabel: true }}>
<button
title="Copy"
type="button"
onClick={async () => {
const copyValue = Array.isArray(value) ? value.join('\r\n') : value;
await copy(copyValue);
onAction?.('copy');
}}
data-testid="databutton-copy"
className="-m-3 p-3 rounded text-grey-500 bg-transparent border border-transparent hover:bg-grey-100 active:bg-grey-200 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-blue-500 focus:bg-grey-50"
>
<InlineCopyIcon
aria-label="Copy"
className="w-6 h-6 items-center justify-center stroke-current"
/>
</button>
</FtlMsg>
);
};
@ -74,18 +100,16 @@ export const GetDataTrio = ({
contentType,
onAction,
}: GetDataTrioProps) => {
const { l10n } = useLocalization();
const ftlMsgResolver = useFtlMsgResolver();
// Fall back to 'Firefox' just in case.
if (contentType == null) {
contentType = 'Firefox';
}
const pageTitle = l10n.getString(
DownloadContentTypeL10nMapping[contentType],
null,
contentType
);
const pageTitleId = DownloadContentTypeL10nMapping[contentType];
const pageTitle = ftlMsgResolver.getMsg(pageTitleId, contentType);
const print = useCallback(() => {
const printWindow = window.open('', 'Print', 'height=600,width=800')!;
@ -99,7 +123,7 @@ export const GetDataTrio = ({
return (
<div className="flex justify-between w-4/5 max-w-48">
<Localized
<FtlMsg
id="get-data-trio-download-2"
attrs={{ title: true, ariaLabel: true }}
>
@ -122,14 +146,14 @@ export const GetDataTrio = ({
className="absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2 fill-current"
/>
</a>
</Localized>
</FtlMsg>
<GetDataCopySingleton {...{ onAction, value }} />
{/** This only opens the page that is responsible
* for triggering the print screen.
**/}
<Localized
<FtlMsg
id="get-data-trio-print-2"
attrs={{ title: true, ariaLabel: true }}
>
@ -150,7 +174,7 @@ export const GetDataTrio = ({
className="absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2 fill-current"
/>
</button>
</Localized>
</FtlMsg>
</div>
);
};

Просмотреть файл

@ -1,6 +1,7 @@
## Two Step Authentication - replace backup authentication code
tfa-replace-code-error-3 = There was a problem replacing your backup authentication codes
tfa-create-code-error = There was a problem creating your backup authentication codes
tfa-replace-code-success-1 = New codes have been created. Save these one-time use
backup authentication codes in a safe place — youll need them to access your account if you dont
have your mobile device.

Просмотреть файл

@ -3,7 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { AppContext } from 'fxa-settings/src/models';
import { mockAppContext, MOCK_ACCOUNT } from 'fxa-settings/src/models/mocks';
import {
mockAppContext,
MOCK_ACCOUNT,
mockSession,
} from 'fxa-settings/src/models/mocks';
import React from 'react';
import { Page2faReplaceRecoveryCodes } from '.';
import AppLayout from '../AppLayout';
@ -11,21 +15,21 @@ import { Meta } from '@storybook/react';
import { LocationProvider } from '@reach/router';
import { withLocalization } from '../../../../.storybook/decorators';
const session = mockSession(true);
const account = {
...MOCK_ACCOUNT,
replaceRecoveryCodes: () =>
Promise.resolve({
recoveryCodes: [
'C1OFZW7R04',
'XVKRLKERT4',
'CF0V94X204',
'C3THX2SGZ4',
'UXC6NRQT54',
'24RF9WFA44',
'ZBULPFN7J4',
'D4J6KY8FL4',
],
}),
updateRecoveryCodes: () => Promise.resolve(true),
generateRecoveryCodes: () =>
Promise.resolve([
'C1OFZW7R04',
'XVKRLKERT4',
'CF0V94X204',
'C3THX2SGZ4',
'UXC6NRQT54',
'24RF9WFA44',
'ZBULPFN7J4',
'D4J6KY8FL4',
]),
} as any;
export default {
@ -36,7 +40,7 @@ export default {
export const Default = () => (
<LocationProvider>
<AppContext.Provider value={mockAppContext({ account })}>
<AppContext.Provider value={mockAppContext({ account, session })}>
<AppLayout>
<Page2faReplaceRecoveryCodes />
</AppLayout>

Просмотреть файл

@ -9,21 +9,24 @@ import { Account, AppContext } from '../../../models';
import { Config } from '../../../lib/config';
import { HomePath } from '../../../constants';
import { typeByTestIdFn } from '../../../lib/test-utils';
import { mockAppContext, renderWithRouter } from '../../../models/mocks';
import {
MOCK_ACCOUNT,
mockAppContext,
renderWithRouter,
} from '../../../models/mocks';
import { Page2faReplaceRecoveryCodes } from '.';
jest.mock('../../../models/AlertBarInfo');
const recoveryCodes = ['0123456789'];
const account = {
primaryEmail: {
email: 'pbooth@mozilla.com',
},
generateRecoveryCodes: jest.fn().mockResolvedValue(recoveryCodes),
...MOCK_ACCOUNT,
generateRecoveryCodes: jest.fn().mockReturnValue(recoveryCodes),
updateRecoveryCodes: jest.fn().mockResolvedValue({ success: true }),
} as unknown as Account;
const config = {
l10n: { strict: false },
recoveryCodes: {
count: 1,
length: 10,
@ -50,6 +53,7 @@ async function renderPage2faReplaceRecoveryCodes() {
it('renders', async () => {
await renderPage2faReplaceRecoveryCodes();
const context = mockAppContext({ account, config });
expect(screen.getByTestId('2fa-recovery-codes')).toBeInTheDocument();
@ -63,13 +67,15 @@ it('renders', async () => {
'download',
expect.stringContaining('Firefox backup authentication codes')
);
expect(context.alertBarInfo?.error).not.toBeCalled();
});
it('displays an error when fails to fetch new backup authentication codes', async () => {
const account = {
replaceRecoveryCodes: jest.fn().mockRejectedValue(new Error('wat')),
...MOCK_ACCOUNT,
generateRecoveryCodes: jest.fn().mockRejectedValue(new Error('wat')),
} as unknown as Account;
const context = mockAppContext({ account });
const context = mockAppContext({ account, config });
await act(async () => {
renderWithRouter(
<AppContext.Provider value={context}>
@ -78,6 +84,35 @@ it('displays an error when fails to fetch new backup authentication codes', asyn
);
});
expect(context.alertBarInfo?.error).toBeCalledTimes(1);
expect(context.alertBarInfo?.error).toHaveBeenCalledWith(
'There was a problem creating your backup authentication codes'
);
});
it('displays an error when fails to update backup authentication codes', async () => {
const account = {
...MOCK_ACCOUNT,
generateRecoveryCodes: jest.fn().mockReturnValue(recoveryCodes),
updateRecoveryCodes: jest.fn().mockRejectedValue(new Error('wat')),
} as unknown as Account;
const context = mockAppContext({ account, config });
await act(async () => {
renderWithRouter(
<AppContext.Provider value={context}>
<Page2faReplaceRecoveryCodes />
</AppContext.Provider>
);
});
fireEvent.click(screen.getByTestId('ack-recovery-code'));
await typeByTestIdFn('recovery-code-input-field')(recoveryCodes[0]);
fireEvent.click(screen.getByTestId('submit-recovery-code'));
await waitFor(() => {
expect(context.alertBarInfo?.error).toBeCalledTimes(1);
expect(context.alertBarInfo?.error).toHaveBeenCalledWith(
'There was a problem replacing your backup authentication codes'
);
});
});
it('forces users to validate backup authentication code', async () => {

Просмотреть файл

@ -14,11 +14,12 @@ import {
useAccount,
useAlertBar,
useConfig,
useFtlMsgResolver,
useSession,
} from '../../../models';
import { useLocalization, Localized } from '@fluent/react';
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
import { copyRecoveryCodes } from '../../../lib/totp';
import { FtlMsg } from 'fxa-react/lib/utils';
export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
const alertBar = useAlertBar();
@ -26,13 +27,13 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
const session = useSession();
const account = useAccount();
const config = useConfig();
const { l10n } = useLocalization();
const ftlMsgResolver = useFtlMsgResolver();
const goHome = () =>
navigate(HomePath + '#two-step-authentication', { replace: true });
const [subtitle, setSubtitle] = useState<string>(
l10n.getString('tfa-replace-code-1-2', null, 'Step 1 of 2')
ftlMsgResolver.getMsg('tfa-replace-code-1-2', 'Step 1 of 2')
);
const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]);
const [recoveryCodesAcknowledged, setRecoveryCodesAcknowledged] =
@ -40,9 +41,8 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
const alertSuccessAndGoHome = () => {
alertBar.success(
l10n.getString(
ftlMsgResolver.getMsg(
'tfa-replace-code-success-alert-3',
null,
'Account backup authentication codes updated'
)
);
@ -59,9 +59,8 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
alertSuccessAndGoHome();
} catch (e) {
alertBar.error(
l10n.getString(
ftlMsgResolver.getMsg(
'tfa-replace-code-error-3',
null,
'There was a problem replacing your backup authentication codes'
)
);
@ -85,27 +84,26 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
setRecoveryCodes(recoveryCodes);
} catch (e) {
alertBar.error(
l10n.getString(
'tfa-replace-code-error-3',
null,
ftlMsgResolver.getMsg(
'tfa-create-code-error',
'There was a problem creating your backup authentication codes'
)
);
}
}, [config, account, alertBar, l10n]);
}, [config, account, alertBar, ftlMsgResolver]);
const activateStep = (step: number) => {
switch (step) {
case 1:
setSubtitle(
l10n.getString('tfa-replace-code-1-2', null, 'Step 1 of 2')
ftlMsgResolver.getMsg('tfa-replace-code-1-2', 'Step 1 of 2')
);
setRecoveryCodesAcknowledged(false);
break;
case 2:
setSubtitle(
l10n.getString('tfa-replace-code-2-2', null, 'Step 2 of 2')
ftlMsgResolver.getMsg('tfa-replace-code-2-2', 'Step 2 of 2')
);
setRecoveryCodesAcknowledged(true);
break;
@ -128,7 +126,7 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
return (
<FlowContainer
title={l10n.getString('tfa-title', null, 'Two-step authentication')}
title={ftlMsgResolver.getMsg('tfa-title', 'Two-step authentication')}
{...{ subtitle, onBackButtonClick: moveBack }}
>
<VerifiedSessionGuard onDismiss={goHome} onError={goHome} />
@ -136,11 +134,11 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
{!recoveryCodesAcknowledged && (
<>
<div className="my-2" data-testid="2fa-recovery-codes">
<Localized id="tfa-replace-code-success">
<FtlMsg id="tfa-replace-code-success-1">
New codes have been created. Save these one-time use backup
authentication codes in a safe place youll need them to access
your account if you dont have your mobile device.
</Localized>
</FtlMsg>
<div className="mt-6 flex flex-col items-center h-auto justify-between">
{recoveryCodes.length > 0 ? (
<DataBlock
@ -155,7 +153,7 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
</div>
</div>
<div className="flex justify-center mt-6 mb-4 mx-auto max-w-64">
<Localized id="tfa-button-cancel">
<FtlMsg id="tfa-button-cancel">
<button
type="button"
className="cta-neutral cta-base-p mx-2 flex-1"
@ -163,8 +161,8 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
>
Cancel
</button>
</Localized>
<Localized id="recovery-key-continue-button">
</FtlMsg>
<FtlMsg id="recovery-key-continue-button">
<button
type="submit"
className="cta-neutral mx-2 px-10 py-2"
@ -175,7 +173,7 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
>
Continue
</button>
</Localized>
</FtlMsg>
</div>
</>
)}
@ -206,7 +204,7 @@ const RecoveryCodeCheck = ({
goHome,
onRecoveryCodeSubmit,
}: RecoverCodeCheckType) => {
const { l10n } = useLocalization();
const ftlMsgResolver = useFtlMsgResolver();
const [recoveryCodeError, setRecoveryCodeError] = useState<string>('');
@ -217,9 +215,8 @@ const RecoveryCodeCheck = ({
const onSubmit = async ({ recoveryCode }: RecoveryCodeForm) => {
if (!recoveryCodes.includes(recoveryCode)) {
setRecoveryCodeError(
l10n.getString(
ftlMsgResolver.getMsg(
'tfa-incorrect-recovery-code-1',
null,
'Incorrect backup authentication code'
)
);
@ -234,15 +231,15 @@ const RecoveryCodeCheck = ({
return (
<form onSubmit={recoveryCodeForm.handleSubmit(onSubmit)}>
<Localized id="tfa-enter-code-to-confirm-1">
<FtlMsg id="tfa-enter-code-to-confirm-1">
<p className="mt-4 mb-4">
Please enter one of your backup authentication codes now to confirm
you've saved it. Youll need a code to login if you dont have access
to your mobile device.
</p>
</Localized>
</FtlMsg>
<div className="mt-4 mb-6" data-testid="recovery-code-input">
<Localized id="tfa-enter-recovery-code-1" attrs={{ label: true }}>
<FtlMsg id="tfa-enter-recovery-code-1" attrs={{ label: true }}>
<InputText
name="recoveryCode"
label="Enter a backup authentication code"
@ -257,11 +254,11 @@ const RecoveryCodeCheck = ({
})}
{...{ errorText: recoveryCodeError }}
/>
</Localized>
</FtlMsg>
</div>
<div className="flex justify-center mb-4 mx-auto max-w-64">
{cancellable && (
<Localized id="tfa-button-cancel">
<FtlMsg id="tfa-button-cancel">
<button
type="button"
className="cta-neutral cta-base-p mx-2 flex-1"
@ -269,9 +266,9 @@ const RecoveryCodeCheck = ({
>
Cancel
</button>
</Localized>
</FtlMsg>
)}
<Localized id="tfa-button-finish">
<FtlMsg id="tfa-button-finish">
<button
type="submit"
data-testid="submit-recovery-code"
@ -283,7 +280,7 @@ const RecoveryCodeCheck = ({
>
Finish
</button>
</Localized>
</FtlMsg>
</div>
</form>
);

Просмотреть файл

@ -8,6 +8,8 @@ import { LocationProvider } from '@reach/router';
import AppLayout from '../AppLayout';
import { Meta } from '@storybook/react';
import { withLocalization } from '../../../../.storybook/decorators';
import { Account, AppContext } from '../../../models';
import { MOCK_ACCOUNT, mockAppContext } from '../../../models/mocks';
export default {
title: 'Pages/Settings/RecoveryKeyAdd',
@ -15,10 +17,19 @@ export default {
decorators: [withLocalization],
} as Meta;
export const Default = () => (
const randomKey = crypto.getRandomValues(new Uint8Array(20));
const account = {
...MOCK_ACCOUNT,
createRecoveryKey: () => Promise.resolve(randomKey),
} as unknown as Account;
export const DefaultWithAnyPassword = () => (
<LocationProvider>
<AppLayout>
<PageRecoveryKeyAdd />
</AppLayout>
<AppContext.Provider value={mockAppContext({ account })}>
<AppLayout>
<PageRecoveryKeyAdd />
</AppLayout>
</AppContext.Provider>
</LocationProvider>
);