From 9873858162ee45cca31cca5dea672c1a2f72f509 Mon Sep 17 00:00:00 2001 From: Mill Date: Wed, 28 Dec 2022 12:13:36 -0800 Subject: [PATCH] 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 Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl Co-authored-by: Bryan Olsson Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl Co-authored-by: Bryan Olsson Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl Co-authored-by: Bryan Olsson Update packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl Co-authored-by: Bryan Olsson --- .../pages/AccountRecoveryResetPassword/en.ftl | 14 ++ .../index.stories.tsx | 38 +++ .../index.test.tsx | 81 ++++++ .../AccountRecoveryResetPassword/index.tsx | 233 ++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl create mode 100644 packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.stories.tsx create mode 100644 packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.test.tsx create mode 100644 packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.tsx diff --git a/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl new file mode 100644 index 0000000000..d305d0420e --- /dev/null +++ b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/en.ftl @@ -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. diff --git a/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.stories.tsx new file mode 100644 index 0000000000..b7a724fcd4 --- /dev/null +++ b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.stories.tsx @@ -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 = () => ( + + + + + + ); + 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', +}); diff --git a/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.test.tsx b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.test.tsx new file mode 100644 index 0000000000..b31b354f49 --- /dev/null +++ b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.test.tsx @@ -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(); + // 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(); + 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(); + 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( + + ); + expect( + screen.getByRole('link', { name: 'Remember your password? Sign in' }) + ).toBeInTheDocument(); + }); + + it('emits a metrics event on render', () => { + render(); + expect(usePageViewEvent).toHaveBeenCalledWith( + `account-recovery-reset-password`, + { + entrypoint_variation: 'react', + } + ); + }); +}); diff --git a/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.tsx b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.tsx new file mode 100644 index 0000000000..2e2f6d6318 --- /dev/null +++ b/packages/fxa-settings/src/pages/AccountRecoveryResetPassword/index.tsx @@ -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(''); + const [newPassword, setNewPassword] = useState(''); + const [currentPasswordErrorText, setCurrentPasswordErrorText] = + useState(''); + const [newPasswordErrorText, setNewPasswordErrorText] = useState(''); + 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({ + 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' && ( + <> +
+

+ + Create new password + +

+
+

+ + 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. + +

+
+ { + 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" + /> + { + 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" + /> + + + + + + + {canGoBack && } + + )} + {linkStatus === 'broken' && ( + <> +
+

+ + Reset password link damaged + +

+
+

+ + The link you clicked was missing characters, and may have been + broken by your email client. Copy the address carefully, and try + again. + +

+ + )} + {linkStatus === 'expired' && ( + <> +
+

+ + Reset password link expired + +

+
+

+ + The link you clicked to reset your password is expired. + +

+
+ + + +
+ + )} + + ); +}; + +export default AccountRecoveryResetPassword;