fxa-settings(storybook): add account_recovery_reset_password

Because:

* We're making React versions of content server's remaining backbone
  pages, and moving them first into Storybook.

This commit:

* Sets up the page view metric, basic tests, and storybook stories for
  the account_recovery_reset_password page.

Closes #https://mozilla-hub.atlassian.net/browse/FXA-6343

Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl

Co-authored-by: Bryan Olsson <bolsson@mozilla.com>

Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl

Co-authored-by: Bryan Olsson <bolsson@mozilla.com>

Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl

Co-authored-by: Bryan Olsson <bolsson@mozilla.com>

Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl

Co-authored-by: Bryan Olsson <bolsson@mozilla.com>

Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl

Co-authored-by: Bryan Olsson <bolsson@mozilla.com>
This commit is contained in:
Mill 2022-12-28 12:13:36 -08:00
Родитель 8eec5f829d
Коммит 9873858162
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 191635F49DE50466
4 изменённых файлов: 366 добавлений и 0 удалений

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

@ -0,0 +1,14 @@
## Account recovery reset password page
# Appears when a link to reset password has expired
password-link-expired-header = Reset password link expired
# Appears when a link to reset password is damaged
password-link-damaged-header = Reset password link damaged
# Header for form to create new password
create-new-password-header = Create new password
# Link that user can click to receive a new reset password link
receive-new-link = Receive new link
confirm-account-recovery-key-button = Reset password
account-restored-success-message = You have successfully restored your account using your account recovery key. Create a new password to secure your data, and store it in a safe location.
password-link-damaged-message = The link you clicked was missing characters, and may have been broken by your email client. Copy the address carefully, and try again.
password-link-expired-message = The link you clicked to reset your password is expired.

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

