diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 97f54b51f..ad94572ae 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2515,6 +2515,19 @@ "message": "Swapping on mobile is here!", "description": "Title for a notification in the 'See What's New' popup. Tells users that they can now use MetaMask Swaps on Mobile." }, + "notifications20ActionText": { + "message": "Learn more", + "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a ledger page to resolve the U2F connection issue." + }, + + "notifications20Description": { + "message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support.", + "description": "Description of a notification in the 'See What's New' popup. Describes the U2F support being dropped by firefox and that it affects ledger users." + }, + "notifications20Title": { + "message": "Ledger and Firefox Users Experiencing Connection Issues", + "description": "Title for a notification in the 'See What's New' popup. Tells users that latest firefox users using U2F may experience connection issues." + }, "notifications3ActionText": { "message": "Read more", "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website." @@ -4518,6 +4531,22 @@ "transferFrom": { "message": "Transfer from" }, + "troubleConnectingToLedgerU2FOnFirefox": { + "message": "We're having trouble connecting your Ledger. $1", + "description": "$1 is a link to the wallet connection guide;" + }, + "troubleConnectingToLedgerU2FOnFirefox2": { + "message": "Review our hardware wallet connection guide and try again.", + "description": "$1 of the ledger wallet connection guide" + }, + "troubleConnectingToLedgerU2FOnFirefoxLedgerSolution": { + "message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support. Learn how to fix this issue $1.", + "description": "It is a link to the ledger website for the workaround." + }, + "troubleConnectingToLedgerU2FOnFirefoxLedgerSolution2": { + "message": "here", + "description": "Second part of the error message; It is a link to the ledger website for the workaround." + }, "troubleConnectingToWallet": { "message": "We had trouble connecting to your $1, try reviewing $2 and try again.", "description": "$1 is the wallet device name; $2 is a link to wallet connection guide" diff --git a/coverage-targets.js b/coverage-targets.js index 6097331d1..e246ecb14 100644 --- a/coverage-targets.js +++ b/coverage-targets.js @@ -6,10 +6,10 @@ // subset of files to check against these targets. module.exports = { global: { - lines: 66, - branches: 54.4, - statements: 65, - functions: 58.5, + lines: 66.7, + branches: 54.9, + statements: 65.75, + functions: 59.55, }, transforms: { branches: 100, diff --git a/shared/notifications/index.js b/shared/notifications/index.js index 10c24bbfe..9bf285957 100644 --- a/shared/notifications/index.js +++ b/shared/notifications/index.js @@ -102,6 +102,10 @@ export const UI_NOTIFICATIONS = { width: '100%', }, }, + 20: { + id: 20, + date: null, + }, }; export const getTranslatedUINotifications = (t, locale) => { @@ -279,5 +283,16 @@ export const getTranslatedUINotifications = (t, locale) => { ) : '', }, + 20: { + ...UI_NOTIFICATIONS[20], + title: t('notifications20Title'), + description: [t('notifications20Description')], + actionText: t('notifications20ActionText'), + date: UI_NOTIFICATIONS[20].date + ? new Intl.DateTimeFormat(formattedLocale).format( + new Date(UI_NOTIFICATIONS[20].date), + ) + : '', + }, }; }; diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index 98c636e50..fd0bf14a7 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -73,6 +73,12 @@ function getActionFunctionById(id, history) { updateViewedNotifications({ 19: true }); history.push(`${EXPERIMENTAL_ROUTE}#autodetect-nfts`); }, + 20: () => { + updateViewedNotifications({ 20: true }); + global.platform.openTab({ + url: ZENDESK_URLS.LEDGER_FIREFOX_U2F_GUIDE, + }); + }, }; return actionFunctions[id]; diff --git a/ui/helpers/constants/zendesk-url.js b/ui/helpers/constants/zendesk-url.js index 1e3f4aec9..79ea52ffb 100644 --- a/ui/helpers/constants/zendesk-url.js +++ b/ui/helpers/constants/zendesk-url.js @@ -15,6 +15,8 @@ const ZENDESK_URLS = { 'https://metamask.zendesk.com/hc/en-us/articles/360015289932', INFURA_BLOCKAGE: 'https://metamask.zendesk.com/hc/en-us/articles/360059386712', + LEDGER_FIREFOX_U2F_GUIDE: + 'https://support.ledger.com/hc/en-us/articles/10371387758493-MetaMask-Firefox-Ledger-Integration-Issue?support=true', LEGACY_WEB3: 'https://metamask.zendesk.com/hc/en-us/articles/360053147012', NFT_TOKENS: 'https://metamask.zendesk.com/hc/en-us/articles/360058238591-NFT-tokens-in-MetaMask-wallet', diff --git a/ui/pages/create-account/connect-hardware/__snapshots__/index.test.tsx.snap b/ui/pages/create-account/connect-hardware/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..5284b9760 --- /dev/null +++ b/ui/pages/create-account/connect-hardware/__snapshots__/index.test.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConnectHardwareForm should match snapshot 1`] = ` +
+
+
+

+ Connect a hardware wallet +

+

+ Select a hardware wallet you'd like to use with MetaMask. +

+
+
+ + +
+
+ + +
+ +
+
+`; diff --git a/ui/pages/create-account/connect-hardware/index.js b/ui/pages/create-account/connect-hardware/index.js index c46800f85..3105ceb29 100644 --- a/ui/pages/create-account/connect-hardware/index.js +++ b/ui/pages/create-account/connect-hardware/index.js @@ -20,7 +20,14 @@ import { HardwareDeviceNames, LedgerTransportTypes, } from '../../../../shared/constants/hardware-wallets'; +import { + BUTTON_TYPES, + BUTTON_SIZES, + Button, + Text, +} from '../../../components/component-library'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; +import { TextColor } from '../../../helpers/constants/design-system'; import SelectHardware from './select-hardware'; import AccountList from './account-list'; @@ -74,6 +81,7 @@ class ConnectHardwareForm extends Component { browserSupported: true, unlocked: false, device: null, + isFirefox: false, }; UNSAFE_componentWillReceiveProps(nextProps) { @@ -89,6 +97,10 @@ class ConnectHardwareForm extends Component { componentDidMount() { this.checkIfUnlocked(); + const useAgent = window.navigator.userAgent; + if (/Firefox/u.test(useAgent)) { + this.setState({ isFirefox: true }); + } } async checkIfUnlocked() { @@ -287,23 +299,64 @@ class ConnectHardwareForm extends Component { renderError() { if (this.state.error === U2F_ERROR) { + if (this.state.device === 'ledger' && this.state.isFirefox) { + return ( + <> + + {this.context.t('troubleConnectingToLedgerU2FOnFirefox', [ + // eslint-disable-next-line react/jsx-key + , + ])} + + + {this.context.t( + 'troubleConnectingToLedgerU2FOnFirefoxLedgerSolution', + [ + // eslint-disable-next-line react/jsx-key + , + ], + )} + + + ); + } return ( -

+ {this.context.t('troubleConnectingToWallet', [ this.state.device, // eslint-disable-next-line react/jsx-key - {this.context.t('walletConnectionGuide')} - , + , ])} -

+ ); } return this.state.error ? ( diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx new file mode 100644 index 000000000..770782462 --- /dev/null +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -0,0 +1,151 @@ +import configureMockStore from 'redux-mock-store'; +import { fireEvent, waitFor } from '@testing-library/react'; +import thunk from 'redux-thunk'; +import React from 'react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { + LedgerTransportTypes, + HardwareDeviceNames, +} from '../../../../shared/constants/hardware-wallets'; +import ConnectHardwareForm from '.'; + +const mockConnectHardware = jest.fn(); +const mockCheckHardwareStatus = jest.fn().mockResolvedValue(false); + +jest.mock('../../../store/actions', () => ({ + connectHardware: () => mockConnectHardware, + checkHardwareStatus: () => mockCheckHardwareStatus, +})); + +jest.mock('../../../selectors', () => ({ + getCurrentChainId: () => jest.fn().mockResolvedValue('0x1'), + getRpcPrefsForCurrentProvider: () => jest.fn().mockResolvedValue({}), + getMetaMaskAccountsConnected: () => jest.fn().mockResolvedValue([]), + getMetaMaskAccounts: () => jest.fn().mockResolvedValue([]), +})); + +jest.mock('../../../ducks/history/history', () => ({ + getMostRecentOverviewPage: () => jest.fn().mockResolvedValue('/'), +})); + +const mockProps = { + forgetDevice: jest.fn(), + showAlert: jest.fn(), + hideAlert: jest.fn(), + unlockHardwareWalletAccount: jest.fn(), + setHardwareWalletDefaultHdPath: jest.fn(), + history: {}, + defaultHdPath: "m/44'/60'/0'/0", + mostRecentOverviewPage: '', +}; + +const mockState = { + metamask: { + provider: { + chainId: '0x1', + }, + }, + appState: { + networkDropdownOpen: false, + gasIsLoading: false, + isLoading: false, + modal: { + open: false, + modalState: { + name: null, + props: {}, + }, + previousModalState: { + name: null, + }, + }, + warning: null, + chainId: '0x1', + rpcPrefs: null, + accounts: [], + connectedAccounts: [], + defaultHdPaths: { + [HardwareDeviceNames.lattice]: "m/44'/60'/0'/0", + [HardwareDeviceNames.ledger]: "m/44'/60'/0'/0", + [HardwareDeviceNames.trezor]: "m/44'/60'/0'/0", + }, + mostRecentOverviewPage: '', + ledgerTransportType: LedgerTransportTypes.webhid, + }, +}; + +describe('ConnectHardwareForm', () => { + const mockStore = configureMockStore([thunk])(mockState); + it('should match snapshot', () => { + const { container } = renderWithProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); + + describe('U2F Error', () => { + it('should render a U2F error', async () => { + mockConnectHardware.mockRejectedValue(new Error('U2F Error')); + const mockStateWithU2F = Object.assign(mockState, {}); + mockStateWithU2F.appState.ledgerTransportType = LedgerTransportTypes.u2f; + const mockStoreWithU2F = configureMockStore([thunk])(mockStateWithU2F); + const { getByText, getByLabelText, queryByText } = renderWithProvider( + , + mockStoreWithU2F, + ); + + const ledgerButton = getByLabelText('Ledger'); + const continueButton = getByText('Continue'); + + fireEvent.click(ledgerButton); + fireEvent.click(continueButton); + + await waitFor(() => { + expect( + getByText('We had trouble connecting to your ledger, try reviewing', { + exact: false, + }), + ).toBeInTheDocument(); + const firefoxError = queryByText( + "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support. Learn how to fix this issue", + { exact: false }, + ); + expect(firefoxError).not.toBeInTheDocument(); + }); + }); + + it('should render a different U2F error for firefox', async () => { + jest + .spyOn(window.navigator, 'userAgent', 'get') + .mockReturnValue( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0', + ); + + mockConnectHardware.mockRejectedValue(new Error('U2F Error')); + const mockStateWithU2F = Object.assign(mockState, {}); + mockStateWithU2F.appState.ledgerTransportType = LedgerTransportTypes.u2f; + const mockStoreWithU2F = configureMockStore([thunk])(mockStateWithU2F); + const { getByText, getByLabelText } = renderWithProvider( + , + mockStoreWithU2F, + ); + + const ledgerButton = getByLabelText('Ledger'); + const continueButton = getByText('Continue'); + + fireEvent.click(ledgerButton); + fireEvent.click(continueButton); + + await waitFor(() => { + expect( + getByText( + "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support. Learn how to fix this issue", + { exact: false }, + ), + ).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index bbe5e53e8..be5a5f492 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -939,6 +939,7 @@ function getAllowedAnnouncementIds(state) { const supportsWebHid = window.navigator.hid !== undefined; const currentlyUsingLedgerLive = getLedgerTransportType(state) === LedgerTransportTypes.live; + const isFirefox = window.navigator.userAgent.includes('Firefox'); return { 1: false, @@ -960,6 +961,7 @@ function getAllowedAnnouncementIds(state) { 17: false, 18: true, 19: true, + 20: currentKeyringIsLedger && isFirefox, }; }