diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 951cdfeecb31..da73cf790060 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6217,6 +6217,18 @@ "siweURI": { "message": "URL" }, + "skipAccountSecurity": { + "message": "Skip account security?" + }, + "skipAccountSecurityDetails": { + "message": "If you lose this Secret Recovery Phrase, you won’t be able to access this wallet." + }, + "skipAccountSecuritySecureNow": { + "message": "Secure now" + }, + "skipAccountSecuritySkip": { + "message": "Skip" + }, "skipDeepLinkInterstitial": { "message": "Don't show interstitial screen when opening deep links" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 951cdfeecb31..da73cf790060 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -6217,6 +6217,18 @@ "siweURI": { "message": "URL" }, + "skipAccountSecurity": { + "message": "Skip account security?" + }, + "skipAccountSecurityDetails": { + "message": "If you lose this Secret Recovery Phrase, you won’t be able to access this wallet." + }, + "skipAccountSecuritySecureNow": { + "message": "Secure now" + }, + "skipAccountSecuritySkip": { + "message": "Skip" + }, "skipDeepLinkInterstitial": { "message": "Don't show interstitial screen when opening deep links" }, diff --git a/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts index 137fa67a5887..f06f00e146eb 100644 --- a/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts +++ b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts @@ -25,6 +25,17 @@ class SecureWalletPage { private readonly revealSecretRecoveryPhraseButton = '[data-testid="recovery-phrase-reveal"]'; + private readonly skipAccountSecurityMessage = { + text: 'Skip account security?', + tag: 'h3', + }; + + private readonly skipSRPBackupCheckbox = + '[data-testid="skip-srp-backup-checkbox"]'; + + private readonly skipSRPBackupConfirmButton = + '[data-testid="skip-srp-backup-button"]'; + private readonly secureWalletRecommendedButton = '[data-testid="recovery-phrase-remind-later"]'; @@ -158,6 +169,11 @@ class SecureWalletPage { async skipSRPBackup(): Promise { console.log('Skip SRP backup on Reveal SRP Onboarding page'); await this.driver.clickElement(this.secureWalletRecommendedButton); + await this.driver.waitForSelector(this.skipAccountSecurityMessage); + await this.driver.clickElement(this.skipSRPBackupCheckbox); + await this.driver.clickElementAndWaitToDisappear( + this.skipSRPBackupConfirmButton, + ); } } diff --git a/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/skip-srp-backup-popover.test.tsx.snap b/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/skip-srp-backup-popover.test.tsx.snap new file mode 100644 index 000000000000..3a619101934a --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/skip-srp-backup-popover.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SkipSRPBackup should match snapshot 1`] = `
`; diff --git a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js index 15cbb4331067..2ba3868a3ab2 100644 --- a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js +++ b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js @@ -1,13 +1,11 @@ import React, { useState, useContext, useCallback, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom-v5-compat'; -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { ONBOARDING_CONFIRM_SRP_ROUTE, - ONBOARDING_METAMETRICS, ONBOARDING_REVEAL_SRP_ROUTE, - ONBOARDING_COMPLETION_ROUTE, REVEAL_SRP_LIST_ROUTE, } from '../../../helpers/constants/routes'; import { @@ -39,26 +37,21 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { getHDEntropyIndex, getFirstTimeFlowType } from '../../../selectors'; +import { getHDEntropyIndex } from '../../../selectors'; import SRPDetailsModal from '../../../components/app/srp-details-modal'; -import { setSeedPhraseBackedUp } from '../../../store/actions'; -import { TraceName } from '../../../../shared/lib/trace'; -import { getBrowserName } from '../../../../shared/modules/browser-runtime.utils'; -import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; -import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import RecoveryPhraseChips from './recovery-phrase-chips'; +import SkipSRPBackup from './skip-srp-backup-popover'; export default function RecoveryPhrase({ secretRecoveryPhrase }) { const navigate = useNavigate(); const t = useI18nContext(); const { search } = useLocation(); - const dispatch = useDispatch(); - const firstTimeFlowType = useSelector(getFirstTimeFlowType); const trackEvent = useContext(MetaMetricsContext); - const { bufferedEndTrace } = trackEvent; const hdEntropyIndex = useSelector(getHDEntropyIndex); const [phraseRevealed, setPhraseRevealed] = useState(false); const [showSrpDetailsModal, setShowSrpDetailsModal] = useState(false); + const [showSkipSRPBackupPopover, setShowSkipSRPBackupPopover] = + useState(false); const searchParams = new URLSearchParams(search); const isFromReminder = searchParams.get('isFromReminder'); const isFromSettingsSecurity = searchParams.get('isFromSettingsSecurity'); @@ -112,37 +105,16 @@ export default function RecoveryPhrase({ secretRecoveryPhrase }) { setShowSrpDetailsModal(true); }, [trackEvent]); - const handleRemindLater = useCallback(async () => { - await dispatch(setSeedPhraseBackedUp(false)); - + const handleClickNotRecommended = () => { trackEvent({ category: MetaMetricsEventCategory.Onboarding, - event: MetaMetricsEventName.OnboardingWalletSecuritySkipConfirmed, + event: MetaMetricsEventName.OnboardingWalletSecuritySkipInitiated, properties: { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - hd_entropy_index: hdEntropyIndex, + hd_entropy_index: hdEntropyIndex ?? 0, }, }); - bufferedEndTrace?.({ name: TraceName.OnboardingNewSrpCreateWallet }); - bufferedEndTrace?.({ name: TraceName.OnboardingJourneyOverall }); - - if ( - getBrowserName() === PLATFORM_FIREFOX || - firstTimeFlowType === FirstTimeFlowType.restore - ) { - navigate(ONBOARDING_COMPLETION_ROUTE, { replace: true }); - } else { - navigate(ONBOARDING_METAMETRICS, { replace: true }); - } - }, [ - bufferedEndTrace, - dispatch, - firstTimeFlowType, - hdEntropyIndex, - navigate, - trackEvent, - ]); + setShowSkipSRPBackupPopover(true); + }; const handleBack = useCallback(() => { navigate( @@ -169,6 +141,14 @@ export default function RecoveryPhrase({ secretRecoveryPhrase }) { data-testid="recovery-phrase" > + {showSkipSRPBackupPopover && + !isFromReminder && + !isFromSettingsSecurity && ( + setShowSkipSRPBackupPopover(false)} + secureYourWallet={handleContinue} + /> + )} {showSrpDetailsModal && ( setShowSrpDetailsModal(false)} /> )} @@ -274,7 +254,7 @@ export default function RecoveryPhrase({ secretRecoveryPhrase }) { width={BlockSize.Full} variant={ButtonVariant.Link} size={ButtonSize.Lg} - onClick={handleRemindLater} + onClick={handleClickNotRecommended} type="button" data-testid="recovery-phrase-remind-later" > diff --git a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.test.js b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.test.js index ee15538b3dd4..fcff9d4e7973 100644 --- a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.test.js +++ b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.test.js @@ -1,4 +1,4 @@ -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate'; @@ -170,4 +170,30 @@ describe('Review Recovery Phrase Component', () => { replace: true, }); }); + + it('should show skip srp backup popover when not from reminder and not from settings security', async () => { + const { getByTestId } = renderWithProvider( + , + mockStore, + ); + + const remindLaterButton = getByTestId('recovery-phrase-remind-later'); + + fireEvent.click(remindLaterButton); + + expect(getByTestId('skip-srp-backup-modal')).toBeInTheDocument(); + + const checkbox = getByTestId('skip-srp-backup-checkbox'); + expect(checkbox).toBeInTheDocument(); + + const confirmSkip = getByTestId('skip-srp-backup-button'); + expect(confirmSkip).toBeInTheDocument(); + expect(confirmSkip).toBeDisabled(); + + fireEvent.click(checkbox); + + await waitFor(() => { + expect(confirmSkip).toBeEnabled(); + }); + }); }); diff --git a/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.test.tsx b/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.test.tsx new file mode 100644 index 000000000000..d86b967a11c5 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.test.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate'; +import * as browserRuntime from '../../../../shared/modules/browser-runtime.utils'; +import { + PLATFORM_FIREFOX, + PLATFORM_CHROME, +} from '../../../../shared/constants/app'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; +import { + ONBOARDING_COMPLETION_ROUTE, + ONBOARDING_METAMETRICS, +} from '../../../helpers/constants/routes'; +import SkipSRPBackup from './skip-srp-backup-popover'; + +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom-v5-compat', () => { + return { + ...jest.requireActual('react-router-dom-v5-compat'), + useNavigate: () => mockNavigate, + }; +}); + +describe('SkipSRPBackup', () => { + const mockStore = { + metamask: { + firstTimeFlowType: FirstTimeFlowType.create, + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + type: 'custom', + url: 'https://mainnet.infura.io', + networkClientId: 'mainnet', + }, + ], + blockExplorerUrls: [], + }, + }, + selectedNetworkClientId: 'mainnet', + networksMetadata: { + mainnet: { + EIPS: { 1559: true }, + status: 'available', + }, + }, + internalAccounts: { + accounts: { + accountId: { + address: '0x0000000000000000000000000000000000000000', + metadata: { + keyring: 'HD Key Tree', + }, + }, + }, + selectedAccount: 'accountId', + }, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x0000000000000000000000000000000000000000'], + }, + ], + }, + localeMessages: { + currentLocale: 'en', + }, + }; + + const store = configureMockStore([thunk])(mockStore); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should match snapshot', () => { + const { container } = renderWithProvider( + , + store, + ); + expect(container).toMatchSnapshot(); + }); + + it('should navigate to onboarding metametrics when skip is confirmed on non-Firefox browser', async () => { + jest + .spyOn(browserRuntime, 'getBrowserName') + .mockReturnValue(PLATFORM_CHROME); + + const { getByTestId } = renderWithProvider( + , + store, + ); + + const checkbox = getByTestId('skip-srp-backup-checkbox'); + expect(checkbox).toBeInTheDocument(); + + const confirmSkip = getByTestId('skip-srp-backup-button'); + expect(confirmSkip).toBeInTheDocument(); + expect(confirmSkip).toBeDisabled(); + + fireEvent.click(checkbox); + + await waitFor(() => { + expect(confirmSkip).toBeEnabled(); + }); + + fireEvent.click(confirmSkip); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ONBOARDING_METAMETRICS); + }); + }); + + it('should navigate to onboarding completion when skip is confirmed on Firefox', async () => { + jest + .spyOn(browserRuntime, 'getBrowserName') + .mockReturnValue(PLATFORM_FIREFOX); + + const { getByTestId } = renderWithProvider( + , + store, + ); + + const checkbox = getByTestId('skip-srp-backup-checkbox'); + expect(checkbox).toBeInTheDocument(); + + const confirmSkip = getByTestId('skip-srp-backup-button'); + expect(confirmSkip).toBeInTheDocument(); + expect(confirmSkip).toBeDisabled(); + + fireEvent.click(checkbox); + + await waitFor(() => { + expect(confirmSkip).toBeEnabled(); + }); + + fireEvent.click(confirmSkip); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ONBOARDING_COMPLETION_ROUTE); + }); + }); +}); diff --git a/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.tsx b/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.tsx new file mode 100644 index 000000000000..2603b3be2536 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/skip-srp-backup-popover.tsx @@ -0,0 +1,165 @@ +import { useNavigate } from 'react-router-dom-v5-compat'; +import { useDispatch, useSelector } from 'react-redux'; +import React, { useCallback, useContext, useState } from 'react'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + AlignItems, + Display, + IconColor, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { + Box, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, + Text, + ButtonSize, + Checkbox, + Button, + ButtonVariant, + Icon, + IconSize, + IconName, +} from '../../../components/component-library'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { getHDEntropyIndex } from '../../../selectors/selectors'; +import { setSeedPhraseBackedUp } from '../../../store/actions'; +import { + ONBOARDING_COMPLETION_ROUTE, + ONBOARDING_METAMETRICS, +} from '../../../helpers/constants/routes'; +import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; +import { getFirstTimeFlowType } from '../../../selectors'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; +import { getBrowserName } from '../../../../shared/modules/browser-runtime.utils'; +import { TraceName } from '../../../../shared/lib/trace'; + +type SkipSRPBackupProps = { + onClose: () => void; + secureYourWallet: () => void; +}; + +// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function SkipSRPBackup({ + onClose, + secureYourWallet, +}: SkipSRPBackupProps) { + const [checked, setChecked] = useState(false); + const t = useI18nContext(); + const dispatch = useDispatch(); + const hdEntropyIndex = useSelector(getHDEntropyIndex); + const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const trackEvent = useContext(MetaMetricsContext); + const { bufferedEndTrace } = trackEvent; + const navigate = useNavigate(); + + const onSkipSrpBackup = useCallback(async () => { + await dispatch(setSeedPhraseBackedUp(false)); + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.OnboardingWalletSecuritySkipConfirmed, + properties: { + // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 + // eslint-disable-next-line @typescript-eslint/naming-convention + hd_entropy_index: hdEntropyIndex, + }, + }); + bufferedEndTrace?.({ name: TraceName.OnboardingNewSrpCreateWallet }); + bufferedEndTrace?.({ name: TraceName.OnboardingJourneyOverall }); + + if ( + getBrowserName() === PLATFORM_FIREFOX || + firstTimeFlowType === FirstTimeFlowType.restore + ) { + navigate(ONBOARDING_COMPLETION_ROUTE); + } else { + navigate(ONBOARDING_METAMETRICS); + } + }, [ + dispatch, + firstTimeFlowType, + hdEntropyIndex, + navigate, + trackEvent, + bufferedEndTrace, + ]); + + return ( + + + + + + + + {t('skipAccountSecurity')} + + + + + { + setChecked(!checked); + }} + label={t('skipAccountSecurityDetails')} + /> + + + + + + + + ); +}