@ -0,0 +1,38 @@
/* 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 AccountRecoveryResetPassword, {
AccountRecoveryResetPasswordProps,
} from '.';
import AppLayout from '../../components/AppLayout';
import { LocationProvider } from '@reach/router';
import { Meta } from '@storybook/react';
export default {
title: 'pages/AccountRecoveryResetPassword',
component: AccountRecoveryResetPassword,
} as Meta;
const storyWithProps = (props: AccountRecoveryResetPasswordProps) => {
const story = () => (
<LocationProvider>
<AppLayout>
<AccountRecoveryResetPassword {...props} />
</AppLayout>
</LocationProvider>
);
return story;
};
export const WithBrokenLink = storyWithProps({ linkStatus: 'broken' });
export const WithExpiredLink = storyWithProps({ linkStatus: 'expired' });
export const WithValidLink = storyWithProps({ linkStatus: 'valid' });
export const CanGoBack = storyWithProps({
canGoBack: true,
linkStatus: 'valid',
});

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

@ -0,0 +1,81 @@
/* 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 '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';
// import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
// import { FluentBundle } from '@fluent/bundle';
import { usePageViewEvent } from '../../lib/metrics';
import AccountRecoveryResetPassword from '.';
jest.mock('../../lib/metrics', () => ({
usePageViewEvent: jest.fn(),
logViewEvent: jest.fn(),
}));
const mockNavigate = jest.fn();
jest.mock('@reach/router', () => ({
...jest.requireActual('@reach/router'),
useNavigate: () => mockNavigate,
}));
describe('PageAccountRecoveryResetPassword', () => {
// TODO: enable l10n tests when they've been updated to handle embedded tags in ftl strings
// TODO: in FXA-6461
// let bundle: FluentBundle;
// beforeAll(async () => {
// bundle = await getFtlBundle('settings');
// });
it('renders as expected with valid link', () => {
render(<AccountRecoveryResetPassword linkStatus="valid" />);
// testAllL10n(screen, bundle);
const headingEl = screen.getByRole('heading', { level: 1 });
expect(headingEl).toHaveTextContent('Create new password');
expect(screen.getByLabelText('New password')).toBeInTheDocument();
expect(screen.getByLabelText('Current password')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Reset password' })
).toBeInTheDocument();
// when 'canGoBack: false' or not passed as prop, the optional RememberPassword link component should not be rendered
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
it('shows a different message when given a broken link', () => {
render(<AccountRecoveryResetPassword linkStatus="broken" />);
const headingEl = screen.getByRole('heading', { level: 1 });
expect(headingEl).toHaveTextContent(`Reset password link damaged`);
});
it('shows a different message for an expired link, with a button for getting a new link', () => {
render(<AccountRecoveryResetPassword linkStatus="expired" />);
const headingEl = screen.getByRole('heading', { level: 1 });
expect(headingEl).toHaveTextContent(`Reset password link expired`);
expect(
screen.getByRole('button', { name: 'Receive new link' })
).toBeInTheDocument();
});
it('renders a "Remember your password?" link if "canGoBack: true"', () => {
render(
<AccountRecoveryResetPassword canGoBack={true} linkStatus="valid" />
);
expect(
screen.getByRole('link', { name: 'Remember your password? Sign in' })
).toBeInTheDocument();
});
it('emits a metrics event on render', () => {
render(<AccountRecoveryResetPassword linkStatus="valid" />);
expect(usePageViewEvent).toHaveBeenCalledWith(
`account-recovery-reset-password`,
{
entrypoint_variation: 'react',
}
);
});
});

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

@ -0,0 +1,233 @@
/* 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 { RouteComponentProps } from '@reach/router';
import React, { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from '@reach/router';
import { logViewEvent, usePageViewEvent } from '../../lib/metrics';
import { useAccount, useAlertBar } from '../../models';
import { FtlMsg } from 'fxa-react/lib/utils';
import { useFtlMsgResolver } from '../../models/hooks';
import { InputText } from '../../components/InputText';
import LinkRememberPassword from '../../components/LinkRememberPassword';
// --canGoBack-- determines if the user can navigate back to an fxa entrypoint
export type AccountRecoveryResetPasswordProps = {
canGoBack?: boolean;
linkStatus: LinkStatus;
};
type FormData = {
currentPassword: string;
newPassword: string;
};
type LinkStatus = 'expired' | 'broken' | 'valid';
const AccountRecoveryResetPassword = ({
canGoBack,
linkStatus,
}: AccountRecoveryResetPasswordProps & RouteComponentProps) => {
usePageViewEvent('account-recovery-reset-password', {
entrypoint_variation: 'react',
});
const [currentPassword, setCurrentPassword] = useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
const [currentPasswordErrorText, setCurrentPasswordErrorText] =
useState<string>('');
const [newPasswordErrorText, setNewPasswordErrorText] = useState<string>('');
const [isFocused, setIsFocused] = useState(false);
const alertBar = useAlertBar();
const account = useAccount();
const navigate = useNavigate();
const onFocusMetricsEvent = 'account-recovery-reset-password.engage';
const ftlMsgResolver = useFtlMsgResolver();
const { handleSubmit } = useForm<FormData>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
currentPassword: '',
newPassword: '',
},
});
const onFocus = () => {
if (!isFocused && onFocusMetricsEvent) {
logViewEvent('flow', onFocusMetricsEvent, {
entrypoint_variation: 'react',
});
setIsFocused(true);
}
};
const navigateToHome = useCallback(() => {
navigate('/settings', { replace: true });
}, [navigate]);
// TO-DO:
// * Set tooltip error message reflecting password requirements.
// const setErrorMessage = (errorText: string) => {
// setEmailErrorText(errorText);
// };
// * Set up metrics for all events
// - submitting the new password
// - focusing the password inputs if desired
// - remembering one's password
// - asking for a new link.
// * Hook up the functionality for sending a user a new verification link,
// instead of this dummy function.
const sendNewLinkEmail = () => {};
const onSubmit = () => {
try {
account.changePassword(currentPassword, newPassword);
navigateToHome();
} catch (e) {
const errorAccountRecoveryResetPassword = ftlMsgResolver.getMsg(
'reset-password-error-general',
'Sorry, there was a problem resetting your password'
);
alertBar.error(errorAccountRecoveryResetPassword);
}
};
const newPasswordLabel = ftlMsgResolver.getMsg(
'new-password-label',
'New password'
);
const currentPasswordLabel = ftlMsgResolver.getMsg(
'current-password-label',
'Current password'
);
return (
<>
{linkStatus === 'valid' && (
<>
<div className="mb-4">
<h1 className="card-header">
<FtlMsg id="create-new-password-header">
Create new password
</FtlMsg>
</h1>
</div>
<p className="text-sm mb-6">
<FtlMsg id="account-restored-success-message">
You have successfully restored your account using your account
recovery key. Create a new password to secure your data, and store
it in a safe location.
</FtlMsg>
</p>
<form
noValidate
className="flex flex-col gap-4"
onSubmit={handleSubmit(onSubmit)}
data-testid="account-recovery-reset-password-form"
>
<InputText
type="password"
label={newPasswordLabel}
onChange={(e) => {
setNewPassword(e.target.value);
// clear error tooltip if user types in the field
if (newPasswordErrorText) {
setNewPasswordErrorText('');
}
}}
onFocusCb={onFocusMetricsEvent ? onFocus : undefined}
autoFocus
errorText={newPasswordErrorText}
className="text-start"
anchorStart
autoComplete="off"
spellCheck={false}
prefixDataTestId="account-recovery-reset-password-new-password"
/>
<InputText
type="password"
label={currentPasswordLabel}
onChange={(e) => {
setCurrentPassword(e.target.value);
// clear error tooltip if user types in the field
if (currentPasswordErrorText) {
setCurrentPasswordErrorText('');
}
}}
onFocusCb={onFocusMetricsEvent ? onFocus : undefined}
errorText={currentPasswordErrorText}
className="text-start"
anchorStart
autoComplete="off"
spellCheck={false}
prefixDataTestId="account-recovery-reset-password-current-password"
/>
<FtlMsg id="confirm-account-recovery-key-button">
<button
data-testid="confirm-account-recovery-key-button"
type="submit"
className="cta-primary cta-xl"
>
Reset password
</button>
</FtlMsg>
</form>
{canGoBack && <LinkRememberPassword />}
</>
)}
{linkStatus === 'broken' && (
<>
<div className="mb-4">
<h1 className="card-header">
<FtlMsg id="password-link-damaged-header">
Reset password link damaged
</FtlMsg>
</h1>
</div>
<p className="text-sm mb-6">
<FtlMsg id="password-link-damaged-message">
The link you clicked was missing characters, and may have been
broken by your email client. Copy the address carefully, and try
again.
</FtlMsg>
</p>
</>
)}
{linkStatus === 'expired' && (
<>
<div className="mb-4">
<h1 className="card-header">
<FtlMsg id="password-link-expired-header">
Reset password link expired
</FtlMsg>
</h1>
</div>
<p className="text-sm mb-6">
<FtlMsg id="password-link-expired-message">
The link you clicked to reset your password is expired.
</FtlMsg>
</p>
<div className="text-sm mt-6">
<FtlMsg id="receive-new-link">
<button
onClick={sendNewLinkEmail}
className="link-blue text-sm"
id="remember-password"
>
Receive new link
</button>
</FtlMsg>
</div>
</>
)}
</>
);
};
export default AccountRecoveryResetPassword;