From 904dad256ff24af30c168b173feed9600cfec4d4 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 21 Oct 2021 16:47:03 -0230 Subject: [PATCH 01/16] Connect Ledger via WebHID (#12411) * Connect ledger via webhid if that option is available * Explicitly setting preference for webhid * Use ledgerTransportType enum instead of booleans for ledger live and webhid preferences * Use single setLEdgerTransport preference methods and property * Temp * Lint fix * Unit test fix * Remove async keyword from setLedgerTransportPreference function definition in preferences controller * Fix ledgelive setting toggle logic * Migrate useLedgerLive preference property to ledgerTransportType * Use shared constants for ledger transport type enums * Use constant for ledger usb vendor id * Use correct property to check if ledgerLive preference is set when deciding whether to ask for webhid connection * Update eth-ledger-bridge-keyring to v0.9.0 * Only show ledger live transaction helper messages if using ledger live * Only show ledger live part of tutorial if ledger live setting is on * Fix ledger related prop type errors * Explicitly use u2f enum instead of empty string as a transport type; default transport type to webhid if available; use constants for u2f and webhid * Cleanup * Wrap ledger webhid device request in try/catch * Clean up * Lint fix * Ensure user can easily connect their ledger wallet when they need to. * Fix locales * Fix/improve locales changes * Remove unused isFirefox property from confirm-transaction-base.container.js * Disable transaction and message signing confirmation if ledger webhid requires connection * Ensure translation keys for ledger connection options in settings dropdown can be properly detected by verify-locales * Drop .component from ledger-instruction-field file name * Move renderLedgerLiveStep to module scope * Remove ledgerLive from function and message names in ledger-instruction-field * Wrap ledger connection logic in ledger-instruction-field in try catch * Clean up signature-request.component.js * Check whether the signing address, and not the selected address, is a ledger account in singature-request.container * Ensure ledger instructions and webhid connection button are shown on signature-request-original signatures * Improve webhid selection handling in select-ledger-transport-type onChange handler * Move metamask redux focused ledger selectors to metamask duck * Lint fix * Use async await in checkWebHidStatusRef.current * Remove unnecessary use of ref in ledger-instruction-field.js * Lint fix * Remove unnecessary try/catch in ledger-instruction-field.js * Check if from address, not selected address, is from a ledger account in confirm-approve * Move findKeyringForAddress to metamask duck * Fix typo in function name * Ensure isEqualCaseInsensitive handles possible differences in address casing * Fix Learn More link size in advanced settings tab * Update app/scripts/migrations/066.js Co-authored-by: Mark Stacey * Update ui/pages/settings/advanced-tab/advanced-tab.component.test.js Co-authored-by: Mark Stacey * Add jsdoc comments for new selectors * Use jest.spyOn for mocking navigator in ledger webhid migration tests * Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType * Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType * Fix font size of link in ledger connection description in advanced settings * Fix return type in setLedgerTransportPreference comment * Clean up connectHardware code for webhid connection in actions.js * Update app/scripts/migrations/066.test.js Co-authored-by: Mark Stacey * Update ui/ducks/metamask/metamask.js Co-authored-by: Mark Stacey * Add migration test for when useLedgerLive is true in a browser that supports webhid * Lint fix * Fix inline-link size Co-authored-by: Mark Stacey --- app/_locales/en/messages.json | 54 +++++-- app/_locales/es/messages.json | 6 - app/_locales/es_419/messages.json | 6 - app/_locales/hi/messages.json | 6 - app/_locales/id/messages.json | 6 - app/_locales/ja/messages.json | 6 - app/_locales/ko/messages.json | 6 - app/_locales/ph/messages.json | 6 - app/_locales/pt_BR/messages.json | 6 - app/_locales/ru/messages.json | 6 - app/_locales/vi/messages.json | 6 - app/scripts/controllers/preferences.js | 25 +-- app/scripts/metamask-controller.js | 23 +-- app/scripts/migrations/066.js | 37 +++++ app/scripts/migrations/066.test.js | 116 +++++++++++++ app/scripts/migrations/index.js | 2 + app/scripts/platforms/extension.js | 11 +- package.json | 2 +- shared/constants/hardware-wallets.js | 17 ++ .../app/ledger-instruction-field/index.js | 1 + .../ledger-instruction-field.js | 153 ++++++++++++++++++ .../signature-request-original.component.js | 10 ++ .../signature-request-original.container.js | 16 +- .../signature-request-footer.component.js | 5 +- .../signature-request.component.js | 17 +- .../signature-request.container.js | 28 +++- ui/ducks/app/app.js | 16 ++ ui/ducks/metamask/metamask.js | 60 ++++++- .../confirm-approve-content.component.js | 13 ++ .../confirm-approve-content/index.scss | 5 + ui/pages/confirm-approve/confirm-approve.js | 17 +- .../confirm-transaction-base.component.js | 60 ++----- .../confirm-transaction-base.container.js | 30 ++-- .../create-account/connect-hardware/index.js | 17 +- .../connect-hardware/select-hardware.js | 5 +- .../select-hardware.stories.js | 5 +- .../advanced-tab/advanced-tab.component.js | 81 ++++++++-- .../advanced-tab.component.test.js | 5 +- .../advanced-tab/advanced-tab.container.js | 8 +- ui/pages/settings/index.scss | 7 + ui/selectors/selectors.js | 33 ++-- ui/store/actionConstants.js | 4 + ui/store/actions.js | 26 ++- yarn.lock | 8 +- 44 files changed, 768 insertions(+), 209 deletions(-) create mode 100644 app/scripts/migrations/066.js create mode 100644 app/scripts/migrations/066.test.js create mode 100644 ui/components/app/ledger-instruction-field/index.js create mode 100644 ui/components/app/ledger-instruction-field/ledger-instruction-field.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 50c923b3f..cbc845506 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -345,6 +345,10 @@ "chromeRequiredForHardwareWallets": { "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, + "clickToConnectLedgerViaWebHID": { + "message": "Click here to connect your Ledger via WebHID", + "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" + }, "clickToRevealSeed": { "message": "Click here to reveal secret words" }, @@ -1251,36 +1255,42 @@ "ledgerAccountRestriction": { "message": "You need to make use your last account before you can add a new one." }, - "ledgerLiveAdvancedSetting": { - "message": "Use Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "The new Ledger Live bridge allows you to more easily use your Ledger. Only available in Chrome." - }, - "ledgerLiveApp": { - "message": "Ledger Live App" - }, - "ledgerLiveDialogHeader": { + "ledgerConnectionInstructionHeader": { "message": "Prior to clicking confirm:" }, - "ledgerLiveDialogStepFour": { + "ledgerConnectionInstructionStepFour": { "message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device" }, - "ledgerLiveDialogStepOne": { + "ledgerConnectionInstructionStepOne": { "message": "Enable Use Ledger Live under Settings > Advanced" }, - "ledgerLiveDialogStepThree": { + "ledgerConnectionInstructionStepThree": { "message": "Plug in your Ledger device and select the Ethereum app" }, - "ledgerLiveDialogStepTwo": { + "ledgerConnectionInstructionStepTwo": { "message": "Open and unlock Ledger Live App" }, + "ledgerConnectionPreferenceDescription": { + "message": "Customize how you connect your Ledger to MetaMask. $1 is recommended, but other options are available. Read more here: $2", + "description": "A description that appears above a dropdown where users can select between up to three options - Ledger Live, U2F or WebHID - depending on what is supported in their browser. $1 is the recommended browser option, it will be either WebHID or U2f. $2 is a link to an article where users can learn more, but will be the translation of the learnMore message." + }, + "ledgerLive": { + "message": "Ledger Live", + "description": "The name of a desktop app that can be used with your ledger device. We can also use it to connect a users Ledger device to MetaMask." + }, + "ledgerLiveApp": { + "message": "Ledger Live App" + }, "ledgerLocked": { "message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened." }, "ledgerTimeout": { "message": "Ledger Live is taking too long to respond or connection timeout. Make sure Ledger Live app is opened and your device is unlocked." }, + "ledgerWebHIDNotConnectedErrorMessage": { + "message": "The ledger device was not connected. If you wish to connect your Ledger, please click 'Continue' again and approve HID connection", + "description": "An error message shown to the user during the hardware connect flow." + }, "letsGoSetUp": { "message": "Yes, let’s get set up!" }, @@ -1710,6 +1720,10 @@ "onlyConnectTrust": { "message": "Only connect with sites you trust." }, + "openFullScreenForLedgerWebHid": { + "message": "Open MetaMask in full screen to connect your ledger via WebHID.", + "description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid." + }, "optional": { "message": "Optional" }, @@ -1772,6 +1786,10 @@ "message": "+ $1 more", "description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items" }, + "preferredLedgerConnectionType": { + "message": "Preferred Ledger Connection Type", + "description": "A header for a dropdown in the advanced section of settings. Appears above the ledgerConnectionPreferenceDescription message" + }, "prev": { "message": "Prev" }, @@ -2817,6 +2835,10 @@ "typePassword": { "message": "Type your MetaMask password" }, + "u2f": { + "message": "U2F", + "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." + }, "unapproved": { "message": "Unapproved" }, @@ -2958,6 +2980,10 @@ "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", "description": "$1 is a clickable link." }, + "webhid": { + "message": "WebHID", + "description": "Refers to a interface for connecting external devices to the browser. Used for connecting ledger to the browser. Read more here https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API" + }, "welcome": { "message": "Welcome to MetaMask" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index f98631f54..63d07557d 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Debe usar su última cuenta antes de poder agregar una nueva." }, - "ledgerLiveAdvancedSetting": { - "message": "Utilizar Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome." - }, "ledgerLiveApp": { "message": "Aplicación de Ledger Live" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index f98631f54..63d07557d 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Debe usar su última cuenta antes de poder agregar una nueva." }, - "ledgerLiveAdvancedSetting": { - "message": "Utilizar Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome." - }, "ledgerLiveApp": { "message": "Aplicación de Ledger Live" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 18147a08b..217bdeca4 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "नया खाता जोड़ने से पहले आपको अपने अंतिम खाते का उपयोग करना होगा।" }, - "ledgerLiveAdvancedSetting": { - "message": "Ledger Live का उपयोग करें" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "नया Ledger Live ब्रिज आपको अपने लेजर का अधिक आसानी से उपयोग करने की अनुमति देता है। केवल Chrome में उपलब्ध है।" - }, "ledgerLiveApp": { "message": "Ledger Live ऐप" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 529c98ed1..915ebf125 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Anda perlu memanfaatkan akun terakhir Anda sebelum menambahkan yang baru." }, - "ledgerLiveAdvancedSetting": { - "message": "Gunakan Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "Jembatan Ledger Live baru memungkinkan Anda untuk menggunakan Ledger Anda dengan lebih mudah. Hanya tersedia di Chrome." - }, "ledgerLiveApp": { "message": "Aplikasi Ledger Live" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index f9e1d125a..a3af48c46 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "新しいアカウントを追加するには、その前に最後のアカウントを使用する必要があります。" }, - "ledgerLiveAdvancedSetting": { - "message": "レジャー ライブを使用" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "新しいレジャー ライブのブリッジを使用すると、レジャーをより簡単に使用できます。Chrome でのみ利用可能。" - }, "ledgerLiveApp": { "message": "レジャー ライブのアプリ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 96ad9f3fa..9ba61d76e 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "새 계정을 추가하려면 먼저 마지막 계정을 사용해야 합니다." }, - "ledgerLiveAdvancedSetting": { - "message": "Ledger Live 사용하기" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능합니다." - }, "ledgerLiveApp": { "message": "Ledger Live 앱" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index d42c1aa80..c7d40dd55 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Kailangan mong gamitin ang huli mong account bago ka magdagdag ng panibago." }, - "ledgerLiveAdvancedSetting": { - "message": "Gamitin ang Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "Binibigyang-daan ka ng bagong Ledger Live bridge na mas madaling magamit ang iyong Ledger. Available lang sa Chrome." - }, "ledgerLiveApp": { "message": "Ledger Live App" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 9e326c99f..40bde73d3 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Você precisa usar sua última conta antes de adicionar uma nova." }, - "ledgerLiveAdvancedSetting": { - "message": "Usar Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "A nova ponte do Ledger Live permite utilizar seu Ledger mais facilmente. Disponível somente no Chrome." - }, "ledgerLiveApp": { "message": "Aplicativo Ledger Live" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 054f5e100..881ee1042 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Вам необходимо использовать свой последний счет, прежде чем вы сможете добавить новый." }, - "ledgerLiveAdvancedSetting": { - "message": "Использовать Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "Новое решение Ledger Live Bridge упрощает использование Ledger. Доступно только в Chrome." - }, "ledgerLiveApp": { "message": "Приложение Ledger Live" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 56ed93266..a1f78fa6d 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1004,12 +1004,6 @@ "ledgerAccountRestriction": { "message": "Bạn cần sử dụng tài khoản gần đây nhất thì mới có thể thêm một tài khoản mới." }, - "ledgerLiveAdvancedSetting": { - "message": "Dùng Ledger Live" - }, - "ledgerLiveAdvancedSettingDescription": { - "message": "Cầu Ledger Live mới cho phép bạn dùng Ledger dễ dàng hơn. Chỉ có trong Chrome." - }, "ledgerLiveApp": { "message": "Ứng dụng Ledger Live" }, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 7f5d59ba6..41630cce3 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -5,6 +5,7 @@ import { ethers } from 'ethers'; import log from 'loglevel'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; +import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; import { NETWORK_EVENTS } from './network'; export default class PreferencesController { @@ -58,7 +59,9 @@ export default class PreferencesController { // ENS decentralized website resolution ipfsGateway: 'dweb.link', infuraBlocked: null, - useLedgerLive: false, + ledgerTransportType: window.navigator.hid + ? LEDGER_TRANSPORT_TYPES.WEBHID + : LEDGER_TRANSPORT_TYPES.U2F, ...opts.initState, }; @@ -516,21 +519,21 @@ export default class PreferencesController { } /** - * A setter for the `useLedgerLive` property - * @param {bool} useLedgerLive - Value for ledger live support - * @returns {Promise} A promise of the update to useLedgerLive + * A setter for the `useWebHid` property + * @param {string} ledgerTransportType - Either 'ledgerLive', 'webhid' or 'u2f' + * @returns {string} The transport type that was set. */ - async setLedgerLivePreference(useLedgerLive) { - this.store.updateState({ useLedgerLive }); - return useLedgerLive; + setLedgerTransportPreference(ledgerTransportType) { + this.store.updateState({ ledgerTransportType }); + return ledgerTransportType; } /** - * A getter for the `useLedgerLive` property - * @returns {boolean} User preference of using Ledger Live + * A getter for the `ledgerTransportType` property + * @returns {boolean} User preference of using WebHid to connect Ledger */ - getLedgerLivePreference() { - return this.store.getState().useLedgerLive; + getLedgerTransportPreference() { + return this.store.getState().ledgerTransportType; } /** diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f28c16bf3..e0813fcff 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -841,7 +841,10 @@ export default class MetamaskController extends EventEmitter { this.unlockHardwareWalletAccount, this, ), - setLedgerLivePreference: nodeify(this.setLedgerLivePreference, this), + setLedgerTransportPreference: nodeify( + this.setLedgerTransportPreference, + this, + ), // mobile fetchInfoToSync: nodeify(this.fetchInfoToSync, this), @@ -1480,9 +1483,9 @@ export default class MetamaskController extends EventEmitter { // keyring's iframe and have the setting initialized properly // Optimistically called to not block Metamask login due to // Ledger Keyring GitHub downtime - this.setLedgerLivePreference( - this.preferencesController.getLedgerLivePreference(), - ); + const transportPreference = this.preferencesController.getLedgerTransportPreference(); + + this.setLedgerTransportPreference(transportPreference); return this.keyringController.fullUpdate(); } @@ -2984,16 +2987,18 @@ export default class MetamaskController extends EventEmitter { * Sets the Ledger Live preference to use for Ledger hardware wallet support * @param {bool} bool - the value representing if the users wants to use Ledger Live */ - async setLedgerLivePreference(bool) { - const currentValue = this.preferencesController.getLedgerLivePreference(); - this.preferencesController.setLedgerLivePreference(bool); + async setLedgerTransportPreference(transportType) { + const currentValue = this.preferencesController.getLedgerTransportPreference(); + const newValue = this.preferencesController.setLedgerTransportPreference( + transportType, + ); const keyring = await this.getKeyringForDevice('ledger'); if (keyring?.updateTransportMethod) { - return keyring.updateTransportMethod(bool).catch((e) => { + return keyring.updateTransportMethod(newValue).catch((e) => { // If there was an error updating the transport, we should // fall back to the original value - this.preferencesController.setLedgerLivePreference(currentValue); + this.preferencesController.setLedgerTransportPreference(currentValue); throw e; }); } diff --git a/app/scripts/migrations/066.js b/app/scripts/migrations/066.js new file mode 100644 index 000000000..31c109ba7 --- /dev/null +++ b/app/scripts/migrations/066.js @@ -0,0 +1,37 @@ +import { cloneDeep } from 'lodash'; +import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; + +const version = 66; + +/** + * Changes the useLedgerLive boolean property to the ledgerTransportType enum + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + const newState = transformState(state); + versionedData.data = newState; + return versionedData; + }, +}; + +function transformState(state) { + const defaultTransportType = window.navigator.hid + ? LEDGER_TRANSPORT_TYPES.WEBHID + : LEDGER_TRANSPORT_TYPES.U2F; + const useLedgerLive = Boolean(state.PreferencesController?.useLedgerLive); + const newState = { + ...state, + PreferencesController: { + ...state?.PreferencesController, + ledgerTransportType: useLedgerLive + ? LEDGER_TRANSPORT_TYPES.LIVE + : defaultTransportType, + }, + }; + delete newState.PreferencesController.useLedgerLive; + return newState; +} diff --git a/app/scripts/migrations/066.test.js b/app/scripts/migrations/066.test.js new file mode 100644 index 000000000..24af0ba45 --- /dev/null +++ b/app/scripts/migrations/066.test.js @@ -0,0 +1,116 @@ +import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; +import migration66 from './066'; + +describe('migration #66', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 65, + }, + data: {}, + }; + + const newStorage = await migration66.migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version: 66, + }); + }); + + it('should set ledgerTransportType to `u2f` if no preferences controller exists and webhid is not available', async () => { + const oldStorage = { + meta: {}, + data: {}, + }; + + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); + }); + + it('should set ledgerTransportType to `u2f` if no useLedgerLive property exists and webhid is not available', async () => { + const oldStorage = { + meta: {}, + data: { + PreferencesController: {}, + }, + }; + + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); + }); + + it('should set ledgerTransportType to `u2f` if useLedgerLive is false and webhid is not available', async () => { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + useLedgerLive: false, + }, + }, + }; + + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); + }); + + it('should set ledgerTransportType to `webhid` if useLedgerLive is false and webhid is available', async () => { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + useLedgerLive: false, + }, + }, + }; + jest + .spyOn(window, 'navigator', 'get') + .mockImplementation(() => ({ hid: true })); + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual(LEDGER_TRANSPORT_TYPES.WEBHID); + }); + + it('should set ledgerTransportType to `ledgerLive` if useLedgerLive is true', async () => { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + useLedgerLive: true, + }, + }, + }; + + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual('ledgerLive'); + }); + + it('should not change ledgerTransportType if useLedgerLive is true and webhid is available', async () => { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + useLedgerLive: true, + }, + }, + }; + jest + .spyOn(window, 'navigator', 'get') + .mockImplementation(() => ({ hid: true })); + const newStorage = await migration66.migrate(oldStorage); + expect( + newStorage.data.PreferencesController.ledgerTransportType, + ).toStrictEqual(LEDGER_TRANSPORT_TYPES.LIVE); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 6ca02a881..7baf840e3 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -69,6 +69,7 @@ import m062 from './062'; import m063 from './063'; import m064 from './064'; import m065 from './065'; +import m066 from './066'; const migrations = [ m002, @@ -135,6 +136,7 @@ const migrations = [ m063, m064, m065, + m066, ]; export default migrations; diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 145e04c88..eeaea008d 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -111,7 +111,11 @@ export default class ExtensionPlatform { return version; } - openExtensionInBrowser(route = null, queryString = null) { + openExtensionInBrowser( + route = null, + queryString = null, + keepWindowOpen = false, + ) { let extensionURL = extension.runtime.getURL('home.html'); if (queryString) { @@ -122,7 +126,10 @@ export default class ExtensionPlatform { extensionURL += `#${route}`; } this.openTab({ url: extensionURL }); - if (getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND) { + if ( + getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND && + !keepWindowOpen + ) { window.close(); } } diff --git a/package.json b/package.json index 98b5ce7bf..c5789c618 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.28.0", "@metamask/controllers": "^17.0.0", - "@metamask/eth-ledger-bridge-keyring": "^0.7.0", + "@metamask/eth-ledger-bridge-keyring": "^0.9.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", "@metamask/jazzicon": "^2.0.0", diff --git a/shared/constants/hardware-wallets.js b/shared/constants/hardware-wallets.js index 880efb5b2..e780555fc 100644 --- a/shared/constants/hardware-wallets.js +++ b/shared/constants/hardware-wallets.js @@ -7,3 +7,20 @@ export const KEYRING_TYPES = { LEDGER: 'Ledger Hardware', TREZOR: 'Trezor Hardware', }; + +/** + * Used for setting the users preference for ledger transport type + */ +export const LEDGER_TRANSPORT_TYPES = { + LIVE: 'ledgerLive', + WEBHID: 'webhid', + U2F: 'u2f', +}; + +export const LEDGER_USB_VENDOR_ID = '0x2c97'; + +export const WEBHID_CONNECTED_STATUSES = { + CONNECTED: 'connected', + NOT_CONNECTED: 'notConnected', + UNKNOWN: 'unknown', +}; diff --git a/ui/components/app/ledger-instruction-field/index.js b/ui/components/app/ledger-instruction-field/index.js new file mode 100644 index 000000000..a4ae8131f --- /dev/null +++ b/ui/components/app/ledger-instruction-field/index.js @@ -0,0 +1 @@ +export { default } from './ledger-instruction-field'; diff --git a/ui/components/app/ledger-instruction-field/ledger-instruction-field.js b/ui/components/app/ledger-instruction-field/ledger-instruction-field.js new file mode 100644 index 000000000..960b6ca07 --- /dev/null +++ b/ui/components/app/ledger-instruction-field/ledger-instruction-field.js @@ -0,0 +1,153 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + LEDGER_TRANSPORT_TYPES, + LEDGER_USB_VENDOR_ID, + WEBHID_CONNECTED_STATUSES, +} from '../../../../shared/constants/hardware-wallets'; +import { + PLATFORM_FIREFOX, + ENVIRONMENT_TYPE_FULLSCREEN, +} from '../../../../shared/constants/app'; + +import { + setLedgerWebHidConnectedStatus, + getLedgerWebHidConnectedStatus, +} from '../../../ducks/app/app'; + +import Typography from '../../ui/typography/typography'; +import Button from '../../ui/button'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + COLORS, + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system'; +import Dialog from '../../ui/dialog'; +import { + getPlatform, + getEnvironmentType, +} from '../../../../app/scripts/lib/util'; +import { getLedgerTransportType } from '../../../ducks/metamask/metamask'; + +const renderInstructionStep = (text, show = true, color = COLORS.PRIMARY3) => { + return ( + show && ( + + {text} + + ) + ); +}; + +export default function LedgerInstructionField({ showDataInstruction }) { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const webHidConnectedStatus = useSelector(getLedgerWebHidConnectedStatus); + const ledgerTransportType = useSelector(getLedgerTransportType); + const environmentType = getEnvironmentType(); + const environmentTypeIsFullScreen = + environmentType === ENVIRONMENT_TYPE_FULLSCREEN; + + useEffect(() => { + const initialConnectedDeviceCheck = async () => { + if ( + ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID && + webHidConnectedStatus !== WEBHID_CONNECTED_STATUSES.CONNECTED + ) { + const devices = await window.navigator.hid.getDevices(); + const webHidIsConnected = devices.some( + (device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID), + ); + dispatch( + setLedgerWebHidConnectedStatus( + webHidIsConnected + ? WEBHID_CONNECTED_STATUSES.CONNECTED + : WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, + ), + ); + } + }; + initialConnectedDeviceCheck(); + }, [dispatch, ledgerTransportType, webHidConnectedStatus]); + + const usingLedgerLive = ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE; + const usingWebHID = ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID; + + const isFirefox = getPlatform() === PLATFORM_FIREFOX; + + return ( +
+
+ +
+ {renderInstructionStep(t('ledgerConnectionInstructionHeader'))} + {renderInstructionStep( + `- ${t('ledgerConnectionInstructionStepOne')}`, + !isFirefox && usingLedgerLive, + )} + {renderInstructionStep( + `- ${t('ledgerConnectionInstructionStepTwo')}`, + !isFirefox && usingLedgerLive, + )} + {renderInstructionStep( + `- ${t('ledgerConnectionInstructionStepThree')}`, + )} + {renderInstructionStep( + `- ${t('ledgerConnectionInstructionStepFour')}`, + showDataInstruction, + )} + {renderInstructionStep( + + + , + usingWebHID && + webHidConnectedStatus === + WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, + COLORS.SECONDARY1, + )} +
+
+
+
+ ); +} + +LedgerInstructionField.propTypes = { + showDataInstruction: PropTypes.bool, +}; diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index ec3baf497..a8233fc98 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { stripHexPrefix } from 'ethereumjs-util'; import classnames from 'classnames'; import { ObjectInspector } from 'react-inspector'; +import LedgerInstructionField from '../ledger-instruction-field'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -36,6 +37,8 @@ export default class SignatureRequestOriginal extends Component { sign: PropTypes.func.isRequired, txData: PropTypes.object.isRequired, domainMetadata: PropTypes.object, + hardwareWalletRequiresConnection: PropTypes.bool, + isLedgerWallet: PropTypes.bool, }; state = { @@ -286,6 +289,7 @@ export default class SignatureRequestOriginal extends Component { mostRecentOverviewPage, sign, txData: { type }, + hardwareWalletRequiresConnection, } = this.props; const { metricsEvent, t } = this.context; @@ -319,6 +323,7 @@ export default class SignatureRequestOriginal extends Component { type="primary" large className="request-signature__footer__sign-button" + disabled={hardwareWalletRequiresConnection} onClick={async (event) => { this._removeBeforeUnload(); await sign(event); @@ -347,6 +352,11 @@ export default class SignatureRequestOriginal extends Component {
{this.renderHeader()} {this.renderBody()} + {this.props.isLedgerWallet ? ( +
+ +
+ ) : null} {this.renderFooter()}
); diff --git a/ui/components/app/signature-request-original/signature-request-original.container.js b/ui/components/app/signature-request-original/signature-request-original.container.js index f6181cc5b..82f313bd4 100644 --- a/ui/components/app/signature-request-original/signature-request-original.container.js +++ b/ui/components/app/signature-request-original/signature-request-original.container.js @@ -8,18 +8,32 @@ import { accountsWithSendEtherInfoSelector, conversionRateSelector, getDomainMetadata, + doesAddressRequireLedgerHidConnection, } from '../../../selectors'; import { getAccountByAddress } from '../../../helpers/utils/util'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; +import { isAddressLedger } from '../../../ducks/metamask/metamask'; import SignatureRequestOriginal from './signature-request-original.component'; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { + const { + msgParams: { from }, + } = ownProps.txData; + + const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection( + state, + from, + ); + const isLedgerWallet = isAddressLedger(state, from); + return { requester: null, requesterAddress: null, conversionRate: conversionRateSelector(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), + hardwareWalletRequiresConnection, + isLedgerWallet, // not passed to component allAccounts: accountsWithSendEtherInfoSelector(state), domainMetadata: getDomainMetadata(state), diff --git a/ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js b/ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js index 0ca254079..37df2a177 100644 --- a/ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js +++ b/ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js @@ -6,6 +6,7 @@ export default class SignatureRequestFooter extends PureComponent { static propTypes = { cancelAction: PropTypes.func.isRequired, signAction: PropTypes.func.isRequired, + disabled: PropTypes.boolean, }; static contextTypes = { @@ -13,13 +14,13 @@ export default class SignatureRequestFooter extends PureComponent { }; render() { - const { cancelAction, signAction } = this.props; + const { cancelAction, signAction, disabled = false } = this.props; return (
-
diff --git a/ui/components/app/signature-request/signature-request.component.js b/ui/components/app/signature-request/signature-request.component.js index 3f460b4c2..73d28ec26 100644 --- a/ui/components/app/signature-request/signature-request.component.js +++ b/ui/components/app/signature-request/signature-request.component.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import Identicon from '../../ui/identicon'; +import LedgerInstructionField from '../ledger-instruction-field'; import Header from './signature-request-header'; import Footer from './signature-request-footer'; import Message from './signature-request-message'; @@ -15,10 +16,11 @@ export default class SignatureRequest extends PureComponent { balance: PropTypes.string, name: PropTypes.string, }).isRequired, - + isLedgerWallet: PropTypes.bool, clearConfirmTransaction: PropTypes.func.isRequired, cancel: PropTypes.func.isRequired, sign: PropTypes.func.isRequired, + hardwareWalletRequiresConnection: PropTypes.func.isRequired, }; static contextTypes = { @@ -69,6 +71,8 @@ export default class SignatureRequest extends PureComponent { }, cancel, sign, + isLedgerWallet, + hardwareWalletRequiresConnection, } = this.props; const { address: fromAddress } = fromAccount; const { message, domain = {} } = JSON.parse(data); @@ -128,8 +132,17 @@ export default class SignatureRequest extends PureComponent { {this.formatWallet(fromAddress)} + {isLedgerWallet ? ( +
+ +
+ ) : null} -