diff --git a/packages/functional-tests/pages/resetPassword.ts b/packages/functional-tests/pages/resetPassword.ts index 265d5a2f979..f8b3df0fa08 100644 --- a/packages/functional-tests/pages/resetPassword.ts +++ b/packages/functional-tests/pages/resetPassword.ts @@ -36,10 +36,14 @@ export class ResetPasswordPage extends BaseLayout { }); } - get statusBar() { + get successBanner() { return this.page.getByRole('status'); } + get errorBanner() { + return this.page.getByRole('alert'); + } + get createNewPasswordHeading() { return this.page.getByRole('heading', { name: 'Create new password' }); } diff --git a/packages/functional-tests/tests/oauth/totp.spec.ts b/packages/functional-tests/tests/oauth/totp.spec.ts index ab57a610b59..fb74b334e25 100644 --- a/packages/functional-tests/tests/oauth/totp.spec.ts +++ b/packages/functional-tests/tests/oauth/totp.spec.ts @@ -29,7 +29,7 @@ test.describe('severity-1 #smoke', () => { const { secret } = await totp.fillOutTotpForms(); await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); await settings.signOut(); diff --git a/packages/functional-tests/tests/react-conversion/signinTotp.spec.ts b/packages/functional-tests/tests/react-conversion/signinTotp.spec.ts index c1250b8916b..52c8a0e68cb 100644 --- a/packages/functional-tests/tests/react-conversion/signinTotp.spec.ts +++ b/packages/functional-tests/tests/react-conversion/signinTotp.spec.ts @@ -33,7 +33,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -78,7 +78,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -122,7 +122,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); diff --git a/packages/functional-tests/tests/resetPassword/resetPassword.spec.ts b/packages/functional-tests/tests/resetPassword/resetPassword.spec.ts index 7559cf89174..ff1953934e4 100644 --- a/packages/functional-tests/tests/resetPassword/resetPassword.spec.ts +++ b/packages/functional-tests/tests/resetPassword/resetPassword.spec.ts @@ -109,7 +109,7 @@ test.describe('severity-1 #smoke', () => { resetPassword.resendButton.click(); - await expect(resetPassword.statusBar).toHaveText( + await expect(resetPassword.successBanner).toHaveText( /A new code was sent to your email./ ); }); @@ -121,7 +121,7 @@ test.describe('severity-1 #smoke', () => { await resetPassword.fillOutEmailForm('email@restmail.net'); - await expect(resetPassword.statusBar).toHaveText('Unknown account'); + await expect(resetPassword.errorBanner).toHaveText('Unknown account'); }); test('browse directly to page with email on query params', async ({ @@ -161,7 +161,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -226,7 +226,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -300,7 +300,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -385,7 +385,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); diff --git a/packages/functional-tests/tests/settings/totp.spec.ts b/packages/functional-tests/tests/settings/totp.spec.ts index c3ac4f2e85e..8614214ad04 100644 --- a/packages/functional-tests/tests/settings/totp.spec.ts +++ b/packages/functional-tests/tests/settings/totp.spec.ts @@ -56,7 +56,7 @@ test.describe('severity-1 #smoke', () => { await expect(settings.settingsHeading).toBeVisible(); await expect(settings.alertBar).toHaveText( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); await expect(settings.totp.status).toHaveText('Enabled'); @@ -343,7 +343,9 @@ async function addTotp( const totpCredentials = await totp.fillOutTotpForms(); await expect(settings.settingsHeading).toBeVisible(); - await expect(settings.alertBar).toHaveText('Two-step authentication enabled'); + await expect(settings.alertBar).toHaveText( + 'Two-step authentication has been enabled' + ); await expect(settings.totp.status).toHaveText('Enabled'); return totpCredentials; diff --git a/packages/fxa-settings/src/components/Banner/index.stories.tsx b/packages/fxa-settings/src/components/Banner/index.stories.tsx index 5237f4af94b..74a2d96d372 100644 --- a/packages/fxa-settings/src/components/Banner/index.stories.tsx +++ b/packages/fxa-settings/src/components/Banner/index.stories.tsx @@ -161,6 +161,21 @@ export const TypeInfo = () => ( ); +export const TypeInfoFancy = () => ( + + alert('Dismiss clicked') }} + /> + +); + export const TypeSuccess = () => ( { render(); expect(screen.getByText('Heading')).toBeInTheDocument(); expect(screen.getByText('This is a description')).toBeInTheDocument(); - expect( - screen.getByRole('img', { name: /Information/i }) - ).toBeInTheDocument(); }); it('renders the component with error type', () => { render(); expect(screen.getByText('Heading')).toBeInTheDocument(); expect(screen.getByText('This is a description')).toBeInTheDocument(); - expect(screen.getByRole('img', { name: /Error/i })).toBeInTheDocument(); }); it('renders the component with success type', () => { render(); expect(screen.getByText('Heading')).toBeInTheDocument(); expect(screen.getByText('This is a description')).toBeInTheDocument(); - expect(screen.getByRole('img', { name: /Success/i })).toBeInTheDocument(); }); it('renders the component with warning type', () => { render(); expect(screen.getByText('Heading')).toBeInTheDocument(); expect(screen.getByText('This is a description')).toBeInTheDocument(); - expect( - screen.getByRole('img', { name: 'Attention' }) - ).toBeInTheDocument(); }); }); diff --git a/packages/fxa-settings/src/components/Banner/index.tsx b/packages/fxa-settings/src/components/Banner/index.tsx index 91323df60bd..9d5acce5b46 100644 --- a/packages/fxa-settings/src/components/Banner/index.tsx +++ b/packages/fxa-settings/src/components/Banner/index.tsx @@ -10,6 +10,7 @@ import { InformationOutlineCurrentIcon as InfoIcon, AlertOutlineCurrentIcon as WarningIcon, ErrorOutlineCurrentIcon as ErrorIcon, + InformationOutlineBlueIcon, } from '../Icons'; import classNames from 'classnames'; import { useFtlMsgResolver } from '../../models'; @@ -23,31 +24,45 @@ export const Banner = ({ animation, dismissButton, link, + isFancy, + bannerId, }: BannerProps) => { return (
{/* Icon fills use 'currentColor' (from text color) for better accessibility in HCM mode */} - {type === 'error' && } - {type === 'info' && } + {type === 'error' && } + {type === 'info' && !isFancy && ( + + )} + {type === 'info' && isFancy && ( + + )} {type === 'success' && ( )} {type === 'warning' && ( - + )}
@@ -86,7 +101,12 @@ export const Banner = ({ className={classNames( 'shrink-0 self-start hover:backdrop-saturate-150 focus:backdrop-saturate-200', type === 'error' && 'hover:bg-red-200 focus:bg-red-300', - type === 'info' && 'hover:bg-blue-100 focus:bg-blue-200', + type === 'info' && + !isFancy && + 'hover:bg-blue-100 focus:bg-blue-200', + type === 'info' && + isFancy && + 'hover:bg-gradient-to-tr hover:from-blue-700/10 hover:to-purple-600/10 focus:bg-gradient-to-tr focus:from-blue-800/10 focus:to-purple-700/10', type === 'success' && 'hover:bg-green-400 focus:bg-green-500', type === 'warning' && 'hover:bg-orange-100 focus:bg-orange-200' )} diff --git a/packages/fxa-settings/src/components/Banner/interfaces.ts b/packages/fxa-settings/src/components/Banner/interfaces.ts index 0e44dc03a33..d3239de30b7 100644 --- a/packages/fxa-settings/src/components/Banner/interfaces.ts +++ b/packages/fxa-settings/src/components/Banner/interfaces.ts @@ -20,6 +20,8 @@ export type BannerProps = { animation?: Animation; dismissButton?: DismissButtonProps; link?: BannerLinkProps; + isFancy?: boolean; + bannerId?: string; }; export type Animation = { diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx index 9ea770a4f5e..0059046175d 100644 --- a/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx +++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx @@ -6,22 +6,55 @@ import React from 'react'; import FormPhoneNumber from '.'; import { withLocalization } from 'fxa-react/lib/storybooks'; import { Meta } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import AppLayout from '../AppLayout'; export default { title: 'Components/FormPhoneNumber', component: FormPhoneNumber, - decorators: [withLocalization], + decorators: [ + (Story) => ( + + + + ), + withLocalization, + ], } as Meta; +const mockSubmit = async (phoneNumber: string) => { + action('submitPhoneNumber')(phoneNumber); + return { hasErrors: false }; +}; + export const Default = () => ( - - - + +); + +export const WithError = () => ( + { + action('submitPhoneNumber')(); + return { hasErrors: true }; + }} + /> ); -export const WithInfo = () => ( - - - +export const WithInfoBanner = () => ( + ); diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx index 44da8b63865..243da99cf86 100644 --- a/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx +++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx @@ -4,14 +4,36 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import userEvent, { UserEvent } from '@testing-library/user-event'; +import userEvent from '@testing-library/user-event'; import FormPhoneNumber from '.'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; +const mockSubmit = jest.fn(); + describe('FormPhoneNumber', () => { function render() { renderWithLocalizationProvider( - + + ); + } + + function renderWithInfoBannerProps() { + renderWithLocalizationProvider( + ); } @@ -26,6 +48,32 @@ describe('FormPhoneNumber', () => { }); } + it('renders the component as expected', async () => { + render(); + await waitFor(() => { + expect(screen.getByRole('combobox')).toBeInTheDocument(); + expect(getPhoneInput()).toBeInTheDocument(); + expect(getSubmitButton()).toBeInTheDocument(); + }); + }); + + it('renders the component with info banner', async () => { + renderWithInfoBannerProps(); + await waitFor(() => { + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + expect(getPhoneInput()).toBeInTheDocument(); + expect(getSubmitButton()).toBeInTheDocument(); + + expect(screen.getByText('This is a banner heading')).toBeInTheDocument(); + expect( + screen.getByText('This is a banner description') + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: /This is a banner link/ }) + ).toBeInTheDocument(); + }); + it('submit button is disabled by default', async () => { render(); await waitFor(() => { @@ -34,49 +82,38 @@ describe('FormPhoneNumber', () => { }); describe('form validity and submission', () => { - // alertMock is temporary until real functionality is implemented - let alertMock: jest.SpyInstance; - beforeEach(() => { - alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {}); - }); - afterEach(() => { - alertMock.mockRestore(); - }); it('submit button is enabled for valid North American number, 1231231234, and is formatted as expected', async () => { const user = userEvent.setup(); render(); - await waitFor(async () => await user.type(getPhoneInput(), '1231231234')); + await waitFor(() => user.type(getPhoneInput(), '1231231234')); expect(getSubmitButton()).toBeEnabled(); - await waitFor(async () => await user.click(getSubmitButton())); - expect(alertMock).toHaveBeenCalledWith( - 'formattedPhoneNumber: +11231231234' + user.click(getSubmitButton()); + await waitFor(() => + expect(mockSubmit).toHaveBeenCalledWith('+11231231234') ); }); it('submit button is enabled for valid North American number, (123) 123-1234, and is formatted as expected', async () => { const user = userEvent.setup(); - const alertMock = jest - .spyOn(window, 'alert') - .mockImplementation(() => {}); render(); await waitFor( async () => await user.type(getPhoneInput(), '(123) 123-1234') ); expect(getSubmitButton()).toBeEnabled(); await waitFor(async () => await user.click(getSubmitButton())); - expect(alertMock).toHaveBeenCalledWith( - 'formattedPhoneNumber: +11231231234' - ); + expect(mockSubmit).toHaveBeenCalledWith('+11231231234'); }); - it('submit button is disabled for invalid 11-digit phoneValidationNorthAmerica, 12312312345', async () => { + it('input value is restricted to 10 digits', async () => { const user = userEvent.setup(); render(); await waitFor( async () => await user.type(getPhoneInput(), '12312312345') ); - expect(getSubmitButton()).toBeDisabled(); + expect(getPhoneInput()).toHaveValue('123-123-1234'); + expect(getSubmitButton()).toBeEnabled(); }); + it('submit button is disabled for invalid number with letters phoneValidationNorthAmerica, abc12312345', async () => { const user = userEvent.setup(); render(); diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx index e280c0b84b7..87a4c75b608 100644 --- a/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx +++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx @@ -3,56 +3,115 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import InputPhoneNumber from '../InputPhoneNumber'; import Banner from '../Banner'; +import { BannerContentProps, BannerLinkProps } from '../Banner/interfaces'; export interface InputPhoneNumberData { phoneNumber: string; countryCode: string; } +export type FormPhoneNumberProps = { + infoBannerContent?: BannerContentProps; + infoBannerLink?: BannerLinkProps; + localizedCTAText: string; + submitPhoneNumber: (phoneNumber: string) => Promise<{ hasErrors: boolean }>; + errorBannerId?: string; +}; + const FormPhoneNumber = ({ - showInfo = false, + infoBannerContent, + infoBannerLink, localizedCTAText, -}: { - showInfo?: boolean; - localizedCTAText: string; -}) => { - const { handleSubmit, register, formState } = useForm({ - mode: 'onChange', - criteriaMode: 'all', - defaultValues: { - phoneNumber: '', - countryCode: '', - }, + submitPhoneNumber, + errorBannerId, +}: FormPhoneNumberProps) => { + const [hasErrors, setHasErrors] = React.useState(false); + const { control, formState, handleSubmit, register } = + useForm({ + mode: 'onChange', + criteriaMode: 'all', + defaultValues: { + phoneNumber: '', + countryCode: '', + }, + }); + + // Use `useWatch` to observe the `phoneNumber` field without causing re-renders + const phoneNumberInput: string | undefined = useWatch({ + control, + name: 'phoneNumber', }); - const onSubmit = async ({ + const formatPhoneNumber = ({ phoneNumber, countryCode, }: InputPhoneNumberData) => { // Strip everything that isn't a number const strippedNumber = phoneNumber.replace(/\D/g, ''); - const formattedPhoneNumber = countryCode + strippedNumber; + return countryCode + strippedNumber; + }; - // TODO, actually send this value where needed - alert(`formattedPhoneNumber: ${formattedPhoneNumber}`); + const onSubmit = async ({ + phoneNumber, + countryCode, + }: InputPhoneNumberData) => { + setHasErrors(false); + const formattedPhoneNumber = formatPhoneNumber({ + phoneNumber, + countryCode, + }); + const result = await submitPhoneNumber(formattedPhoneNumber); + if (result !== undefined && result.hasErrors) { + setHasErrors(true); + const phoneInput = document.querySelector( + 'input[name="phoneNumber"]' + ) as HTMLInputElement; + phoneInput && phoneInput.focus(); + } }; return (
- + + + {infoBannerContent && !infoBannerLink && ( + + )} - {showInfo && ( - + {infoBannerContent && infoBannerLink && ( + )}
diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/mocks.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/mocks.tsx deleted file mode 100644 index 1ac5b226ed2..00000000000 --- a/packages/fxa-settings/src/components/FormPhoneNumber/mocks.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * 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 React from 'react'; -import FormPhoneNumber from '.'; -import AppLayout from '../AppLayout'; - -export const Subject = () => { - return ( - - - - ); -}; diff --git a/packages/fxa-settings/src/components/FormVerifyTotp/index.tsx b/packages/fxa-settings/src/components/FormVerifyTotp/index.tsx index fa7444adfce..1aec6ac9fd7 100644 --- a/packages/fxa-settings/src/components/FormVerifyTotp/index.tsx +++ b/packages/fxa-settings/src/components/FormVerifyTotp/index.tsx @@ -18,6 +18,7 @@ const FormVerifyTotp = ({ clearBanners, codeLength, codeType, + errorBannerId, errorMessage, localizedInputLabel, localizedSubmitButtonText, @@ -104,6 +105,7 @@ const FormVerifyTotp = ({ spellCheck={false} inputRef={register({ required: true })} hasErrors={!!errorMessage} + aria-describedby={errorBannerId} /> -

{title}

+

{title}

{subtitle && (

diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/en.ftl b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/en.ftl new file mode 100644 index 00000000000..52a8b9d78cc --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/en.ftl @@ -0,0 +1,16 @@ +## FlowSetupPhoneConfirmCode + +# verification code refers to a code sent by text message to confirm phone number ownership +# and complete setup +flow-setup-phone-confirm-code-heading = Enter verification code + +# $phoneNumber is a partially obfuscated phone number with only the last 4 digits showing (e.g., *** *** 1234) +# span element applies formatting to ensure the number is always displayed left-to-right +flow-setup-phone-confirm-code-instruction = A six-digit code was sent to { $phoneNumber } by text message. This code expires after 5 minutes. +flow-setup-phone-confirm-code-input-label = Enter 6-digit code +flow-setup-phone-confirm-code-button = Confirm +# button to resend a code by text message to the user's phone +# followed by a button to resend a code +flow-setup-phone-confirm-code-expired = Code expired? +flow-setup-phone-confirm-code-resend-code-button = Resend code +flow-setup-phone-confirm-code-success-message = Backup recovery phone added diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.stories.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.stories.tsx new file mode 100644 index 00000000000..ea67ae999e4 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.stories.tsx @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React from 'react'; +import { Meta } from '@storybook/react'; +import { withLocalization } from 'fxa-react/lib/storybooks'; +import SettingsLayout from '../SettingsLayout'; +import { action } from '@storybook/addon-actions'; +import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors'; +import FlowSetupRecoveryPhoneConfirmCode from '.'; + +export default { + title: 'Components/Settings/FlowSetupRecoveryPhoneConfirmCode', + component: FlowSetupRecoveryPhoneConfirmCode, + decorators: [withLocalization], +} as Meta; + +const localizedBackButtonTitle = 'Back'; +const localizedPageTitle = 'Add recovery phone'; + +const navigateBackward = async () => { + action('navigateBackward')(); +}; + +const navigateForward = async () => { + action('navigateForward')(); +}; + +const resendCodeSuccess = async () => { + action('resendCode')(); +}; + +const resendCodeFailure = async () => { + return Promise.reject(AuthUiErrors.THROTTLED); +}; + +const verifyRecoveryCodeSuccess = async (code: string) => { + action('verifyRecoveryCode')(code); +}; + +const verifyRecoveryCodeFailure = async (code: string) => { + return Promise.reject(AuthUiErrors.UNEXPECTED_ERROR); +}; + +const formattedPhoneNumber = '+1 123-456-3019'; + +export const Success = () => ( + + + +); + +export const Error = () => ( + + + +); diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.test.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.test.tsx new file mode 100644 index 00000000000..65b1dcac55a --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.test.tsx @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import FlowSetupRecoveryPhoneConfirmCode from '.'; +import '@testing-library/jest-dom'; + +jest.mock('../../../lib/error-utils', () => ({ + getLocalizedErrorMessage: jest.fn(() => 'Localized error message'), +})); + +jest.mock('../../../models', () => ({ + useAlertBar: jest.fn(() => ({ + success: jest.fn(), + })), + useFtlMsgResolver: jest.fn(() => ({ + getMsg: (id: string, fallback: string) => fallback, + })), +})); + +const mockNavigateBackward = jest.fn(); +const mockNavigateForward = jest.fn(); +const mockSendCode = jest.fn(); +const mockVerifyRecoveryCode = jest.fn(); + +const defaultProps = { + localizedBackButtonTitle: 'Back', + localizedPageTitle: 'Add phone number', + navigateBackward: mockNavigateBackward, + navigateForward: mockNavigateForward, + formattedPhoneNumber: '+1 (555) 555-5555', + sendCode: mockSendCode, + verifyRecoveryCode: mockVerifyRecoveryCode, +}; + +describe('FlowSetupRecoveryPhoneConfirmCode', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders component correctly', () => { + render(); + + expect( + screen.getByRole('heading', { name: /Enter verification code/i }) + ).toBeInTheDocument(); + expect( + screen.getByText(/A six-digit code was sent to/i) + ).toBeInTheDocument(); + expect(screen.getByText(/\+1 \(555\) 555-5555/)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Resend code/i }) + ).toBeInTheDocument(); + const disabledSubmitButton = screen.getByRole('button', { + name: /Enter 6-digit code to continue/i, + }); + expect(disabledSubmitButton).toBeInTheDocument(); + expect(disabledSubmitButton).toBeDisabled(); + }); + + test('enables submit button when code is entered', async () => { + render(); + + // when code in input field is empty or incomplete + // a title is added to the button to indicate to the user to enter the code + const input = screen.getByLabelText(/Enter 6-digit code/i); + await waitFor(() => + fireEvent.change(input, { target: { value: '123456' } }) + ); + + // submit button is enabled, the accessible button text becomes the button text without the title + const submitButton = screen.getByRole('button', { + name: /Confirm/i, + }); + expect(submitButton).toBeInTheDocument(); + expect(submitButton).toBeEnabled(); + }); + + test('handles resend code action successfully', async () => { + mockSendCode.mockResolvedValueOnce(undefined); + + render(); + + fireEvent.click(screen.getByRole('button', { name: /Resend code/i })); + + await waitFor(() => { + expect(mockSendCode).toHaveBeenCalledTimes(1); + expect(screen.getByText(/Resend code/i)).toBeInTheDocument(); + }); + }); + + test('handles error when resending code', async () => { + mockSendCode.mockRejectedValueOnce(new Error('Network error')); + + render(); + + fireEvent.click(screen.getByRole('button', { name: /Resend code/i })); + + await waitFor(() => { + expect(screen.getByText(/Localized error message/i)).toBeInTheDocument(); + }); + }); + + test('handles successful verification code submission', async () => { + mockVerifyRecoveryCode.mockResolvedValueOnce(undefined); + + render(); + + const input = screen.getByLabelText(/Enter 6-digit code/i); + await waitFor(() => + fireEvent.change(input, { target: { value: '123456' } }) + ); + + fireEvent.click(screen.getByRole('button', { name: /Confirm/i })); + + await waitFor(() => { + expect(mockVerifyRecoveryCode).toHaveBeenCalledWith('123456'); + expect(mockNavigateForward).toHaveBeenCalledTimes(1); + }); + }); + + test('handles error during verification code submission', async () => { + mockVerifyRecoveryCode.mockRejectedValueOnce(new Error('Invalid code')); + + render(); + + const input = screen.getByLabelText(/Enter 6-digit code/i); + await waitFor(() => + fireEvent.change(input, { target: { value: '654321' } }) + ); + + fireEvent.click(screen.getByRole('button', { name: /Confirm/i })); + + await waitFor(() => { + expect(mockVerifyRecoveryCode).toHaveBeenCalledWith('654321'); + expect(screen.getByText(/Localized error message/i)).toBeInTheDocument(); + }); + }); + + test('navigates backward when back button is clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /Back/i })); + + expect(mockNavigateBackward).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.tsx new file mode 100644 index 00000000000..2cd1b238712 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneConfirmCode/index.tsx @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React, { useState } from 'react'; +import FlowContainer from '../FlowContainer'; +import ProgressBar from '../ProgressBar'; +import { FtlMsg } from 'fxa-react/lib/utils'; +import Banner, { ResendCodeSuccessBanner } from '../../Banner'; +import FormVerifyTotp from '../../FormVerifyTotp'; +import { BackupRecoveryPhoneCodeImage } from '../../images'; +import { getLocalizedErrorMessage } from '../../../lib/error-utils'; +import { useAlertBar, useFtlMsgResolver } from '../../../models'; +import { ResendStatus } from '../../../lib/types'; + +export type FlowSetupRecoveryPhoneConfirmCodeProps = { + currentStep?: number; + formattedPhoneNumber: string; + localizedBackButtonTitle: string; + localizedPageTitle: string; + navigateBackward: () => void; + navigateForward: () => void; + numberOfSteps?: number; + sendCode: () => Promise; + verifyRecoveryCode: (code: string) => Promise; +}; + +export const FlowSetupRecoveryPhoneConfirmCode = ({ + currentStep = 2, + formattedPhoneNumber, + localizedBackButtonTitle, + localizedPageTitle, + navigateBackward, + navigateForward, + numberOfSteps = 2, + sendCode, + verifyRecoveryCode, +}: FlowSetupRecoveryPhoneConfirmCodeProps) => { + const [localizedErrorBannerMessage, setLocalizedErrorBannerMessage] = + useState(''); + const [resendStatus, setResendStatus] = useState( + ResendStatus.none + ); + + const alertBar = useAlertBar(); + const ftlMsgResolver = useFtlMsgResolver(); + + const clearBanners = async () => { + setResendStatus(ResendStatus.none); + setLocalizedErrorBannerMessage(''); // Clear the banner message + }; + + const handleResendCode = async () => { + await clearBanners(); + try { + await sendCode(); + setResendStatus(ResendStatus.sent); + } catch (error) { + const localizedError = getLocalizedErrorMessage(ftlMsgResolver, error); + setLocalizedErrorBannerMessage(localizedError); + } + return; + }; + + const handleSubmit = async (code: string) => { + await clearBanners(); + + try { + await verifyRecoveryCode(code); + alertBar.success( + ftlMsgResolver.getMsg( + 'flow-setup-phone-confirm-code-success-message', + 'Backup recovery phone added' + ) + ); + navigateForward(); + } catch (error) { + const localizedError = getLocalizedErrorMessage(ftlMsgResolver, error); + setLocalizedErrorBannerMessage(localizedError); + const codeInput = document.querySelector( + 'input[name="code"]' + ) as HTMLInputElement; + if (codeInput) { + codeInput.focus(); + } + } + return; + }; + + return ( + + + {resendStatus === ResendStatus.sent && !localizedErrorBannerMessage && ( + + )} + {localizedErrorBannerMessage && ( + + )} + + +

Enter verification code

+
+ }} + vars={{ phoneNumber: formattedPhoneNumber }} + > +

+ A six-digit code was sent to{' '} + + {formattedPhoneNumber} + {' '} + by text message. This code expires after 5 minutes. +

+
+ + +
+ +

Code expired?

+
+ + + +
+
+ ); +}; + +export default FlowSetupRecoveryPhoneConfirmCode; diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/en.ftl b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/en.ftl new file mode 100644 index 00000000000..b5f98921eb5 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/en.ftl @@ -0,0 +1,15 @@ +## FlowSetupPhoneConfirmCode + +flow-setup-phone-submit-number-heading = Verify your phone number +# The code is a 6-digit code send by text message/SMS +flow-setup-phone-verify-number-instruction = You’ll get a text message from Mozilla with a code to verify your number. Don't share this code with anyone. + +# The initial rollout of the backup recovery phone is only available to users with US and Canada mobile phone numbers. +# Voice over Internet Protocol (VoIP), is a technology that uses a broadband Internet connection instead of a regular (or analog) phone line to make calls. +# Phone mask services (for example Relay) provide a temporary virtual number to avoid providing a real phone number. +# Both VoIP and phone masks can be unreliable for one-time-passcode (OTP) verification +flow-setup-phone-submit-number-info-message = Backup recovery phone is only available in the United States and Canada. VoIP numbers and phone masks are not recommended. + +flow-setup-phone-submit-number-legal = By providing your number, you agree to us storing it so we can text you for account verification only. Message and data rates may apply. +# cliking on the button sends a code by text message to the phone number typed in by the user +flow-setup-phone-submit-number-button = Send code diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.stories.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.stories.tsx new file mode 100644 index 00000000000..506fdee9651 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.stories.tsx @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React from 'react'; +import { Meta } from '@storybook/react'; +import { withLocalization } from 'fxa-react/lib/storybooks'; +import SettingsLayout from '../SettingsLayout'; +import { action } from '@storybook/addon-actions'; +import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors'; +import FlowSetupRecoveryPhoneSubmitNumber from '.'; + +export default { + title: 'Components/Settings/FlowSetupRecoveryPhoneSubmitNumber', + component: FlowSetupRecoveryPhoneSubmitNumber, + decorators: [withLocalization], +} as Meta; + +const localizedBackButtonTitle = 'Back'; +const localizedPageTitle = 'Add recovery phone'; + +const navigateBackward = async () => { + action('navigateBackward')(); +}; + +const navigateForward = async () => { + action('navigateForward')(); +}; + +const verifyNumberSuccess = async (phoneNumber: string) => { + action('verifyPhoneNumber')(phoneNumber); +}; + +const verifyNumberFailure = async (phoneNumber: string) => { + return Promise.reject(AuthUiErrors.UNEXPECTED_ERROR); +}; + +export const Success = () => ( + + + +); + +export const Error = () => ( + + + +); diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.test.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.test.tsx new file mode 100644 index 00000000000..36e3ff1784c --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.test.tsx @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import FlowSetupRecoveryPhoneSubmitNumber from '.'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; + +jest.mock('../../../lib/error-utils', () => ({ + getLocalizedErrorMessage: jest.fn(() => 'Localized error message'), +})); + +jest.mock('../../../models', () => ({ + useFtlMsgResolver: jest.fn(() => ({ + getMsg: (id: string, fallback: string) => fallback, + })), +})); + +const mockNavigateBackward = jest.fn(); +const mockNavigateForward = jest.fn(); +const mockVerifyPhoneNumber = jest.fn(); + +const defaultProps = { + localizedBackButtonTitle: 'Back', + localizedPageTitle: 'Add phone number', + navigateBackward: mockNavigateBackward, + navigateForward: mockNavigateForward, + verifyPhoneNumber: mockVerifyPhoneNumber, +}; + +describe('FlowSetupRecoveryPhoneSubmitNumber', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders component as expected', async () => { + renderWithLocalizationProvider( + + ); + + await waitFor(() => + expect( + screen.getByRole('heading', { name: /Verify your phone number/i }) + ).toBeInTheDocument() + ); + expect( + screen.getByText(/You’ll get a text message from Mozilla/i) + ).toBeInTheDocument(); + expect( + screen.getByRole('combobox', { name: /Select country/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole('textbox', { name: /Enter phone number/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Send code/i }) + ).toBeInTheDocument(); + expect( + screen.getByText( + /By providing your number, you agree to us storing it so we can text you for account verification only. Message and data rates may apply./i + ) + ).toBeInTheDocument(); + }); + + test('handles successful number verification', async () => { + const user = userEvent.setup(); + renderWithLocalizationProvider( + + ); + + await waitFor(() => + user.type( + screen.getByRole('textbox', { name: /Enter phone number/i }), + '1231231234' + ) + ); + user.click(screen.getByRole('button', { name: /Send code/i })); + + await waitFor(() => expect(mockVerifyPhoneNumber).toHaveBeenCalledTimes(1)); + expect(mockNavigateForward).toHaveBeenCalledTimes(1); + }); + + test('handles error during number verification', async () => { + mockVerifyPhoneNumber.mockRejectedValueOnce(new Error('error')); + const user = userEvent.setup(); + renderWithLocalizationProvider( + + ); + + await waitFor(() => + user.type( + screen.getByRole('textbox', { name: /Enter phone number/i }), + '1231231234' + ) + ); + user.click(screen.getByRole('button', { name: /Send code/i })); + + await waitFor(() => expect(mockVerifyPhoneNumber).toHaveBeenCalledTimes(0)); + expect(mockNavigateForward).toHaveBeenCalledTimes(0); + await waitFor(() => + expect(screen.getByText('Localized error message')).toBeInTheDocument() + ); + }); + + test('navigates backward when back button is clicked', async () => { + const user = userEvent.setup(); + renderWithLocalizationProvider( + + ); + + user.click(screen.getByRole('button', { name: /Back/i })); + + await waitFor(() => expect(mockNavigateBackward).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.tsx b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.tsx new file mode 100644 index 00000000000..a9a17f635e9 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/FlowSetupRecoveryPhoneSubmitNumber/index.tsx @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React, { useState } from 'react'; +import FlowContainer from '../FlowContainer'; +import ProgressBar from '../ProgressBar'; +import { FtlMsg } from 'fxa-react/lib/utils'; +import Banner from '../../Banner'; +import { BackupRecoveryPhoneImage } from '../../images'; +import { getLocalizedErrorMessage } from '../../../lib/error-utils'; +import { useFtlMsgResolver } from '../../../models'; +import FormPhoneNumber from '../../FormPhoneNumber'; + +export type FlowSetupRecoveryPhoneSubmitNumberProps = { + currentStep?: number; + localizedBackButtonTitle: string; + localizedPageTitle: string; + navigateBackward: () => void; + navigateForward: () => void; + numberOfSteps?: number; + verifyPhoneNumber: (phoneNumber: string) => Promise; +}; + +export const FlowSetupRecoveryPhoneSubmitNumber = ({ + currentStep = 1, + localizedBackButtonTitle, + localizedPageTitle, + navigateBackward, + navigateForward, + numberOfSteps = 2, + verifyPhoneNumber, +}: FlowSetupRecoveryPhoneSubmitNumberProps) => { + const [localizedErrorBannerMessage, setLocalizedErrorBannerMessage] = + useState(''); + + const ftlMsgResolver = useFtlMsgResolver(); + + const clearBanners = async () => { + setLocalizedErrorBannerMessage(''); // Clear the banner message + }; + + const handlePhoneNumber = async (phoneNumber: string) => { + await clearBanners(); + try { + await verifyPhoneNumber(phoneNumber); + navigateForward(); + return { hasErrors: false }; + } catch (error) { + const localizedError = getLocalizedErrorMessage(ftlMsgResolver, error); + setLocalizedErrorBannerMessage(localizedError); + return { hasErrors: true }; + } + }; + + return ( + + + {localizedErrorBannerMessage && ( + + )} + + +

Verify your phone number

+
+ +

+ You’ll get a text message from Mozilla with a code to verify your + number. Don't share this code with anyone. +

+
+ + +

+ By providing your number, you agree to us storing it so we can text + you for account verification only. Message and data rates may apply. +

+
+
+ ); +}; + +export default FlowSetupRecoveryPhoneSubmitNumber; diff --git a/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/en.ftl b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/en.ftl new file mode 100644 index 00000000000..8c7f8bac0f4 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/en.ftl @@ -0,0 +1,3 @@ +## PageSetupRecoveryPhone + +page-setup-recovery-phone-heading = Add recovery phone diff --git a/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.stories.tsx b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.stories.tsx new file mode 100644 index 00000000000..8986202ae91 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.stories.tsx @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React from 'react'; +import PageRecoveryPhoneSetup from '.'; +import { withLocalization } from 'fxa-react/lib/storybooks'; +import { Meta } from '@storybook/react'; +import SettingsLayout from '../SettingsLayout'; +import { LocationProvider } from '@reach/router'; + +export default { + title: 'Pages/Settings/RecoveryPhoneSetup', + component: PageRecoveryPhoneSetup, + decorators: [withLocalization], +} as Meta; + +export const Step1 = () => ( + + + + + +); + +export const Step2 = () => ( + + + + + +); diff --git a/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.tsx b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.tsx new file mode 100644 index 00000000000..a922a1c8473 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/PageRecoveryPhoneSetup/index.tsx @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 React, { useState } from 'react'; +import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery'; +import { SETTINGS_PATH } from '../../../constants'; +import { useFtlMsgResolver } from '../../../models'; +import VerifiedSessionGuard from '../VerifiedSessionGuard'; +import FlowSetupRecoveryPhoneConfirmCode from '../FlowSetupRecoveryPhoneConfirmCode'; +import FlowSetupRecoveryPhoneSubmitNumber from '../FlowSetupRecoveryPhoneSubmitNumber'; + +const numberOfSteps = 2; + +type PageRecoveryPhoneSetupProps = { + testPhoneNumber?: string; + testStep?: 1 | 2; +}; + +// temporary props for storybook purposes +export const PageRecoveryPhoneSetup = ({ + testPhoneNumber, + testStep, +}: PageRecoveryPhoneSetupProps) => { + const ftlMsgResolver = useFtlMsgResolver(); + const navigate = useNavigate(); + + const [currentStep, setCurrentStep] = useState(testStep || 1); + const [formattedPhoneNumber, setFormattedPhoneNumber] = useState( + testPhoneNumber || '' + ); + + const goHome = () => + navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true }); + + const localizedPageTitle = ftlMsgResolver.getMsg( + 'page-setup-recovery-phone-title', + 'Add recovery phone' + ); + + const localizedBackButtonTitle = ftlMsgResolver.getMsg( + 'page-setup-recovery-phone-back-button-title', + 'Back to settings' + ); + + const navigateBackward = () => { + navigate(SETTINGS_PATH); + }; + + const navigateForward = (e?: React.MouseEvent) => { + e?.preventDefault(); + if (currentStep + 1 <= numberOfSteps) { + setCurrentStep(currentStep + 1); + } else { + navigate(SETTINGS_PATH); + } + }; + + const sendCode = async () => { + // Placeholder until we have a proper SMS code sender + }; + + const verifyRecoveryCode = async (code: string) => { + // Placeholder until we have a proper SMS code verifier + }; + + const verifyPhoneNumber = async (phoneNumber: string) => { + // Placeholder until we have a proper phone number verifier + // for now let's just make it available for the next step + await setFormattedPhoneNumber(phoneNumber); + }; + + return ( + <> + + {/* Verify and submit phone number */} + {currentStep === 1 && ( + + )} + + {/* Confirm code received via SMS */} + {currentStep === 2 && ( + + )} + + ); +}; + +export default PageRecoveryPhoneSetup; diff --git a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/en.ftl b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/en.ftl index a5086393507..bd21edf07cf 100644 --- a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/en.ftl +++ b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/en.ftl @@ -14,7 +14,7 @@ tfa-incorrect-totp = Incorrect two-step authentication code tfa-cannot-retrieve-code = There was a problem retrieving your code. tfa-cannot-verify-code-4 = There was a problem confirming your backup authentication code tfa-incorrect-recovery-code-1 = Incorrect backup authentication code -tfa-enabled = Two-step authentication enabled +tfa-enabled-v2 = Two-step authentication has been enabled tfa-scan-this-code = Scan this QR code using one of these authentication apps. diff --git a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.test.tsx b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.test.tsx index 16a6607a467..5f047d51b1e 100644 --- a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.test.tsx +++ b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.test.tsx @@ -326,7 +326,7 @@ describe('step 3', () => { ); expect(alertBarInfo.success).toHaveBeenCalledTimes(1); expect(alertBarInfo.success).toHaveBeenCalledWith( - 'Two-step authentication enabled' + 'Two-step authentication has been enabled' ); }); }); diff --git a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.tsx b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.tsx index fdf9eb56938..559095955d7 100644 --- a/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.tsx +++ b/packages/fxa-settings/src/components/Settings/PageTwoStepAuthentication/index.tsx @@ -37,7 +37,11 @@ export const PageTwoStepAuthentication = (_: RouteComponentProps) => { navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true }); const alertSuccessAndGoHome = useCallback(() => { alertBar.success( - l10n.getString('tfa-enabled', null, 'Two-step authentication enabled') + l10n.getString( + 'tfa-enabled-v2', + null, + 'Two-step authentication has been enabled' + ) ); navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true }); }, [alertBar, l10n, navigate]); diff --git a/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/en.ftl b/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/en.ftl index 37957404d22..40a1a90fd6b 100644 --- a/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/en.ftl +++ b/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/en.ftl @@ -11,9 +11,11 @@ tfa-row-button-refresh = tfa-row-cannot-refresh = Sorry, there was a problem refreshing two-step authentication. tfa-row-enabled-description = Your account is protected by two-step authentication. You will need to enter a one-time passcode from your authentication app when logging into your { -product-mozilla-account }. +# "this" refers to two-step authentication +# Link goes to https://support.mozilla.org/kb/secure-firefox-account-two-step-authentication +tfa-row-enabled-info-link = How this protects your account -# goes to https://support.mozilla.org/kb/secure-firefox-account-two-step-authentication -tfa-row-disabled-description = Help secure your account by using a third-party authenticator app as a second step to sign in. +tfa-row-disabled-description-v2 = Help secure your account by using a third-party authenticator app as a second step to sign in. tfa-row-cannot-verify-session-4 = Sorry, there was a problem confirming your session tfa-row-disable-modal-heading = Disable two-step authentication? diff --git a/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/index.tsx b/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/index.tsx index 7b792eb8f52..374557e2f6e 100644 --- a/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/index.tsx +++ b/packages/fxa-settings/src/components/Settings/UnitRowTwoStepAuth/index.tsx @@ -171,13 +171,15 @@ export const UnitRowTwoStepAuth = ({ return subRows; }; - const authenticatorAppInfoLink = ( - - third-party authenticator app - + const howThisProtectsYourAccountLink = ( + + + How this protects your account + + ); return ( @@ -203,25 +205,21 @@ export const UnitRowTwoStepAuth = ({ > {exists && verified ? ( -

+

Your account is protected by two-step authentication. You will need to enter a one-time passcode from your authenticator app when logging into your Mozilla account.

) : ( - +

- Help secure your account by using a {authenticatorAppInfoLink} as - a second step to sign in. + Help secure your account by using a third-party authenticator app + as a second step to sign in.

)} + {howThisProtectsYourAccountLink} {disable2FAModalRevealed && } diff --git a/packages/fxa-settings/src/pages/InlineRecoverySetup/en.ftl b/packages/fxa-settings/src/pages/InlineRecoverySetup/en.ftl index 33114473987..e4f05c5c664 100644 --- a/packages/fxa-settings/src/pages/InlineRecoverySetup/en.ftl +++ b/packages/fxa-settings/src/pages/InlineRecoverySetup/en.ftl @@ -30,4 +30,4 @@ inline-recovery-confirmation-header-default = Confirm backup authentication code # If more appropriate in a locale, the string within the , "to continue to { $serviceName }" can stand alone as "Continue to { $serviceName }" # $serviceName - the name of the service which is using Mozilla accounts to authenticate inline-recovery-confirmation-header = Confirm backup authentication code to continue to { $serviceName } -inline-recovery-2fa-enabled = Two-step authentication enabled +inline-recovery-2fa-enabled-v2 = Two-step authentication has been enabled diff --git a/packages/fxa-settings/src/pages/InlineRecoverySetup/index.tsx b/packages/fxa-settings/src/pages/InlineRecoverySetup/index.tsx index ac0e72622c0..f06ec493c9a 100644 --- a/packages/fxa-settings/src/pages/InlineRecoverySetup/index.tsx +++ b/packages/fxa-settings/src/pages/InlineRecoverySetup/index.tsx @@ -52,8 +52,8 @@ const InlineRecoverySetup = ({ type="success" content={{ localizedHeading: ftlMsgResolver.getMsg( - 'inline-recovery-2fa-enabled', - 'Two-step authentication enabled' + 'inline-recovery-2fa-enabled-v2', + 'Two-step authentication has been enabled' ), }} />