diff --git a/package.json b/package.json index 1baac3b9ad0..13d5e7a446c 100644 --- a/package.json +++ b/package.json @@ -298,8 +298,6 @@ "packages/*" ], "resolutions": { - "@react-pdf/layout": "3.9.1", - "@react-pdf/textkit": "4.3.0", "@types/react": "18.2.14", "asn1.js": ">=5.4.1", "axios": "^1.13.4", diff --git a/packages/functional-tests/package.json b/packages/functional-tests/package.json index 652acb42180..93ca51a4b7f 100644 --- a/packages/functional-tests/package.json +++ b/packages/functional-tests/package.json @@ -15,7 +15,6 @@ "devDependencies": { "@playwright/test": "1.61.1", "@types/eslint": "^8", - "@types/pdf-parse": "^1.1.4", "@types/upng-js": "^2", "eslint": "^8.56.0", "eslint-plugin-playwright": "^0.22.2", @@ -23,7 +22,6 @@ "fxa-content-server": "workspace:*", "fxa-settings": "workspace:*", "jsqr": "^1.4.0", - "pdf-parse": "^1.1.1", "typescript": "5.5.3", "upng-js": "^2.1.0" }, diff --git a/packages/functional-tests/tests/resetPassword/oauthResetPasswordRecoveryKey.spec.ts b/packages/functional-tests/tests/resetPassword/oauthResetPasswordRecoveryKey.spec.ts index c97b330a423..dc7b1b37589 100644 --- a/packages/functional-tests/tests/resetPassword/oauthResetPasswordRecoveryKey.spec.ts +++ b/packages/functional-tests/tests/resetPassword/oauthResetPasswordRecoveryKey.spec.ts @@ -55,7 +55,7 @@ test.describe('severity-1 #smoke', () => { const keyDownload = await resetPassword.downloadRecoveryKey(); const filename = keyDownload.suggestedFilename(); expect(filename).toMatch( - new RegExp(`Mozilla-Recovery-Key_[0-9-]{10}_${credentials.email}.pdf`) + new RegExp(`Mozilla-Recovery-Key_[0-9-]{10}_${credentials.email}.txt`) ); expect(filename.length).toBeGreaterThan(0); diff --git a/packages/functional-tests/tests/resetPassword/resetPasswordRecoveryKey.spec.ts b/packages/functional-tests/tests/resetPassword/resetPasswordRecoveryKey.spec.ts index 651bc32adbb..170aac8410b 100644 --- a/packages/functional-tests/tests/resetPassword/resetPasswordRecoveryKey.spec.ts +++ b/packages/functional-tests/tests/resetPassword/resetPasswordRecoveryKey.spec.ts @@ -75,7 +75,7 @@ test.describe('severity-1 #smoke', () => { const keyDownload = await resetPassword.downloadRecoveryKey(); const filename = keyDownload.suggestedFilename(); expect(filename).toMatch( - new RegExp(`Mozilla-Recovery-Key_[0-9-]{10}_${credentials.email}.pdf`) + new RegExp(`Mozilla-Recovery-Key_[0-9-]{10}_${credentials.email}.txt`) ); expect(filename.length).toBeGreaterThan(0); diff --git a/packages/functional-tests/tests/settings/recoveryKey.spec.ts b/packages/functional-tests/tests/settings/recoveryKey.spec.ts index 3272ca85ff0..ca6aa1ed6f5 100644 --- a/packages/functional-tests/tests/settings/recoveryKey.spec.ts +++ b/packages/functional-tests/tests/settings/recoveryKey.spec.ts @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import fs from 'fs'; -import pdfParse from 'pdf-parse'; import { Page, expect, test } from '../../lib/fixtures/standard'; import { BaseTarget, Credentials } from '../../lib/targets/base'; import { TestAccountTracker } from '../../lib/testAccountTracker'; @@ -45,7 +44,7 @@ test.describe('severity-1 #smoke', () => { expect(clipboard).toEqual(newKey); }); - test('can download recovery key as PDF', async ({ + test('can download recovery key as a text file', async ({ target, pages: { page, recoveryKey, settings, signin }, testAccountTracker, @@ -80,7 +79,7 @@ test.describe('severity-1 #smoke', () => { const filename = dl.suggestedFilename(); expect(filename.length).toBeLessThanOrEqual(75); expect(filename).toBe( - `Mozilla-Recovery-Key_${date}_${credentials.email}.pdf` + `Mozilla-Recovery-Key_${date}_${credentials.email}.txt` ); // Test uses try/finally to ensure the downloaded file is deleted after tests @@ -90,15 +89,8 @@ test.describe('severity-1 #smoke', () => { await dl.saveAs(filename); expect(fs.existsSync(filename)).toBeTruthy(); - const getPDF = async (file: fs.PathOrFileDescriptor) => { - const readFileSync = fs.readFileSync(file); - const pdfExtract = await pdfParse(readFileSync); - // Verify downloaded file contains key - expect(pdfExtract.text).toContain(newKey); - // Verify the PDF file contains only one page - expect(pdfExtract.numpages).toEqual(1); - }; - getPDF(filename); + const contents = await fs.promises.readFile(filename, 'utf8'); + expect(contents).toBe(newKey); } finally { // Delete the downloaded file await fs.promises.unlink(filename); diff --git a/packages/fxa-settings/package.json b/packages/fxa-settings/package.json index a09924cd576..b45b3ae8847 100644 --- a/packages/fxa-settings/package.json +++ b/packages/fxa-settings/package.json @@ -131,7 +131,6 @@ "@emotion/styled": "^11.13.0", "@material-ui/core": "v5.0.0-alpha.24", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@react-pdf/renderer": "3.2.1", "@svgr/webpack": "^8.1.0", "@types/material-ui": "^0.21.8", "babel-jest": "^29.7.0", @@ -153,7 +152,6 @@ "eslint": "^8.3.0", "eslint-webpack-plugin": "^4.2.0", "file-loader": "^6.2.0", - "file-saver": "^2.0.5", "fs-extra": "^11.2.0", "fxa-auth-client": "workspace:*", "fxa-pairing-channel": "1.0.2", @@ -201,7 +199,6 @@ "@testing-library/jest-dom": "^6.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.6.0", - "@types/file-saver": "^2.0.5", "@types/jest": "26.0.23", "@types/node": "^22.13.5", "@types/prop-types": "^15", diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/en.ftl b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/en.ftl new file mode 100644 index 00000000000..bbf68e42a60 --- /dev/null +++ b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/en.ftl @@ -0,0 +1,11 @@ +## ButtonDownloadRecoveryKey +## Clicking on this button downloads a plain text file that contains the user's account recovery key +## The account recovery key can be used to recover data when users forget their account password + +# Button to download the account recovery key as a plain text file and navigate to the next step +# The next (and final) step is an optional prompt to save a storage hint +recovery-key-download-button-v4 = Download and continue + +# Error message shown in a banner if the account recovery key download failed. +# The id keeps "pdf" from when this was a PDF, to preserve existing translations. +recovery-key-pdf-download-error = Sorry, there was a problem downloading your account recovery key. diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.stories.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.stories.tsx similarity index 75% rename from packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.stories.tsx rename to packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.stories.tsx index 6057d478cfe..63595ce2d45 100644 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.stories.tsx +++ b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.stories.tsx @@ -4,25 +4,22 @@ import { Meta } from '@storybook/react'; import AppLayout from '../AppLayout'; -import ButtonDownloadRecoveryKeyPDF from '.'; +import ButtonDownloadRecoveryKey from '.'; import { withLocalization } from 'fxa-react/lib/storybooks'; import { MOCK_EMAIL } from '../../pages/mocks'; export default { - title: 'Components/ButtonDownloadRecoveryKeyPDF', - component: ButtonDownloadRecoveryKeyPDF, + title: 'Components/ButtonDownloadRecoveryKey', + component: ButtonDownloadRecoveryKey, decorators: [withLocalization], } as Meta; const recoveryKeyValue = 'ABCD 1234 ABCD 1234 ABCD 1234 ABCD O0O0'; -const viewName = 'settings.recovery-key'; const storyWithAccount = (email = MOCK_EMAIL) => { const story = () => ( - + ); return story; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.test.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.test.tsx new file mode 100644 index 00000000000..c94b486e303 --- /dev/null +++ b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.test.tsx @@ -0,0 +1,166 @@ +/* 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 { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; +import { ButtonDownloadRecoveryKey, getFilename } from '.'; +import { downloadTextFile } from '../../lib/download'; +import * as Sentry from '@sentry/browser'; +import { MOCK_EMAIL } from '../../pages/mocks'; + +jest.mock('../../lib/download', () => ({ + downloadTextFile: jest.fn(), +})); + +jest.mock('@sentry/browser', () => ({ + captureException: jest.fn(), +})); + +const MOCK_RECOVERY_KEY = 'WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ'; + +const renderButton = (navigateForward?: () => void) => + renderWithLocalizationProvider( + + ); + +const getDownloadButton = () => + screen.getByRole('button', { name: 'Download and continue' }); + +const renderAndClickDownload = async (navigateForward?: () => void) => { + const user = userEvent.setup(); + renderButton(navigateForward); + await user.click(getDownloadButton()); +}; + +const mockDownloadFailure = () => + jest.mocked(downloadTextFile).mockImplementationOnce(() => { + throw new Error('download failed'); + }); + +beforeEach(() => { + jest.clearAllMocks(); + // clearAllMocks leaves implementations queued; reset so an unconsumed + // failure cannot leak into the next test. + jest.mocked(downloadTextFile).mockReset(); +}); + +describe('ButtonDownloadRecoveryKey', () => { + it('renders the download button', () => { + renderButton(); + + expect(getDownloadButton()).toBeInTheDocument(); + }); + + it('tags the button for Glean click metrics', () => { + renderButton(); + + expect(getDownloadButton()).toHaveAttribute( + 'data-glean-id', + 'account_pref_recovery_key_download' + ); + }); + + it('downloads the recovery key as a text file when clicked', async () => { + await renderAndClickDownload(); + + const today = new Date().toISOString().split('T')[0]; + expect(downloadTextFile).toHaveBeenCalledWith( + MOCK_RECOVERY_KEY, + `Mozilla-Recovery-Key_${today}_${MOCK_EMAIL}.txt` + ); + }); + + it('shows an inline error banner when the download throws', async () => { + mockDownloadFailure(); + + await renderAndClickDownload(); + + expect(screen.getByRole('alert')).toHaveTextContent( + 'Sorry, there was a problem downloading your account recovery key.' + ); + }); + + it('reports the failure to Sentry when the download throws', async () => { + mockDownloadFailure(); + + await renderAndClickDownload(); + + expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error)); + }); + + it('does not navigate forward when the download throws', async () => { + mockDownloadFailure(); + const navigateForward = jest.fn(); + + await renderAndClickDownload(navigateForward); + + expect(navigateForward).not.toHaveBeenCalled(); + }); + + it('navigates forward when the download succeeds', async () => { + const navigateForward = jest.fn(); + + await renderAndClickDownload(navigateForward); + + expect(navigateForward).toHaveBeenCalledTimes(1); + }); + + it('clears a previous error once a retry succeeds', async () => { + const user = userEvent.setup(); + mockDownloadFailure(); + renderButton(); + + await user.click(getDownloadButton()); + expect(screen.getByRole('alert')).toBeInTheDocument(); + + await user.click(getDownloadButton()); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); + + it('renders no error banner before any download is attempted', () => { + renderButton(); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); +}); + +describe('getFilename', () => { + const MOCK_DATE = '2026-08-10'; + + beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date(`${MOCK_DATE}T12:00:00.000Z`)); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('builds the filename from the prefix, current date and email', () => { + expect(getFilename(MOCK_EMAIL)).toBe( + `Mozilla-Recovery-Key_${MOCK_DATE}_${MOCK_EMAIL}.txt` + ); + }); + + it('replaces characters that are unsafe in a filename', () => { + expect(getFilename('a/b@example.com')).toBe( + `Mozilla-Recovery-Key_${MOCK_DATE}_a_b@example.com.txt` + ); + }); + + it('truncates a very long email but keeps the prefix, date and extension', () => { + const longEmail = + 'supercalifragilisticexpialidocious@marypoppins.superfan.conference.com'; + const filename = getFilename(longEmail); + + expect(filename).toBe( + `Mozilla-Recovery-Key_${MOCK_DATE}_supercalifragilisticexpialidocious@mary.txt` + ); + }); +}); diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.tsx new file mode 100644 index 00000000000..7d8468df936 --- /dev/null +++ b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKey/index.tsx @@ -0,0 +1,90 @@ +/* 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 { useState } from 'react'; +import * as Sentry from '@sentry/browser'; +import { RelierCmsInfo, useFtlMsgResolver } from '../../models'; +import { FtlMsg } from 'fxa-react/lib/utils'; +import { downloadTextFile } from '../../lib/download'; +import Banner from '../Banner'; +import CmsButtonWithFallback from '../CmsButtonWithFallback'; + +interface ButtonDownloadRecoveryKeyProps { + navigateForward?: () => void; + recoveryKeyValue: string; + email: string; + cmsInfo?: RelierCmsInfo; +} + +const FILENAME_EXTENSION = '.txt'; +// Arbitrary, but well under the 260 character Windows path limit. +const MAX_FILENAME_LENGTH = 75; + +export const getFilename = (email: string) => { + const date = new Date().toISOString().split('T')[0]; + // FxA permits "/" in an email local part, so don't rely on the browser + // alone to reduce the download hint to a safe leaf name. + const safeEmail = email.replace(/[^\w.@+-]/g, '_'); + const stem = `Mozilla-Recovery-Key_${date}_${safeEmail}`.slice( + 0, + MAX_FILENAME_LENGTH - FILENAME_EXTENSION.length + ); + + return stem + FILENAME_EXTENSION; +}; + +export const ButtonDownloadRecoveryKey = ({ + navigateForward, + recoveryKeyValue, + email, + cmsInfo, +}: ButtonDownloadRecoveryKeyProps) => { + const ftlMsgResolver = useFtlMsgResolver(); + const [localizedError, setLocalizedError] = useState(''); + + const handleDownloadClick = () => { + try { + downloadTextFile(recoveryKeyValue, getFilename(email)); + } catch (error) { + // The key is the only way back into encrypted Sync data, so a systemic + // failure here needs to be visible in aggregate, not just to the user. + Sentry.captureException(error); + // Not useAlertBar: is only mounted by SettingsLayout, and + // two of the three screens using this button render under AppLayout. + setLocalizedError( + ftlMsgResolver.getMsg( + 'recovery-key-pdf-download-error', + 'Sorry, there was a problem downloading your account recovery key.' + ) + ); + return; + } + + setLocalizedError(''); + navigateForward?.(); + }; + + return ( + <> + {localizedError && ( + + )} + + + Download and continue + + + + ); +}; + +export default ButtonDownloadRecoveryKey; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/RecoveryKeyPDF.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/RecoveryKeyPDF.tsx deleted file mode 100644 index c27b88e285a..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/RecoveryKeyPDF.tsx +++ /dev/null @@ -1,262 +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 { - Document, - Font, - Link, - Page, - StyleSheet, - Text, - View, -} from '@react-pdf/renderer'; -import { LocalizedRecoveryKeyPdfContent } from '.'; -import { - FolderSvg, - GlobeSvg, - KeySvg, - LockSvg, - PrinterSvg, - RecoveryKeySvg, - MozillaLogoSvg, -} from './VectorImagesForPdf'; -import notoSansMonoSemiBold from './fonts/NotoSansMono-SemiBold.ttf'; -import { FontData } from './requiredFont'; - -interface RecoveryKeyPDFProps { - recoveryKeyValue: string; - localizedText: LocalizedRecoveryKeyPdfContent; - requiredFont: FontData; - email: string; -} - -export const RecoveryKeyPDF = ({ - recoveryKeyValue, - requiredFont, - email, - localizedText, -}: RecoveryKeyPDFProps) => { - const { family, sources, direction, breakwords } = requiredFont; - - Font.register({ - family, - fonts: sources, - }); - - Font.register({ - family: 'Noto Sans Mono', - fonts: [ - { - src: notoSansMonoSemiBold, - fontStyle: 'normal', - fontWeight: 'semibold', - }, - ], - }); - - const splitText = (text: string) => { - return text.split(' '); - }; - - // By default, react-pdf inserts hyphens wherever a word overflows its containers, - // without regard to language rules. - // For languages such as Chinese and Japanese that do not contain spaces between words, - // text should break at the end of the line without adding a hyphen. - // Code snippet here was taken from: - // https://github.com/diegomura/react-pdf/issues/692#issuecomment-631819389 - breakwords - ? Font.registerHyphenationCallback((word: string) => { - if (word.length === 1) { - return [word]; - } else { - return Array.from(word).reduce((acc: (string | '')[], char) => { - acc = acc.concat([char, '']); - return acc; - }, []); - } - }) - : // For other languages that contain spaces between words, text that would overflow its container - // should break at the preceeding space between words, without hyphenation. - // See documentation about disabling default hyphenation: - // https://react-pdf.org/fonts#registerhyphenationcallback - Font.registerHyphenationCallback((word: string) => [word]); - - const styles = StyleSheet.create({ - page: { - flexDirection: 'column', - backgroundColor: '#ffffff', - paddingVertical: '2cm', - paddingHorizontal: '2cm', - fontFamily: family, - textAlign: direction === 'ltr' ? 'left' : 'right', - }, - purpleSection: { - flexDirection: 'column', - marginTop: '56px', - paddingHorizontal: '28px', - paddingBottom: '28px', - borderRadius: 10, - backgroundColor: '#e3dff5', - textAlign: 'center', - }, - whiteSection: { - padding: '16px', - borderRadius: 5, - backgroundColor: '#ffffff', - fontSize: '12px', - gap: '8px', - alignItems: 'flex-start', - }, - heading: { - fontSize: '18px', - fontWeight: 'bold', - marginTop: '10px', - marginBottom: '4px', - }, - subheading: { - fontSize: '12px', - fontWeight: 'semibold', - marginTop: '28px', - marginBottom: '12px', - }, - listItem: { - flexDirection: direction === 'ltr' ? 'row' : 'row-reverse', - gap: '4px', - alignItems: 'center', - flexBasis: '50%', - paddingVertical: '4px', - }, - listItemText: { - fontSize: '10px', - fontWeight: 'normal', - }, - }); - - return ( - - - - - - - - {localizedText.heading} - {/* TODO in FXA-8313: Verify formatting of RTL dates - bidirectionality markers are not respected - and numeric values are incorrectly reversed (e.g., 3202 instead of 2023) */} - - {localizedText.dateGenerated} - - - {email} - - - - - - {localizedText.keyLegend} - - - - {recoveryKeyValue} - - - - - - {direction === 'ltr' ? ( - - {localizedText.instructions} - - ) : ( - // this is a hack to support RTL text that flows over more than one line - // react-pdf does not (yet) support RTL out-of-the-box, and long paragraphs of RTL - // text were flowing from the bottom up. This splits the text into an array of words - // that can be reflowed from top-right to bottom-left. - // TODO in FXA-8313: Verify punctuation incl. parentheses. They may be inverted and misplaced. - splitText(localizedText.instructions).map((word, i) => { - return ( - - {word} - - ); - }) - )} - - {localizedText.storageHeading} - - - - - {localizedText.storageIdeaFolder} - - - - - - {localizedText.storageIdeaCloud} - - - - - - {localizedText.storageIdeaPrint} - - - - - - {localizedText.storageIdeaPwdManager} - - - - - {localizedText.findOutMoreHeading} - - - https://mzl.la/3bNrM1I - - - - - ); -}; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/VectorImagesForPdf.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/VectorImagesForPdf.tsx deleted file mode 100644 index fc2c2cee9bc..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/VectorImagesForPdf.tsx +++ /dev/null @@ -1,214 +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 { Svg, Path, Rect, Circle } from '@react-pdf/renderer'; - -/* - To include SVG in PDFs with react-pdf/renderer, SVGs must to be added directly in the format explained in docs. - This requires modifying the source code of SVGs to match the expected format. - - Note that support for advanced SVG features (e.g., gradients and shadows) is limited/null, - and it's generally preferable to use simplifed versions of graphics without these complexities. - - If we need to we can explore turning the SVG into base64 PNG programmatically, - with a last resort to just use a PNG version of the image (discouraged). - - See docs: https://react-pdf.org/svg -*/ - -export const RecoveryKeySvg = () => { - return ( - - - - - - - {/* Circle under the checkmark */} - - - {/* Checkmark */} - - - {/* Star 1 (4 flat color quadrants) */} - - - - - - {/* Star 2 (4 flat color quadrants) */} - - - - - - {/* Star 3 (4 flat color quadrants) */} - - - - - - ); -}; - -export const FolderSvg = () => { - return ( - - - - ); -}; - -export const GlobeSvg = () => { - return ( - - - - ); -}; - -export const LockSvg = () => { - return ( - - - - - ); -}; - -export const PrinterSvg = () => { - return ( - - - - ); -}; - -export const KeySvg = (props: { direction: 'rtl' | 'ltr' }) => { - const { direction } = props; - return ( - - - - ); -}; - -export const MozillaLogoSvg = () => { - return ( - - - - - ); -}; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/__snapshots__/index.test.tsx.snap b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 044683fe856..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ButtonDownloadRecoveryKeyPDF renders button as expected: without CMS 1`] = ` -
- - - -
-`; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/en.ftl b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/en.ftl deleted file mode 100644 index 3bb9c477cbf..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/en.ftl +++ /dev/null @@ -1,33 +0,0 @@ -## ButtonDownloadRecoveryKeyPDF -## Clicking on this button downloads a PDF file that contains the user's account recovery key -## The account recovery key can be used to recover data when users forget their account password - -# Button to download the account recovery key as a PDF file and navigate to the next step -# The next (and final) step is an optional prompt to save a storage hint -# .title will displayed as a tooltip on the button -recovery-key-download-button-v3 = Download and continue - .title = Download and continue - -recovery-key-pdf-heading = Account Recovery Key - -# Date when the account recovery key was created and this file was downloaded -# { $date }: formatted date with 'medium' dateStyle format (e.g., for 'en': Jul 31, 2023) -recovery-key-pdf-download-date = Generated: { $date } - -# Shown directly above recovery key value and preceeded by a key icon -recovery-key-pdf-key-legend = Account Recovery Key - -# Instructions in the text file to prompt the user to keep this information in a secure, easy to remember location. -# Password resets without this account recovery key can result in data loss. -# "key" here refers to "account recovery key" -recovery-key-pdf-instructions = This key allows you to recover your encrypted browser data (including passwords, bookmarks, and history) if you forget your password. Store it in a place you’ll remember. - -# This heading is shown above a list of options for storing the account recovery key -# "key" here refers to "account recovery key" -recovery-key-pdf-storage-ideas-heading = Places to store your key - -# Followed by a link (https://mzl.la/3bNrM1I) to get more information and support -recovery-key-pdf-support = Learn more about your account recovery key - -# Error message displayed in an alert bar if the PDF download failed. -recovery-key-pdf-download-error = Sorry, there was a problem downloading your account recovery key. diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Bold.ttf deleted file mode 100644 index 0d19068399c..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Regular.ttf deleted file mode 100644 index 7552fbe806d..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-SemiBold.ttf deleted file mode 100644 index b460754c54b..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSans-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Bold.ttf deleted file mode 100644 index cc088702dbc..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Regular.ttf deleted file mode 100644 index 49163cc37e2..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-SemiBold.ttf deleted file mode 100644 index c81606b8be9..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansGeorgian-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Bold.ttf deleted file mode 100644 index 04e5eb0f56a..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Regular.ttf deleted file mode 100644 index ca7a0fdb7f9..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-SemiBold.ttf deleted file mode 100644 index 92c9637e367..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansHebrew-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Bold.ttf deleted file mode 100644 index 384f8ebb89a..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Regular.ttf deleted file mode 100644 index 1583096a2d7..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-SemiBold.ttf deleted file mode 100644 index cc72ab304d9..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansJP-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Bold.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Bold.otf deleted file mode 100644 index be388bf5f96..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Bold.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Medium.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Medium.otf deleted file mode 100644 index 5ddbbc03803..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Medium.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Regular.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Regular.otf deleted file mode 100644 index 7c5c2fae3f7..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansKR-Regular.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansMono-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansMono-SemiBold.ttf deleted file mode 100644 index 5b899b45f4e..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansMono-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Bold.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Bold.otf deleted file mode 100644 index 172eb674127..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Bold.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Medium.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Medium.otf deleted file mode 100644 index 0a5bd9e5fe6..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Medium.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Regular.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Regular.otf deleted file mode 100644 index d350ffa79e3..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansSC-Regular.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Bold.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Bold.otf deleted file mode 100644 index 6f0c8be4bb9..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Bold.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Medium.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Medium.otf deleted file mode 100644 index a0811eb27d9..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Medium.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Regular.otf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Regular.otf deleted file mode 100644 index 29724773df3..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansTC-Regular.otf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Bold.ttf deleted file mode 100644 index d081be4b28e..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Regular.ttf deleted file mode 100644 index a6c4d1c9438..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-SemiBold.ttf deleted file mode 100644 index 26f6a6fa251..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSansThai-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Bold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Bold.ttf deleted file mode 100644 index 354bd731ccb..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Bold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Regular.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Regular.ttf deleted file mode 100644 index 37e0ed1424c..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-Regular.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-SemiBold.ttf b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-SemiBold.ttf deleted file mode 100644 index 37ac49c1a83..00000000000 Binary files a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/NotoSerifGurmukhi-SemiBold.ttf and /dev/null differ diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/README.md b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/README.md deleted file mode 100644 index 6535a4fd9a2..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/README.md +++ /dev/null @@ -1,29 +0,0 @@ -## Why these fonts? - -These fonts are used to generate recovery key PDFs client-side with @react-pdf in fxa-settings. - -## Why Noto? - -Noto is a collection of fonts with multiple weights and widths in sans, serif, mono, and other styles. This family of fonts supports more than 1,000 languages and over 150 writing systems. - -Read more: https://fonts.google.com/noto -These fonts are licensed under the [Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL). - -For most of the locales supported by Mozilla accounts (see `supportedLanguages.json`), we can default to the standard Noto Sans, with a few exceptions that require specific fonts: - -- Chinese: Noto Sans SC -- Georgian: Noto Sans Georgian -- Hebrew: Noto Sans Hebrew -- Japanese: Noto Sans JP -- Korean: Noto Sans KR -- Punjabi: Noto Sans Gurmukhi -- Thai: Noto Sans Thai - -## Adding new fonts/locales for PDF localization - -New fonts (e.g., for additional languages or font weights/styles) must be registered for use by react-pdf. - -1. Add required font files to `assets/fonts` -2. Verify if new type declarations need to be added for typescript to recognize the font files as valid import modules. Currently, `*.ttf` and `*.otf` are included - if the fonts have another extension, add it to the `fonts.d.ts`. -3. Import the font(s) in `ButtonDownloadRecoveryPDF/index.tsx` -4. Add a new case to the switch in `ButtonDownloadRecoveryPDF/GetRequiredFont.tsx` for the new locale (see exisiting examples in the file and consult react-pdf docs: https://react-pdf.org/fonts#register). diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/fonts.d.ts b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/fonts.d.ts deleted file mode 100644 index d04fb10a72f..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/fonts/fonts.d.ts +++ /dev/null @@ -1,6 +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/. */ - -declare module '*.ttf'; -declare module '*.otf'; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.test.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.test.tsx deleted file mode 100644 index 42cb85e4a5d..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.test.tsx +++ /dev/null @@ -1,110 +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 { fireEvent, screen } from '@testing-library/react'; -import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; -import { ButtonDownloadRecoveryKeyPDF, getFilename } from '.'; -import { logViewEvent } from '../../lib/metrics'; -import { TextEncoder } from 'util'; -import { MOCK_EMAIL } from '../../pages/mocks'; -import { MemoryRouter } from 'react-router'; - -Object.assign(global, { TextEncoder }); - -const recoveryKeyValue = 'WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ WXYZ'; -const viewName = 'settings.account-recovery'; - -jest.mock('../../lib/metrics', () => ({ - logViewEvent: jest.fn(), -})); - -jest.mock('@react-pdf/renderer', () => { - return { - pdf: jest.fn().mockResolvedValue({ - toBlob: jest.fn().mockResolvedValue(new globalThis.Blob()), - updateContainer: jest.fn(), - }), - }; -}); - -beforeAll(() => { - window.URL.createObjectURL = jest.fn(); -}); - -describe('ButtonDownloadRecoveryKeyPDF', () => { - it('renders button as expected', () => { - const { container } = renderWithLocalizationProvider( - - - - ); - screen.getByText('Download and continue'); - expect(container).toMatchSnapshot('without CMS'); - }); - - // Content of downloaded file is tested in Playwright tests - // including validating that the expected key is included and matches the key in the DataBlock - it('emits a metrics event when the link is clicked', () => { - renderWithLocalizationProvider( - - - - ); - const downloadButton = screen.getByText('Download and continue'); - fireEvent.click(downloadButton); - - expect(logViewEvent).toHaveBeenCalledWith( - `flow.${viewName}`, - 'recovery-key.download-option' - ); - }); -}); - -describe('getFilename function', () => { - it('sets the filename as expected with a reasonably-sized email', () => { - const filename = getFilename(MOCK_EMAIL); - - // Test the date formatting - const mockDateObject = '2023-05-10T17:00:40.722Z'; - const mockDateFunctionOutput = new Date(mockDateObject) - .toISOString() - .split('T')[0]; - const mockExpectedDate = '2023-05-10'; - - expect(mockDateFunctionOutput).toEqual(mockExpectedDate); - - // Filename should be created from current date - const date = new Date().toISOString().split('T')[0]; - - expect(filename).toContain( - `Mozilla-Recovery-Key_${date}_${MOCK_EMAIL}.pdf` - ); - expect(filename.length).toBeLessThanOrEqual(75); - }); - - it('sets the filename with a truncated email as expected when the email is very long', () => { - const longEmail = - 'supercalifragilisticexpialidocious@marypoppins.superfan.conference.com'; - const filename = getFilename(longEmail); - - // Filename should be created from current date - const date = new Date().toISOString().split('T')[0]; - - const fullFilename = `Mozilla-Recovery-Key_${date}_${longEmail}.pdf`; - // Full filename would be longer than 75 char if not truncated - expect(fullFilename.length).toBeGreaterThan(75); - - // Actual filename is truncated - expect(filename.length).toBeLessThanOrEqual(75); - // filename still contains full prefix and date - expect(filename).toContain('Mozilla-Recovery-Key'); - expect(filename).toContain(date); - }); -}); diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.tsx deleted file mode 100644 index e20c629925c..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/index.tsx +++ /dev/null @@ -1,165 +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 { RelierCmsInfo, useAlertBar, useFtlMsgResolver } from '../../models'; -import { pdf } from '@react-pdf/renderer'; -import { saveAs } from 'file-saver'; -import { RecoveryKeyPDF } from '../ButtonDownloadRecoveryKeyPDF/RecoveryKeyPDF'; -import { - FtlMsg, - LocalizedDateOptions, - getLocalizedDate, -} from 'fxa-react/lib/utils'; -import { logViewEvent } from '../../lib/metrics'; -import { FontData, getRequiredFont } from './requiredFont'; -import { determineLocale } from '@fxa/shared/l10n'; -import CmsButtonWithFallback from '../CmsButtonWithFallback'; - -export interface LocalizedRecoveryKeyPdfContent { - heading: string; - dateGenerated: string; - keyLegend: string; - instructions: string; - storageHeading: string; - storageIdeaFolder: string; - storageIdeaCloud: string; - storageIdeaPrint: string; - storageIdeaPwdManager: string; - findOutMoreHeading: string; -} - -interface ButtonDownloadRecoveryKeyPDFProps { - navigateForward?: () => void; - recoveryKeyValue: string; - viewName: string; - email: string; - cmsInfo?: RelierCmsInfo; -} - -export const getFilename = (email: string) => { - const date = new Date().toISOString().split('T')[0]; - const prefix = 'Mozilla-Recovery-Key'; - // Windows has a max directory length of 260 characters (including path) - // filename should be kept much shorter (maxLength is arbitrary). - let filename = `${prefix}_${date}_${email}`.substring(0, 71) + `.pdf`; - - return filename; -}; - -export const ButtonDownloadRecoveryKeyPDF = ({ - navigateForward, - recoveryKeyValue, - viewName, - email, - cmsInfo, -}: ButtonDownloadRecoveryKeyPDFProps) => { - const keyCreated = Date.now(); - const currentLanguage = determineLocale( - window.navigator.languages.join(', ') - ); - const ftlMsgResolver = useFtlMsgResolver(); - - const alertBar = useAlertBar(); - - const keyDateFallback = Intl.DateTimeFormat('default', { - dateStyle: 'medium', - }).format(new Date(keyCreated)); - - const keyDateFluent = getLocalizedDate( - keyCreated, - LocalizedDateOptions.MediumDateStyle - ); - - const localizedText: LocalizedRecoveryKeyPdfContent = { - heading: ftlMsgResolver.getMsg( - 'recovery-key-pdf-heading', - 'Account Recovery Key' - ), - dateGenerated: ftlMsgResolver.getMsg( - 'recovery-key-pdf-download-date', - `Generated: ${keyDateFallback}`, - { date: keyDateFluent } - ), - keyLegend: ftlMsgResolver.getMsg( - 'recovery-key-pdf-key-legend', - 'Account Recovery Key' - ), - instructions: ftlMsgResolver.getMsg( - 'recovery-key-pdf-instructions', - 'This key allows you to recover your encrypted browser data (including passwords, bookmarks, and history) if you forget your password. Store it in a place you’ll remember. ' - ), - storageHeading: ftlMsgResolver.getMsg( - 'recovery-key-pdf-storage-ideas-heading', - 'Places to store your key' - ), - storageIdeaFolder: ftlMsgResolver.getMsg( - 'flow-recovery-key-download-storage-ideas-folder-v2', - 'Folder on secure device' - ), - storageIdeaCloud: ftlMsgResolver.getMsg( - 'flow-recovery-key-download-storage-ideas-cloud', - 'Trusted cloud storage' - ), - storageIdeaPrint: ftlMsgResolver.getMsg( - 'flow-recovery-key-download-storage-ideas-print-v2', - 'Printed physical copy' - ), - storageIdeaPwdManager: ftlMsgResolver.getMsg( - 'flow-recovery-key-download-storage-ideas-pwd-manager', - 'Password manager' - ), - findOutMoreHeading: ftlMsgResolver.getMsg( - 'recovery-key-pdf-support', - 'Learn more about your account recovery key' - ), - }; - - const requiredFont: FontData = getRequiredFont(currentLanguage); - - // File download test coverage is provided by Playwright functional test - const downloadFile = async () => { - const doc = ( - - ); - - try { - const asPdf = pdf(); - asPdf.updateContainer(doc); - const blob = await asPdf.toBlob(); - const filename = getFilename(email); - saveAs(blob, filename); - logViewEvent(`flow.${viewName}`, `recovery-key.download-success`); - } catch (e) { - logViewEvent(`flow.${viewName}`, `recovery-key.download-failed`); - alertBar.error( - ftlMsgResolver.getMsg( - 'recovery-key-pdf-download-error', - 'Sorry, there was a problem downloading your account recovery key.' - ) - ); - } - }; - - return ( - <> - - { - logViewEvent(`flow.${viewName}`, `recovery-key.download-option`); - await downloadFile(); - navigateForward && navigateForward(); - }} - buttonColor={cmsInfo?.shared.buttonColor} - > - Download and continue - - - - ); -}; - -export default ButtonDownloadRecoveryKeyPDF; diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.test.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.test.tsx deleted file mode 100644 index e463205fe77..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.test.tsx +++ /dev/null @@ -1,70 +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 { getRequiredFont } from './requiredFont'; - -describe('getRequiredFont', () => { - it('returns the default font when the languages is unknown or not recognized', async () => { - const requiredFont = getRequiredFont('unknown'); - expect(requiredFont.family).toEqual('Noto Sans'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - it('returns the expected font data when the language is Chinese (mainland)', async () => { - const requiredFont = getRequiredFont('zh-cn'); - expect(requiredFont.family).toEqual('Noto Sans SC'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - it('returns the expected font data when the language is Chinese (Taiwan)', async () => { - const requiredFont = getRequiredFont('zh-tw'); - expect(requiredFont.family).toEqual('Noto Sans TC'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - it('returns the expected font data when the language is Georgian', async () => { - const requiredFont = getRequiredFont('zh-tw'); - expect(requiredFont.family).toEqual('Noto Sans TC'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - it('returns the expected font data when the language is Hebrew', async () => { - let requiredFont = getRequiredFont('he'); - expect(requiredFont.family).toEqual('Noto Sans Hebrew'); - expect(requiredFont.direction).toEqual('rtl'); - }); - - it('returns the expected font data when the language is Japanese', async () => { - let requiredFont = getRequiredFont('ja'); - expect(requiredFont.family).toEqual('Noto Sans JP'); - expect(requiredFont.direction).toEqual('ltr'); - - // can match if lowercase or uppercase - requiredFont = getRequiredFont('ja-jp'); - expect(requiredFont.family).toEqual('Noto Sans JP'); - - requiredFont = getRequiredFont('ja-JP'); - expect(requiredFont.family).toEqual('Noto Sans JP'); - }); - - it('returns the expected font data when the language is Korean', async () => { - const requiredFont = getRequiredFont('ko'); - expect(requiredFont.family).toEqual('Noto Sans KR'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - // Punjabi (India) uses Gurmukhi script and is written left-to-right - // Punjabi (Pakistan) - pa-PK - is currently unsupported and uses Shahmukhi script and is written right-to-left - it('returns the expected font data when the language is Punjabi', async () => { - const requiredFont = getRequiredFont('pa'); - expect(requiredFont.family).toEqual('Noto Serif Gurmukhi'); - expect(requiredFont.direction).toEqual('ltr'); - }); - - it('returns the expected font data when the language is Thai', async () => { - const requiredFont = getRequiredFont('th'); - expect(requiredFont.family).toEqual('Noto Sans Thai'); - expect(requiredFont.direction).toEqual('ltr'); - }); -}); diff --git a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.tsx b/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.tsx deleted file mode 100644 index 497114a8a7c..00000000000 --- a/packages/fxa-settings/src/components/ButtonDownloadRecoveryKeyPDF/requiredFont.tsx +++ /dev/null @@ -1,300 +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 notoSansRegular from './fonts/NotoSans-Regular.ttf'; -import notoSansSemibold from './fonts/NotoSans-SemiBold.ttf'; -import notoSansBold from './fonts/NotoSans-Bold.ttf'; -import notoSansGeorgianRegular from './fonts/NotoSansGeorgian-Regular.ttf'; -import notoSansGeorgianSemibold from './fonts/NotoSansGeorgian-SemiBold.ttf'; -import notoSansGeorgianBold from './fonts/NotoSansGeorgian-Bold.ttf'; -import notoSerifGurmukhiRegular from './fonts/NotoSerifGurmukhi-Regular.ttf'; -import notoSerifGurmukhiSemibold from './fonts/NotoSerifGurmukhi-SemiBold.ttf'; -import notoSerifGurmukhiBold from './fonts/NotoSerifGurmukhi-Bold.ttf'; -import notoSansHebrewRegular from './fonts/NotoSansHebrew-Regular.ttf'; -import notoSansHebrewSemibold from './fonts/NotoSansHebrew-SemiBold.ttf'; -import notoSansHebrewBold from './fonts/NotoSansHebrew-Bold.ttf'; -import notoSansJPRegular from './fonts/NotoSansJP-Regular.ttf'; -import notoSansJPSemibold from './fonts/NotoSansJP-SemiBold.ttf'; -import notoSansJPBold from './fonts/NotoSansJP-Bold.ttf'; -import notoSansKRRegular from './fonts/NotoSansKR-Regular.otf'; -import notoSansKRSemibold from './fonts/NotoSansKR-Medium.otf'; -import notoSansKRBold from './fonts/NotoSansKR-Bold.otf'; -import notoSansSCRegular from './fonts/NotoSansSC-Regular.otf'; -import notoSansSCMedium from './fonts/NotoSansSC-Medium.otf'; -import notoSansSCBold from './fonts/NotoSansSC-Bold.otf'; -import notoSansThaiRegular from './fonts/NotoSansThai-Regular.ttf'; -import notoSansThaiSemibold from './fonts/NotoSansThai-SemiBold.ttf'; -import notoSansThaiBold from './fonts/NotoSansThai-Bold.ttf'; -import notoSansTCRegular from './fonts/NotoSansTC-Regular.otf'; -import notoSansTCMedium from './fonts/NotoSansTC-Medium.otf'; -import notoSansTCBold from './fonts/NotoSansTC-Bold.otf'; - -export interface FontSource { - src: string; - style: 'normal' | 'italic' | 'oblique'; - weight: - | 'thin' - | 'ultralight' - | 'light' - | 'normal' - | 'medium' - | 'semibold' - | 'bold' - | 'ultrabold' - | 'heavy' - | number; -} -export interface FontData { - family: string; - sources: FontSource[]; - direction: 'ltr' | 'rtl'; - breakwords: boolean; -} - -export const getRequiredFont = (language: string) => { - // Languages supported below based on 'supportedLanguages.json' - // New cases should only be added if additional languages are not supported by the default Noto Sans font - // Specifying the directionality of the language will impact the layout of the PDF - // Fonts should be saved to fxa-settings/src/assets/fonts - see instructions in fxa-settings/src/assets/fonts/README.md - - let requiredFont: FontData; - switch (language.toLowerCase()) { - // Japanese - case 'ja': - case 'ja-jp': - requiredFont = { - family: 'Noto Sans JP', - sources: [ - { - src: notoSansJPRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansJPSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSansJPBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: true, - }; - break; - // Georgian - case 'ka': - requiredFont = { - family: 'Noto Sans Georgian', - sources: [ - { - src: notoSansGeorgianRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansGeorgianSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSansGeorgianBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: false, - }; - break; - // Hebrew - case 'he': - case 'he-il': - requiredFont = { - family: 'Noto Sans Hebrew', - sources: [ - { - src: notoSansHebrewRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansHebrewSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSansHebrewBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'rtl', - breakwords: false, - }; - break; - // Korean - case 'ko': - case 'ko-kp': - case 'ko-kr': - requiredFont = { - family: 'Noto Sans KR', - sources: [ - { - src: notoSansKRRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansKRSemibold, - style: 'normal', - weight: 'medium', - }, - { - src: notoSansKRBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: false, - }; - break; - // Punjabi (India) - // Noto Sans variant of this font was not working as expected and was preventing PDF generation - // Replaced with Noto Serif version - // NOTE: Punjabi (Pakistan) - pa-PK - is not currently supported and falls back to Punjabi (India) - // If Punjabi (Pakistan) becomes supported, and will need its own script (Shamukhi) and direction (RTL) - case 'pa': - case 'pa-in': - requiredFont = { - family: 'Noto Serif Gurmukhi', - sources: [ - { - src: notoSerifGurmukhiRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSerifGurmukhiSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSerifGurmukhiBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: false, - }; - break; - // Thai - case 'th': - requiredFont = { - family: 'Noto Sans Thai', - sources: [ - { - src: notoSansThaiRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansThaiSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSansThaiBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: false, - }; - break; - // Chinese (Mainland) - Simplified Chinese - case 'zh': - case 'zh-cn': - requiredFont = { - family: 'Noto Sans SC', - sources: [ - { - src: notoSansSCRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansSCMedium, - style: 'normal', - weight: 'medium', - }, - { - src: notoSansSCBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: true, - }; - break; - // Chinese (Taiwan) - Traditional Chinese - case 'zh-tw': - requiredFont = { - family: 'Noto Sans TC', - sources: [ - { - src: notoSansTCRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansTCMedium, - style: 'normal', - weight: 'medium', - }, - { - src: notoSansTCBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: true, - }; - break; - default: - requiredFont = { - family: 'Noto Sans', - sources: [ - { - src: notoSansRegular, - style: 'normal', - weight: 'normal', - }, - { - src: notoSansSemibold, - style: 'normal', - weight: 'semibold', - }, - { - src: notoSansBold, - style: 'normal', - weight: 'bold', - }, - ], - direction: 'ltr', - breakwords: false, - }; - break; - } - - return requiredFont; -}; diff --git a/packages/fxa-settings/src/components/DataBlock/index.test.tsx b/packages/fxa-settings/src/components/DataBlock/index.test.tsx index 0ccb0353c59..8b54b9f07b2 100644 --- a/packages/fxa-settings/src/components/DataBlock/index.test.tsx +++ b/packages/fxa-settings/src/components/DataBlock/index.test.tsx @@ -22,7 +22,6 @@ const multiValue = [ Object.defineProperty(window.navigator, 'clipboard', { value: { writeText: jest.fn() }, }); -window.URL.createObjectURL = jest.fn(); it('can render single values', () => { renderWithLocalizationProvider(); diff --git a/packages/fxa-settings/src/components/DataBlockInline/index.test.tsx b/packages/fxa-settings/src/components/DataBlockInline/index.test.tsx index 1fdea6a4cf8..deba654dba8 100644 --- a/packages/fxa-settings/src/components/DataBlockInline/index.test.tsx +++ b/packages/fxa-settings/src/components/DataBlockInline/index.test.tsx @@ -11,7 +11,6 @@ const singleValue = 'ANMD 1S09 7Y2Y 4EES 02CW BJ6Z PYKP H69F'; Object.defineProperty(window.navigator, 'clipboard', { value: { writeText: jest.fn() }, }); -window.URL.createObjectURL = jest.fn(); it('can render single values', () => { renderWithLocalizationProvider(); diff --git a/packages/fxa-settings/src/components/GetDataTrio/index.test.tsx b/packages/fxa-settings/src/components/GetDataTrio/index.test.tsx index 2dc837affb4..c4e8ae9aab8 100644 --- a/packages/fxa-settings/src/components/GetDataTrio/index.test.tsx +++ b/packages/fxa-settings/src/components/GetDataTrio/index.test.tsx @@ -14,7 +14,6 @@ const url = 'https://mozilla.org'; const setTooltipVisible = jest.fn(); it('renders Trio as expected', () => { - window.URL.createObjectURL = jest.fn(); renderWithLocalizationProvider( { }); it('renders single Copy button as expected', () => { - window.URL.createObjectURL = jest.fn(); renderWithLocalizationProvider( Download and continue diff --git a/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.test.tsx b/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.test.tsx index 54c86c31953..2794a8bf83f 100644 --- a/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.test.tsx +++ b/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.test.tsx @@ -2,7 +2,7 @@ * 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 { screen, waitFor, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; import { Subject } from './mocks'; import { @@ -27,13 +27,7 @@ describe('RecoveryKeySetupDownload', () => { const listItems = within(list).getAllByRole('listitem'); expect(listItems.length).toBe(4); - await waitFor( - () => { - const b = screen.getByRole('button', { name: 'Download and continue' }); - expect(b).toBeInTheDocument(); - }, - { timeout: 5000 } - ); + screen.getByRole('button', { name: 'Download and continue' }); screen.getByRole('button', { name: 'Continue without downloading', diff --git a/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.tsx b/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.tsx index a405ea26840..48ea48e75bd 100644 --- a/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.tsx +++ b/packages/fxa-settings/src/components/RecoveryKeySetupDownload/index.tsx @@ -2,7 +2,6 @@ * 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, { Suspense, lazy } from 'react'; import { FolderIconListItem, GlobeIconListItem, @@ -11,8 +10,8 @@ import { } from '../IconListItem'; import { FtlMsg } from 'fxa-react/lib/utils'; import { logViewEvent } from '../../lib/metrics'; -import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; import DataBlockInline from '../DataBlockInline'; +import ButtonDownloadRecoveryKey from '../ButtonDownloadRecoveryKey'; import { RelierCmsInfo } from '../../models'; export interface RecoveryKeySetupDownloadProps { @@ -23,14 +22,6 @@ export interface RecoveryKeySetupDownloadProps { cmsInfo?: RelierCmsInfo; } -const spinner = ( - -); - -const ButtonDownloadRecoveryKeyPDF = lazy( - () => import('../ButtonDownloadRecoveryKeyPDF') -); - export const InlineRecoveryKeySetupDownload = ({ recoveryKeyValue, email, @@ -81,11 +72,9 @@ export const InlineRecoveryKeySetupDownload = ({ - - - +