Merge remote-tracking branch 'origin/develop' into master-sync

feature/default_network_editable
ryanml 3 years ago
commit 48f4e9845b
  1. 1
      .eslintrc.js
  2. 13
      README.md
  3. 113
      app/_locales/en/messages.json
  4. 6
      app/_locales/es/messages.json
  5. 6
      app/_locales/es_419/messages.json
  6. 6
      app/_locales/hi/messages.json
  7. 6
      app/_locales/id/messages.json
  8. 6
      app/_locales/ja/messages.json
  9. 6
      app/_locales/ko/messages.json
  10. 6
      app/_locales/ph/messages.json
  11. 6
      app/_locales/pt_BR/messages.json
  12. 6
      app/_locales/ru/messages.json
  13. 6
      app/_locales/vi/messages.json
  14. 1
      app/images/onboarding-pin-browser.svg
  15. 25
      app/scripts/controllers/preferences.js
  16. 27
      app/scripts/controllers/transactions/index.js
  17. 18
      app/scripts/lib/get-first-preferred-lang-code.js
  18. 23
      app/scripts/metamask-controller.js
  19. 37
      app/scripts/migrations/066.js
  20. 116
      app/scripts/migrations/066.test.js
  21. 2
      app/scripts/migrations/index.js
  22. 11
      app/scripts/platforms/extension.js
  23. 2
      package.json
  24. 4
      shared/constants/gas.js
  25. 17
      shared/constants/hardware-wallets.js
  26. 12
      shared/modules/contract-utils.js
  27. 30
      shared/modules/contract-utils.test.js
  28. 5
      ui/components/app/account-list-item/account-list-item.js
  29. 4
      ui/components/app/account-menu/account-menu.component.js
  30. 6
      ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
  31. 2
      ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
  32. 6
      ui/components/app/contact-list/contact-list.component.js
  33. 1
      ui/components/app/ledger-instruction-field/index.js
  34. 153
      ui/components/app/ledger-instruction-field/ledger-instruction-field.js
  35. 2
      ui/components/app/modal/modal-content/modal-content.component.js
  36. 10
      ui/components/app/signature-request-original/signature-request-original.component.js
  37. 16
      ui/components/app/signature-request-original/signature-request-original.container.js
  38. 5
      ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js
  39. 2
      ui/components/app/signature-request/signature-request-header/signature-request-header.component.js
  40. 17
      ui/components/app/signature-request/signature-request.component.js
  41. 28
      ui/components/app/signature-request/signature-request.container.js
  42. 1
      ui/components/app/step-progress-bar/index.scss
  43. 2
      ui/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js
  44. 2
      ui/components/ui/actionable-message/actionable-message.js
  45. 2
      ui/components/ui/button/button.component.js
  46. 4
      ui/components/ui/chip/chip.js
  47. 2
      ui/components/ui/icon-with-label/icon-with-label.js
  48. 16
      ui/components/ui/list-item/list-item.component.js
  49. 6
      ui/components/ui/page-container/page-container-header/page-container-header.component.js
  50. 8
      ui/components/ui/qr-code/qr-code.js
  51. 2
      ui/components/ui/unit-input/unit-input.component.js
  52. 16
      ui/ducks/app/app.js
  53. 60
      ui/ducks/metamask/metamask.js
  54. 12
      ui/ducks/send/send.js
  55. 2
      ui/helpers/constants/routes.js
  56. 5
      ui/helpers/higher-order-components/authenticated/authenticated.component.js
  57. 7
      ui/helpers/utils/transactions.util.js
  58. 13
      ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
  59. 5
      ui/pages/confirm-approve/confirm-approve-content/index.scss
  60. 17
      ui/pages/confirm-approve/confirm-approve.js
  61. 63
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  62. 30
      ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
  63. 2
      ui/pages/create-account/connect-hardware/account-list.js
  64. 17
      ui/pages/create-account/connect-hardware/index.js
  65. 7
      ui/pages/create-account/connect-hardware/select-hardware.js
  66. 5
      ui/pages/create-account/connect-hardware/select-hardware.stories.js
  67. 4
      ui/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
  68. 10
      ui/pages/keychains/index.scss
  69. 3
      ui/pages/keychains/restore-vault.js
  70. 2
      ui/pages/onboarding-flow/index.scss
  71. 41
      ui/pages/onboarding-flow/metametrics/index.scss
  72. 152
      ui/pages/onboarding-flow/metametrics/metametrics.js
  73. 9
      ui/pages/onboarding-flow/metametrics/metametrics.stories.js
  74. 19
      ui/pages/onboarding-flow/onboarding-flow.js
  75. 23
      ui/pages/onboarding-flow/pin-extension/index.scss
  76. 597
      ui/pages/onboarding-flow/pin-extension/pin-billboard.js
  77. 75
      ui/pages/onboarding-flow/pin-extension/pin-extension.js
  78. 9
      ui/pages/onboarding-flow/pin-extension/pin-extension.stories.js
  79. 2
      ui/pages/onboarding-flow/privacy-settings/privacy-settings.js
  80. 12
      ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js
  81. 6
      ui/pages/onboarding-flow/welcome/welcome.js
  82. 4
      ui/pages/routes/routes.component.js
  83. 19
      ui/pages/send/send-content/send-content.component.js
  84. 4
      ui/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js
  85. 83
      ui/pages/settings/advanced-tab/advanced-tab.component.js
  86. 5
      ui/pages/settings/advanced-tab/advanced-tab.component.test.js
  87. 8
      ui/pages/settings/advanced-tab/advanced-tab.container.js
  88. 4
      ui/pages/settings/contact-list-tab/contact-list-tab.component.js
  89. 35
      ui/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js
  90. 3
      ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js
  91. 7
      ui/pages/settings/index.scss
  92. 2
      ui/pages/settings/networks-tab/networks-tab.component.js
  93. 2
      ui/pages/settings/security-tab/security-tab.component.js
  94. 2
      ui/pages/settings/settings-tab/settings-tab.component.js
  95. 2
      ui/pages/swaps/awaiting-swap/awaiting-swap.js
  96. 55
      ui/pages/swaps/searchable-item-list/item-list/item-list.component.js
  97. 37
      ui/selectors/selectors.js
  98. 4
      ui/store/actionConstants.js
  99. 26
      ui/store/actions.js
  100. 8
      yarn.lock

@ -33,6 +33,7 @@ module.exports = {
'nyc_output/**', 'nyc_output/**',
'.vscode/**', '.vscode/**',
'lavamoat/*/policy.json', 'lavamoat/*/policy.json',
'storybook-build/**',
], ],
extends: [ extends: [

@ -75,19 +75,6 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
[![Architecture Diagram](./docs/architecture.png)][1] [![Architecture Diagram](./docs/architecture.png)][1]
## Development
```bash
yarn
yarn start
```
## Build for Publishing
```bash
yarn dist
```
## Other Docs ## Other Docs
- [How to add custom build to Chrome](./docs/add-to-chrome.md) - [How to add custom build to Chrome](./docs/add-to-chrome.md)

@ -345,6 +345,10 @@
"chromeRequiredForHardwareWallets": { "chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." "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": { "clickToRevealSeed": {
"message": "Click here to reveal secret words" "message": "Click here to reveal secret words"
}, },
@ -1251,36 +1255,42 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one." "message": "You need to make use your last account before you can add a new one."
}, },
"ledgerLiveAdvancedSetting": { "ledgerConnectionInstructionHeader": {
"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": {
"message": "Prior to clicking confirm:" "message": "Prior to clicking confirm:"
}, },
"ledgerLiveDialogStepFour": { "ledgerConnectionInstructionStepFour": {
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device" "message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
}, },
"ledgerLiveDialogStepOne": { "ledgerConnectionInstructionStepOne": {
"message": "Enable Use Ledger Live under Settings > Advanced" "message": "Enable Use Ledger Live under Settings > Advanced"
}, },
"ledgerLiveDialogStepThree": { "ledgerConnectionInstructionStepThree": {
"message": "Plug in your Ledger device and select the Ethereum app" "message": "Plug in your Ledger device and select the Ethereum app"
}, },
"ledgerLiveDialogStepTwo": { "ledgerConnectionInstructionStepTwo": {
"message": "Open and unlock Ledger Live App" "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": { "ledgerLocked": {
"message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened." "message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened."
}, },
"ledgerTimeout": { "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." "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": { "letsGoSetUp": {
"message": "Yes, let’s get set up!" "message": "Yes, let’s get set up!"
}, },
@ -1357,6 +1367,9 @@
"metametricsCommitmentsAllowOptOut": { "metametricsCommitmentsAllowOptOut": {
"message": "Always allow you to opt-out via Settings" "message": "Always allow you to opt-out via Settings"
}, },
"metametricsCommitmentsAllowOptOut2": {
"message": "Always be able to opt-out via Settings"
},
"metametricsCommitmentsBoldNever": { "metametricsCommitmentsBoldNever": {
"message": "Never", "message": "Never",
"description": "This string is localized separately from some of the commitments so that we can bold it" "description": "This string is localized separately from some of the commitments so that we can bold it"
@ -1364,6 +1377,9 @@
"metametricsCommitmentsIntro": { "metametricsCommitmentsIntro": {
"message": "MetaMask will.." "message": "MetaMask will.."
}, },
"metametricsCommitmentsNeverCollect": {
"message": "Never collect keys, addresses, transactions, balances, hashes, or any personal information"
},
"metametricsCommitmentsNeverCollectIP": { "metametricsCommitmentsNeverCollectIP": {
"message": "$1 collect your full IP address", "message": "$1 collect your full IP address",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
@ -1372,6 +1388,12 @@
"message": "$1 collect keys, addresses, transactions, balances, hashes, or any personal information", "message": "$1 collect keys, addresses, transactions, balances, hashes, or any personal information",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
}, },
"metametricsCommitmentsNeverIP": {
"message": "Never collect your full IP address"
},
"metametricsCommitmentsNeverSell": {
"message": "Never sell data for profit. Ever!"
},
"metametricsCommitmentsNeverSellDataForProfit": { "metametricsCommitmentsNeverSellDataForProfit": {
"message": "$1 sell data for profit. Ever!", "message": "$1 sell data for profit. Ever!",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
@ -1380,11 +1402,17 @@
"message": "Send anonymized click & pageview events" "message": "Send anonymized click & pageview events"
}, },
"metametricsHelpImproveMetaMask": { "metametricsHelpImproveMetaMask": {
"message": "Help Us Improve MetaMask" "message": "Help us improve MetaMask"
}, },
"metametricsOptInDescription": { "metametricsOptInDescription": {
"message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem." "message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem."
}, },
"metametricsOptInDescription2": {
"message": "We would like to gather basic usage data to improve the usability of our product. These metrics will..."
},
"metametricsTitle": {
"message": "Join 6M+ users to improve MetaMask"
},
"mismatchedChain": { "mismatchedChain": {
"message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.", "message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.",
"description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key" "description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key"
@ -1638,6 +1666,42 @@
"onboardingImportWallet": { "onboardingImportWallet": {
"message": "Import an existing wallet" "message": "Import an existing wallet"
}, },
"onboardingPinExtensionBillboardAccess": {
"message": "Full Access"
},
"onboardingPinExtensionBillboardDescription": {
"message": "These extensions can see and change information"
},
"onboardingPinExtensionBillboardDescription2": {
"message": "on this site."
},
"onboardingPinExtensionBillboardTitle": {
"message": "Extensions"
},
"onboardingPinExtensionChrome": {
"message": "Click the browser extension icon"
},
"onboardingPinExtensionDescription": {
"message": "Pin MetaMask on your browser so it's accessible and easy to view transaction confirmations."
},
"onboardingPinExtensionDescription2": {
"message": "You can open MetaMask by clicking on the extension and access your wallet with 1 click."
},
"onboardingPinExtensionDescription3": {
"message": "Click browser extension icon to access it instantly"
},
"onboardingPinExtensionLabel": {
"message": "Pin MetaMask"
},
"onboardingPinExtensionStep1": {
"message": "1"
},
"onboardingPinExtensionStep2": {
"message": "2"
},
"onboardingPinExtensionTitle": {
"message": "Your MetaMask install is complete!"
},
"onboardingReturnNotice": { "onboardingReturnNotice": {
"message": "\"$1\" will close this tab and direct back to $2", "message": "\"$1\" will close this tab and direct back to $2",
"description": "Return the user to the site that initiated onboarding" "description": "Return the user to the site that initiated onboarding"
@ -1656,6 +1720,10 @@
"onlyConnectTrust": { "onlyConnectTrust": {
"message": "Only connect with sites you trust." "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": { "optional": {
"message": "Optional" "message": "Optional"
}, },
@ -1718,6 +1786,10 @@
"message": "+ $1 more", "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" "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": { "prev": {
"message": "Prev" "message": "Prev"
}, },
@ -1915,6 +1987,9 @@
"secretPhrase": { "secretPhrase": {
"message": "Only the first account on this wallet will auto load. After completing this process, to add additional accounts, click the drop down menu, then select Create Account." "message": "Only the first account on this wallet will auto load. After completing this process, to add additional accounts, click the drop down menu, then select Create Account."
}, },
"secretPhraseWarning": {
"message": "If you restore using another Secret Recovery Phrase, your current wallet, accounts and assets will be removed from this app permanently. This action cannot be undone."
},
"secretRecoveryPhrase": { "secretRecoveryPhrase": {
"message": "Secret Recovery Phrase" "message": "Secret Recovery Phrase"
}, },
@ -2760,6 +2835,10 @@
"typePassword": { "typePassword": {
"message": "Type your MetaMask password" "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": { "unapproved": {
"message": "Unapproved" "message": "Unapproved"
}, },
@ -2901,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.", "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." "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": { "welcome": {
"message": "Welcome to MetaMask" "message": "Welcome to MetaMask"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva." "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": { "ledgerLiveApp": {
"message": "Aplicación de Ledger Live" "message": "Aplicación de Ledger Live"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva." "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": { "ledgerLiveApp": {
"message": "Aplicación de Ledger Live" "message": "Aplicación de Ledger Live"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "नय पहल आपक अपनिम ख उपयग करन।" "message": "नय पहल आपक अपनिम ख उपयग करन।"
}, },
"ledgerLiveAdvancedSetting": {
"message": "Ledger Live क उपयग कर"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "नय Ledger Live बिज आपक अपनजर क अधिक आस उपयग करन अनमति। कवल Chrome म उपलबध ह।"
},
"ledgerLiveApp": { "ledgerLiveApp": {
"message": "Ledger Live ऐप" "message": "Ledger Live ऐप"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Anda perlu memanfaatkan akun terakhir Anda sebelum menambahkan yang baru." "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": { "ledgerLiveApp": {
"message": "Aplikasi Ledger Live" "message": "Aplikasi Ledger Live"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "新しいアカウントを追加するには、その前に最後のアカウントを使用する必要があります。" "message": "新しいアカウントを追加するには、その前に最後のアカウントを使用する必要があります。"
}, },
"ledgerLiveAdvancedSetting": {
"message": "レジャー ライブを使用"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "新しいレジャー ライブのブリッジを使用すると、レジャーをより簡単に使用できます。Chrome でのみ利用可能。"
},
"ledgerLiveApp": { "ledgerLiveApp": {
"message": "レジャー ライブのアプリ" "message": "レジャー ライブのアプリ"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "새 계정을 추가하려면 먼저 마지막 계정을 사용해야 합니다." "message": "새 계정을 추가하려면 먼저 마지막 계정을 사용해야 합니다."
}, },
"ledgerLiveAdvancedSetting": {
"message": "Ledger Live 사용하기"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능합니다."
},
"ledgerLiveApp": { "ledgerLiveApp": {
"message": "Ledger Live 앱" "message": "Ledger Live 앱"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Kailangan mong gamitin ang huli mong account bago ka magdagdag ng panibago." "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": { "ledgerLiveApp": {
"message": "Ledger Live App" "message": "Ledger Live App"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Você precisa usar sua última conta antes de adicionar uma nova." "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": { "ledgerLiveApp": {
"message": "Aplicativo Ledger Live" "message": "Aplicativo Ledger Live"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Вам необходимо использовать свой последний счет, прежде чем вы сможете добавить новый." "message": "Вам необходимо использовать свой последний счет, прежде чем вы сможете добавить новый."
}, },
"ledgerLiveAdvancedSetting": {
"message": "Использовать Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "Новое решение Ledger Live Bridge упрощает использование Ledger. Доступно только в Chrome."
},
"ledgerLiveApp": { "ledgerLiveApp": {
"message": "Приложение Ledger Live" "message": "Приложение Ledger Live"
}, },

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "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." "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": { "ledgerLiveApp": {
"message": "Ứng dụng Ledger Live" "message": "Ứng dụng Ledger Live"
}, },

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1022 KiB

@ -5,6 +5,7 @@ import { ethers } from 'ethers';
import log from 'loglevel'; import log from 'loglevel';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
import { NETWORK_EVENTS } from './network'; import { NETWORK_EVENTS } from './network';
export default class PreferencesController { export default class PreferencesController {
@ -58,7 +59,9 @@ export default class PreferencesController {
// ENS decentralized website resolution // ENS decentralized website resolution
ipfsGateway: 'dweb.link', ipfsGateway: 'dweb.link',
infuraBlocked: null, infuraBlocked: null,
useLedgerLive: false, ledgerTransportType: window.navigator.hid
? LEDGER_TRANSPORT_TYPES.WEBHID
: LEDGER_TRANSPORT_TYPES.U2F,
...opts.initState, ...opts.initState,
}; };
@ -516,21 +519,21 @@ export default class PreferencesController {
} }
/** /**
* A setter for the `useLedgerLive` property * A setter for the `useWebHid` property
* @param {bool} useLedgerLive - Value for ledger live support * @param {string} ledgerTransportType - Either 'ledgerLive', 'webhid' or 'u2f'
* @returns {Promise<string>} A promise of the update to useLedgerLive * @returns {string} The transport type that was set.
*/ */
async setLedgerLivePreference(useLedgerLive) { setLedgerTransportPreference(ledgerTransportType) {
this.store.updateState({ useLedgerLive }); this.store.updateState({ ledgerTransportType });
return useLedgerLive; return ledgerTransportType;
} }
/** /**
* A getter for the `useLedgerLive` property * A getter for the `ledgerTransportType` property
* @returns {boolean} User preference of using Ledger Live * @returns {boolean} User preference of using WebHid to connect Ledger
*/ */
getLedgerLivePreference() { getLedgerTransportPreference() {
return this.store.getState().useLedgerLive; return this.store.getState().ledgerTransportType;
} }
/** /**

@ -39,6 +39,7 @@ import {
CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP,
} from '../../../../shared/constants/network'; } from '../../../../shared/constants/network';
import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils';
import { readAddressAsContract } from '../../../../shared/modules/contract-utils';
import TransactionStateManager from './tx-state-manager'; import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils'; import TxGasUtil from './tx-gas-utils';
import PendingTransactionTracker from './pending-tx-tracker'; import PendingTransactionTracker from './pending-tx-tracker';
@ -1234,23 +1235,21 @@ export default class TransactionController extends EventEmitter {
result = TRANSACTION_TYPES.DEPLOY_CONTRACT; result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
} }
let code; let contractCode;
if (!result) {
try {
code = await this.query.getCode(to);
} catch (e) {
code = null;
log.warn(e);
}
const codeIsEmpty = !code || code === '0x' || code === '0x0'; if (!result) {
const {
result = codeIsEmpty contractCode: resultCode,
? TRANSACTION_TYPES.SIMPLE_SEND isContractAddress,
: TRANSACTION_TYPES.CONTRACT_INTERACTION; } = await readAddressAsContract(this.query, to);
contractCode = resultCode;
result = isContractAddress
? TRANSACTION_TYPES.CONTRACT_INTERACTION
: TRANSACTION_TYPES.SIMPLE_SEND;
} }
return { type: result, getCodeResponse: code }; return { type: result, getCodeResponse: contractCode };
} }
/** /**

@ -38,9 +38,23 @@ export default async function getFirstPreferredLangCode() {
userPreferredLocaleCodes = []; userPreferredLocaleCodes = [];
} }
const firstPreferredLangCode = userPreferredLocaleCodes let firstPreferredLangCode = userPreferredLocaleCodes
.map((code) => code.toLowerCase().replace('_', '-')) .map((code) => code.toLowerCase().replace('_', '-'))
.find((code) => existingLocaleCodes[code] !== undefined); .find(
(code) =>
existingLocaleCodes[code] !== undefined ||
existingLocaleCodes[code.split('-')[0]] !== undefined,
);
// if we have matched against a code with a '-' present, meaning its a regional
// code for which we have a non-regioned locale, we need to set firstPreferredLangCode
// to the correct non-regional code.
if (
firstPreferredLangCode !== undefined &&
existingLocaleCodes[firstPreferredLangCode] === undefined
) {
firstPreferredLangCode = firstPreferredLangCode.split('-')[0];
}
return existingLocaleCodes[firstPreferredLangCode] || 'en'; return existingLocaleCodes[firstPreferredLangCode] || 'en';
} }

@ -841,7 +841,10 @@ export default class MetamaskController extends EventEmitter {
this.unlockHardwareWalletAccount, this.unlockHardwareWalletAccount,
this, this,
), ),
setLedgerLivePreference: nodeify(this.setLedgerLivePreference, this), setLedgerTransportPreference: nodeify(
this.setLedgerTransportPreference,
this,
),
// mobile // mobile
fetchInfoToSync: nodeify(this.fetchInfoToSync, this), fetchInfoToSync: nodeify(this.fetchInfoToSync, this),
@ -1480,9 +1483,9 @@ export default class MetamaskController extends EventEmitter {
// keyring's iframe and have the setting initialized properly // keyring's iframe and have the setting initialized properly
// Optimistically called to not block Metamask login due to // Optimistically called to not block Metamask login due to
// Ledger Keyring GitHub downtime // Ledger Keyring GitHub downtime
this.setLedgerLivePreference( const transportPreference = this.preferencesController.getLedgerTransportPreference();
this.preferencesController.getLedgerLivePreference(),
); this.setLedgerTransportPreference(transportPreference);
return this.keyringController.fullUpdate(); 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 * 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 * @param {bool} bool - the value representing if the users wants to use Ledger Live
*/ */
async setLedgerLivePreference(bool) { async setLedgerTransportPreference(transportType) {
const currentValue = this.preferencesController.getLedgerLivePreference(); const currentValue = this.preferencesController.getLedgerTransportPreference();
this.preferencesController.setLedgerLivePreference(bool); const newValue = this.preferencesController.setLedgerTransportPreference(
transportType,
);
const keyring = await this.getKeyringForDevice('ledger'); const keyring = await this.getKeyringForDevice('ledger');
if (keyring?.updateTransportMethod) { 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 // If there was an error updating the transport, we should
// fall back to the original value // fall back to the original value
this.preferencesController.setLedgerLivePreference(currentValue); this.preferencesController.setLedgerTransportPreference(currentValue);
throw e; throw e;
}); });
} }

@ -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;
}

@ -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);
});
});

@ -69,6 +69,7 @@ import m062 from './062';
import m063 from './063'; import m063 from './063';
import m064 from './064'; import m064 from './064';
import m065 from './065'; import m065 from './065';
import m066 from './066';
const migrations = [ const migrations = [
m002, m002,
@ -135,6 +136,7 @@ const migrations = [
m063, m063,
m064, m064,
m065, m065,
m066,
]; ];
export default migrations; export default migrations;

@ -111,7 +111,11 @@ export default class ExtensionPlatform {
return version; return version;
} }
openExtensionInBrowser(route = null, queryString = null) { openExtensionInBrowser(
route = null,
queryString = null,
keepWindowOpen = false,
) {
let extensionURL = extension.runtime.getURL('home.html'); let extensionURL = extension.runtime.getURL('home.html');
if (queryString) { if (queryString) {
@ -122,7 +126,10 @@ export default class ExtensionPlatform {
extensionURL += `#${route}`; extensionURL += `#${route}`;
} }
this.openTab({ url: extensionURL }); this.openTab({ url: extensionURL });
if (getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND) { if (
getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND &&
!keepWindowOpen
) {
window.close(); window.close();
} }
} }

@ -105,7 +105,7 @@
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.28.0", "@metamask/contract-metadata": "^1.28.0",
"@metamask/controllers": "^17.0.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/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",

@ -1,11 +1,11 @@
import { addHexPrefix } from 'ethereumjs-util'; import { addHexPrefix } from 'ethereumjs-util';
import { MIN_GAS_LIMIT_HEX } from '../../ui/pages/send/send.constants';
const TWENTY_ONE_THOUSAND = 21000;
const ONE_HUNDRED_THOUSAND = 100000; const ONE_HUNDRED_THOUSAND = 100000;
export const GAS_LIMITS = { export const GAS_LIMITS = {
// maximum gasLimit of a simple send // maximum gasLimit of a simple send
SIMPLE: addHexPrefix(TWENTY_ONE_THOUSAND.toString(16)), SIMPLE: addHexPrefix(MIN_GAS_LIMIT_HEX),
// a base estimate for token transfers. // a base estimate for token transfers.
BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)), BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)),
}; };

@ -7,3 +7,20 @@ export const KEYRING_TYPES = {
LEDGER: 'Ledger Hardware', LEDGER: 'Ledger Hardware',
TREZOR: 'Trezor 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',
};

@ -0,0 +1,12 @@
export const readAddressAsContract = async (ethQuery, address) => {
let contractCode;
try {
contractCode = await ethQuery.getCode(address);
} catch (e) {
contractCode = null;
}
const isContractAddress =
contractCode && contractCode !== '0x' && contractCode !== '0x0';
return { contractCode, isContractAddress };
};

@ -0,0 +1,30 @@
const { readAddressAsContract } = require('./contract-utils');
describe('Contract Utils', () => {
it('checks is an address is a contract address or not', async () => {
let mockEthQuery = {
getCode: () => {
return '0xa';
},
};
const { isContractAddress } = await readAddressAsContract(
mockEthQuery,
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84',
);
expect(isContractAddress).toStrictEqual(true);
mockEthQuery = {
getCode: () => {
return '0x';
},
};
const {
isContractAddress: isNotContractAddress,
} = await readAddressAsContract(
mockEthQuery,
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84',
);
expect(isNotContractAddress).toStrictEqual(false);
});
});

@ -24,11 +24,8 @@ export default function AccountListItem({
className="account-list-item__identicon" className="account-list-item__identicon"
diameter={18} diameter={18}
/> />
<div className="account-list-item__account-name">{name || address}</div> <div className="account-list-item__account-name">{name || address}</div>
{icon ? <div className="account-list-item__icon">{icon}</div> : null}
{icon && <div className="account-list-item__icon">{icon}</div>}
<AccountMismatchWarning address={address} /> <AccountMismatchWarning address={address} />
</div> </div>

@ -196,7 +196,9 @@ export default class AccountMenu extends Component {
key={identity.address} key={identity.address}
> >
<div className="account-menu__check-mark"> <div className="account-menu__check-mark">
{isSelected && <div className="account-menu__check-mark-icon" />} {isSelected ? (
<div className="account-menu__check-mark-icon" />
) : null}
</div> </div>
<Identicon address={identity.address} diameter={24} /> <Identicon address={identity.address} diameter={24} />
<div className="account-menu__account-info"> <div className="account-menu__account-info">

@ -93,7 +93,7 @@ export default class ConfirmPageContainerContent extends Component {
return ( return (
<div className="confirm-page-container-content"> <div className="confirm-page-container-content">
{warning && <ConfirmPageContainerWarning warning={warning} />} {warning ? <ConfirmPageContainerWarning warning={warning} /> : null}
{ethGasPriceWarning && ( {ethGasPriceWarning && (
<ConfirmPageContainerWarning warning={ethGasPriceWarning} /> <ConfirmPageContainerWarning warning={ethGasPriceWarning} />
)} )}
@ -124,7 +124,9 @@ export default class ConfirmPageContainerContent extends Component {
submitText={submitText} submitText={submitText}
disabled={disabled} disabled={disabled}
> >
{unapprovedTxCount > 1 && <a onClick={onCancelAll}>{rejectNText}</a>} {unapprovedTxCount > 1 ? (
<a onClick={onCancelAll}>{rejectNText}</a>
) : null}
</PageContainerFooter> </PageContainerFooter>
</div> </div>
); );

@ -56,7 +56,7 @@ export default function ConfirmPageContainerHeader({
</span> </span>
</div> </div>
)} )}
{!isFullScreen && <NetworkDisplay />} {isFullScreen ? null : <NetworkDisplay />}
</div> </div>
{children} {children}
</div> </div>

@ -106,9 +106,9 @@ export default class ContactList extends PureComponent {
return ( return (
<div className="send__select-recipient-wrapper__list"> <div className="send__select-recipient-wrapper__list">
{children || null} {children || null}
{searchForRecents && this.renderRecents()} {searchForRecents ? this.renderRecents() : null}
{searchForContacts && this.renderAddressBook()} {searchForContacts ? this.renderAddressBook() : null}
{searchForMyAccounts && this.renderMyAccounts()} {searchForMyAccounts ? this.renderMyAccounts() : null}
</div> </div>
); );
} }

@ -0,0 +1 @@
export { default } from './ledger-instruction-field';

@ -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 && (
<Typography
boxProps={{ margin: 0 }}
color={color}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H7}
>
{text}
</Typography>
)
);
};
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 (
<div>
<div className="confirm-detail-row">
<Dialog type="message">
<div className="ledger-live-dialog">
{renderInstructionStep(t('ledgerConnectionInstructionHeader'))}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepOne')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepTwo')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepThree')}`,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepFour')}`,
showDataInstruction,
)}
{renderInstructionStep(
<span>
<Button
type="link"
onClick={async () => {
if (environmentTypeIsFullScreen) {
const connectedDevices = await window.navigator.hid.requestDevice(
{
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
},
);
const webHidIsConnected = connectedDevices.some(
(device) =>
device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
dispatch(
setLedgerWebHidConnectedStatus({
webHidConnectedStatus: webHidIsConnected
? WEBHID_CONNECTED_STATUSES.CONNECTED
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED,
}),
);
} else {
global.platform.openExtensionInBrowser(null, null, true);
}
}}
>
{environmentTypeIsFullScreen
? t('clickToConnectLedgerViaWebHID')
: t('openFullScreenForLedgerWebHid')}
</Button>
</span>,
usingWebHID &&
webHidConnectedStatus ===
WEBHID_CONNECTED_STATUSES.NOT_CONNECTED,
COLORS.SECONDARY1,
)}
</div>
</Dialog>
</div>
</div>
);
}
LedgerInstructionField.propTypes = {
showDataInstruction: PropTypes.bool,
};

@ -12,7 +12,7 @@ export default class ModalContent extends PureComponent {
return ( return (
<div className="modal-content"> <div className="modal-content">
{title && <div className="modal-content__title">{title}</div>} {title ? <div className="modal-content__title">{title}</div> : null}
{description && ( {description && (
<div className="modal-content__description">{description}</div> <div className="modal-content__description">{description}</div>
)} )}

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { stripHexPrefix } from 'ethereumjs-util'; import { stripHexPrefix } from 'ethereumjs-util';
import classnames from 'classnames'; import classnames from 'classnames';
import { ObjectInspector } from 'react-inspector'; import { ObjectInspector } from 'react-inspector';
import LedgerInstructionField from '../ledger-instruction-field';
import { import {
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
@ -36,6 +37,8 @@ export default class SignatureRequestOriginal extends Component {
sign: PropTypes.func.isRequired, sign: PropTypes.func.isRequired,
txData: PropTypes.object.isRequired, txData: PropTypes.object.isRequired,
domainMetadata: PropTypes.object, domainMetadata: PropTypes.object,
hardwareWalletRequiresConnection: PropTypes.bool,
isLedgerWallet: PropTypes.bool,
}; };
state = { state = {
@ -286,6 +289,7 @@ export default class SignatureRequestOriginal extends Component {
mostRecentOverviewPage, mostRecentOverviewPage,
sign, sign,
txData: { type }, txData: { type },
hardwareWalletRequiresConnection,
} = this.props; } = this.props;
const { metricsEvent, t } = this.context; const { metricsEvent, t } = this.context;
@ -319,6 +323,7 @@ export default class SignatureRequestOriginal extends Component {
type="primary" type="primary"
large large
className="request-signature__footer__sign-button" className="request-signature__footer__sign-button"
disabled={hardwareWalletRequiresConnection}
onClick={async (event) => { onClick={async (event) => {
this._removeBeforeUnload(); this._removeBeforeUnload();
await sign(event); await sign(event);
@ -347,6 +352,11 @@ export default class SignatureRequestOriginal extends Component {
<div className="request-signature__container"> <div className="request-signature__container">
{this.renderHeader()} {this.renderHeader()}
{this.renderBody()} {this.renderBody()}
{this.props.isLedgerWallet ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
) : null}
{this.renderFooter()} {this.renderFooter()}
</div> </div>
); );

@ -8,18 +8,32 @@ import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
conversionRateSelector, conversionRateSelector,
getDomainMetadata, getDomainMetadata,
doesAddressRequireLedgerHidConnection,
} from '../../../selectors'; } from '../../../selectors';
import { getAccountByAddress } from '../../../helpers/utils/util'; import { getAccountByAddress } from '../../../helpers/utils/util';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import SignatureRequestOriginal from './signature-request-original.component'; 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 { return {
requester: null, requester: null,
requesterAddress: null, requesterAddress: null,
conversionRate: conversionRateSelector(state), conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
hardwareWalletRequiresConnection,
isLedgerWallet,
// not passed to component // not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state), allAccounts: accountsWithSendEtherInfoSelector(state),
domainMetadata: getDomainMetadata(state), domainMetadata: getDomainMetadata(state),

@ -6,6 +6,7 @@ export default class SignatureRequestFooter extends PureComponent {
static propTypes = { static propTypes = {
cancelAction: PropTypes.func.isRequired, cancelAction: PropTypes.func.isRequired,
signAction: PropTypes.func.isRequired, signAction: PropTypes.func.isRequired,
disabled: PropTypes.boolean,
}; };
static contextTypes = { static contextTypes = {
@ -13,13 +14,13 @@ export default class SignatureRequestFooter extends PureComponent {
}; };
render() { render() {
const { cancelAction, signAction } = this.props; const { cancelAction, signAction, disabled = false } = this.props;
return ( return (
<div className="signature-request-footer"> <div className="signature-request-footer">
<Button onClick={cancelAction} type="secondary" large> <Button onClick={cancelAction} type="secondary" large>
{this.context.t('cancel')} {this.context.t('cancel')}
</Button> </Button>
<Button onClick={signAction} type="primary" large> <Button onClick={signAction} type="primary" disabled={disabled} large>
{this.context.t('sign')} {this.context.t('sign')}
</Button> </Button>
</div> </div>

@ -14,7 +14,7 @@ export default class SignatureRequestHeader extends PureComponent {
return ( return (
<div className="signature-request-header"> <div className="signature-request-header">
<div className="signature-request-header--account"> <div className="signature-request-header--account">
{fromAccount && <AccountListItem account={fromAccount} />} {fromAccount ? <AccountListItem account={fromAccount} /> : null}
</div> </div>
<div className="signature-request-header--network"> <div className="signature-request-header--network">
<NetworkDisplay colored={false} /> <NetworkDisplay colored={false} />

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import Identicon from '../../ui/identicon'; import Identicon from '../../ui/identicon';
import LedgerInstructionField from '../ledger-instruction-field';
import Header from './signature-request-header'; import Header from './signature-request-header';
import Footer from './signature-request-footer'; import Footer from './signature-request-footer';
import Message from './signature-request-message'; import Message from './signature-request-message';
@ -15,10 +16,11 @@ export default class SignatureRequest extends PureComponent {
balance: PropTypes.string, balance: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
}).isRequired, }).isRequired,
isLedgerWallet: PropTypes.bool,
clearConfirmTransaction: PropTypes.func.isRequired, clearConfirmTransaction: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired, cancel: PropTypes.func.isRequired,
sign: PropTypes.func.isRequired, sign: PropTypes.func.isRequired,
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -69,6 +71,8 @@ export default class SignatureRequest extends PureComponent {
}, },
cancel, cancel,
sign, sign,
isLedgerWallet,
hardwareWalletRequiresConnection,
} = this.props; } = this.props;
const { address: fromAddress } = fromAccount; const { address: fromAddress } = fromAccount;
const { message, domain = {} } = JSON.parse(data); const { message, domain = {} } = JSON.parse(data);
@ -128,8 +132,17 @@ export default class SignatureRequest extends PureComponent {
{this.formatWallet(fromAddress)} {this.formatWallet(fromAddress)}
</div> </div>
</div> </div>
{isLedgerWallet ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
) : null}
<Message data={message} /> <Message data={message} />
<Footer cancelAction={onCancel} signAction={onSign} /> <Footer
cancelAction={onCancel}
signAction={onSign}
disabled={hardwareWalletRequiresConnection}
/>
</div> </div>
); );
} }

@ -1,12 +1,28 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { accountsWithSendEtherInfoSelector } from '../../../selectors'; import {
accountsWithSendEtherInfoSelector,
doesAddressRequireLedgerHidConnection,
} from '../../../selectors';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import { getAccountByAddress } from '../../../helpers/utils/util'; import { getAccountByAddress } from '../../../helpers/utils/util';
import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import SignatureRequest from './signature-request.component'; import SignatureRequest from './signature-request.component';
function mapStateToProps(state) { function mapStateToProps(state, ownProps) {
const { txData } = ownProps;
const {
msgParams: { from },
} = txData;
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
state,
from,
);
const isLedgerWallet = isAddressLedger(state, from);
return { return {
isLedgerWallet,
hardwareWalletRequiresConnection,
// not forwarded to component // not forwarded to component
allAccounts: accountsWithSendEtherInfoSelector(state), allAccounts: accountsWithSendEtherInfoSelector(state),
}; };
@ -19,7 +35,11 @@ function mapDispatchToProps(dispatch) {
} }
function mergeProps(stateProps, dispatchProps, ownProps) { function mergeProps(stateProps, dispatchProps, ownProps) {
const { allAccounts } = stateProps; const {
allAccounts,
isLedgerWallet,
hardwareWalletRequiresConnection,
} = stateProps;
const { const {
signPersonalMessage, signPersonalMessage,
signTypedMessage, signTypedMessage,
@ -58,6 +78,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
txData, txData,
cancel, cancel,
sign, sign,
isLedgerWallet,
hardwareWalletRequiresConnection,
}; };
} }

@ -3,6 +3,7 @@
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
width: 500px; width: 500px;
margin: 0 auto;
} }
ul.two-steps { ul.two-steps {

@ -40,7 +40,7 @@ export default class TransactionActivityLogIcon extends PureComponent {
return ( return (
<div className={classnames('transaction-activity-log-icon', className)}> <div className={classnames('transaction-activity-log-icon', className)}>
{imagePath && <img src={imagePath} height="9" width="9" alt="" />} {imagePath ? <img src={imagePath} height="9" width="9" alt="" /> : null}
</div> </div>
); );
} }

@ -35,7 +35,7 @@ export default function ActionableMessage({
return ( return (
<div className={actionableMessageClassName}> <div className={actionableMessageClassName}>
{useIcon && <InfoTooltipIcon fillColor={iconFillColor} />} {useIcon ? <InfoTooltipIcon fillColor={iconFillColor} /> : null}
{infoTooltipText && ( {infoTooltipText && (
<InfoTooltip <InfoTooltip
position="left" position="left"

@ -66,7 +66,7 @@ const Button = ({
)} )}
{...buttonProps} {...buttonProps}
> >
{icon && <span className="button__icon">{icon}</span>} {icon ? <span className="button__icon">{icon}</span> : null}
{children} {children}
</Tag> </Tag>
); );

@ -35,7 +35,7 @@ export default function Chip({
role={isInteractive ? 'button' : undefined} role={isInteractive ? 'button' : undefined}
tabIndex={isInteractive ? 0 : undefined} tabIndex={isInteractive ? 0 : undefined}
> >
{leftIcon && <div className="chip__left-icon">{leftIcon}</div>} {leftIcon ? <div className="chip__left-icon">{leftIcon}</div> : null}
{children ?? ( {children ?? (
<Typography <Typography
className="chip__label" className="chip__label"
@ -47,7 +47,7 @@ export default function Chip({
{label} {label}
</Typography> </Typography>
)} )}
{rightIcon && <div className="chip__right-icon">{rightIcon}</div>} {rightIcon ? <div className="chip__right-icon">{rightIcon}</div> : null}
</div> </div>
); );
} }

@ -6,7 +6,7 @@ export default function IconWithLabel({ icon, label, className }) {
return ( return (
<div className={classnames('icon-with-label', className)}> <div className={classnames('icon-with-label', className)}>
{icon} {icon}
{label && <span className="icon-with-label__label">{label}</span>} {label ? <span className="icon-with-label__label">{label}</span> : null}
</div> </div>
); );
} }

@ -33,7 +33,7 @@ export default function ListItem({
} }
}} }}
> >
{icon && <div className="list-item__icon">{icon}</div>} {icon ? <div className="list-item__icon">{icon}</div> : null}
<div className="list-item__heading"> <div className="list-item__heading">
{React.isValidElement(title) ? ( {React.isValidElement(title) ? (
title title
@ -44,12 +44,16 @@ export default function ListItem({
<div className="list-item__heading-wrap">{titleIcon}</div> <div className="list-item__heading-wrap">{titleIcon}</div>
)} )}
</div> </div>
{subtitle && <div className="list-item__subheading">{subtitle}</div>} {subtitle ? (
{children && <div className="list-item__actions">{children}</div>} <div className="list-item__subheading">{subtitle}</div>
{midContent && <div className="list-item__mid-content">{midContent}</div>} ) : null}
{rightContent && ( {children ? <div className="list-item__actions">{children}</div> : null}
{midContent ? (
<div className="list-item__mid-content">{midContent}</div>
) : null}
{rightContent ? (
<div className="list-item__right-content">{rightContent}</div> <div className="list-item__right-content">{rightContent}</div>
)} ) : null}
</div> </div>
); );
} }

@ -21,7 +21,7 @@ export default class PageContainerHeader extends Component {
renderTabs() { renderTabs() {
const { tabs } = this.props; const { tabs } = this.props;
return tabs && <ul className="page-container__tabs">{tabs}</ul>; return tabs ? <ul className="page-container__tabs">{tabs}</ul> : null;
} }
renderCloseAction() { renderCloseAction() {
@ -99,7 +99,9 @@ export default class PageContainerHeader extends Component {
{title} {title}
</div> </div>
)} )}
{subtitle && <div className="page-container__subtitle">{subtitle}</div>} {subtitle ? (
<div className="page-container__subtitle">{subtitle}</div>
) : null}
{this.renderCloseAction()} {this.renderCloseAction()}

@ -26,6 +26,10 @@ function QrCodeView(props) {
qrImage.addData(address); qrImage.addData(address);
qrImage.make(); qrImage.make();
const header = message ? (
<div className="qr-code__header">{message}</div>
) : null;
return ( return (
<div className="qr-code"> <div className="qr-code">
{Array.isArray(message) ? ( {Array.isArray(message) ? (
@ -37,9 +41,9 @@ function QrCodeView(props) {
))} ))}
</div> </div>
) : ( ) : (
message && <div className="qr-code__header">{message}</div> header
)} )}
{warning && <span className="qr_code__error">{warning}</span>} {warning ? <span className="qr_code__error">{warning}</span> : null}
<div <div
className="qr-code__wrapper" className="qr-code__wrapper"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{

@ -108,7 +108,7 @@ export default class UnitInput extends PureComponent {
}} }}
autoFocus autoFocus
/> />
{suffix && <div className="unit-input__suffix">{suffix}</div>} {suffix ? <div className="unit-input__suffix">{suffix}</div> : null}
</div> </div>
{children} {children}
</div> </div>

@ -1,3 +1,4 @@
import { WEBHID_CONNECTED_STATUSES } from '../../../shared/constants/hardware-wallets';
import * as actionConstants from '../../store/actionConstants'; import * as actionConstants from '../../store/actionConstants';
// actionConstants // actionConstants
@ -48,6 +49,7 @@ export default function reduceApp(state = {}, action) {
testKey: null, testKey: null,
}, },
gasLoadingAnimationIsShowing: false, gasLoadingAnimationIsShowing: false,
ledgerWebHidConnectedStatus: WEBHID_CONNECTED_STATUSES.UNKNOWN,
...state, ...state,
}; };
@ -340,6 +342,12 @@ export default function reduceApp(state = {}, action) {
gasLoadingAnimationIsShowing: action.value, gasLoadingAnimationIsShowing: action.value,
}; };
case actionConstants.SET_WEBHID_CONNECTED_STATUS:
return {
...appState,
ledgerWebHidConnectedStatus: action.value,
};
default: default:
return appState; return appState;
} }
@ -363,6 +371,10 @@ export function toggleGasLoadingAnimation(value) {
return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value }; return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value };
} }
export function setLedgerWebHidConnectedStatus(value) {
return { type: actionConstants.SET_WEBHID_CONNECTED_STATUS, value };
}
// Selectors // Selectors
export function getQrCodeData(state) { export function getQrCodeData(state) {
return state.appState.qrCodeData; return state.appState.qrCodeData;
@ -371,3 +383,7 @@ export function getQrCodeData(state) {
export function getGasLoadingAnimationIsShowing(state) { export function getGasLoadingAnimationIsShowing(state) {
return state.appState.gasLoadingAnimationIsShowing; return state.appState.gasLoadingAnimationIsShowing;
} }
export function getLedgerWebHidConnectedStatus(state) {
return state.appState.ledgerWebHidConnectedStatus;
}

@ -1,4 +1,4 @@
import { addHexPrefix, isHexString } from 'ethereumjs-util'; import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
import * as actionConstants from '../../store/actionConstants'; import * as actionConstants from '../../store/actionConstants';
import { ALERT_TYPES } from '../../../shared/constants/alerts'; import { ALERT_TYPES } from '../../../shared/constants/alerts';
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
@ -10,7 +10,9 @@ import {
import { updateTransaction } from '../../store/actions'; import { updateTransaction } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
export default function reduceMetamask(state = {}, action) { export default function reduceMetamask(state = {}, action) {
const metamaskState = { const metamaskState = {
@ -340,3 +342,59 @@ export function getIsUnlocked(state) {
export function getSeedPhraseBackedUp(state) { export function getSeedPhraseBackedUp(state) {
return state.metamask.seedPhraseBackedUp; return state.metamask.seedPhraseBackedUp;
} }
/**
* Given the redux state object and an address, finds a keyring that contains that address, if one exists
*
* @param {Object} state - the redux state object
* @param {string} address - the address to search for among the keyring addresses
* @returns {Object|undefined} The keyring which contains the passed address, or undefined
*/
export function findKeyringForAddress(state, address) {
const keyring = state.metamask.keyrings.find((kr) => {
return kr.accounts.some((account) => {
return (
isEqualCaseInsensitive(account, addHexPrefix(address)) ||
isEqualCaseInsensitive(account, stripHexPrefix(address))
);
});
});
return keyring;
}
/**
* Given the redux state object, returns the users preferred ledger transport type
*
* @param {Object} state - the redux state object
* @returns {string} The users preferred ledger transport type. One of'ledgerLive', 'webhid' or 'u2f'
*/
export function getLedgerTransportType(state) {
return state.metamask.ledgerTransportType;
}
/**
* Given the redux state object and an address, returns a boolean indicating whether the passed address is part of a Ledger keyring
*
* @param {Object} state - the redux state object
* @param {string} address - the address to search for among all keyring addresses
* @returns {boolean} true if the passed address is part of a ledger keyring, and false otherwise
*/
export function isAddressLedger(state, address) {
const keyring = findKeyringForAddress(state, address);
return keyring?.type === KEYRING_TYPES.LEDGER;
}
/**
* Given the redux state object, returns a boolean indicating whether the user has any Ledger accounts added to MetaMask (i.e. Ledger keyrings
* in state)
*
* @param {Object} state - the redux state object
* @returns {boolean} true if the user has a Ledger account and false otherwise
*/
export function doesUserHaveALedgerAccount(state) {
return state.metamask.keyrings.some((kr) => {
return kr.type === KEYRING_TYPES.LEDGER;
});
}

@ -88,6 +88,7 @@ import {
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common'; import { ETH, GWEI } from '../../helpers/constants/common';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction'; import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
// typedefs // typedefs
/** /**
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
@ -230,13 +231,12 @@ async function estimateGasLimitForSend({
// address. If this returns 0x, 0x0 or a nullish value then the address // address. If this returns 0x, 0x0 or a nullish value then the address
// is an externally owned account (NOT a contract account). For these // is an externally owned account (NOT a contract account). For these
// types of transactions the gasLimit will always be 21,000 or 0x5208 // types of transactions the gasLimit will always be 21,000 or 0x5208
const contractCode = Boolean(to) && (await global.eth.getCode(to)); const { isContractAddress } = to
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0' ? await readAddressAsContract(global.eth, to)
const contractCodeIsEmpty = : {};
!contractCode || contractCode === '0x' || contractCode === '0x0'; if (!isContractAddress && !isNonStandardEthChain) {
if (contractCodeIsEmpty && !isNonStandardEthChain) {
return GAS_LIMITS.SIMPLE; return GAS_LIMITS.SIMPLE;
} else if (contractCodeIsEmpty && isNonStandardEthChain) { } else if (!isContractAddress && isNonStandardEthChain) {
isSimpleSendOnNonStandardNetwork = true; isSimpleSendOnNonStandardNetwork = true;
} }
} }

@ -66,6 +66,7 @@ const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = '/onboarding/secure-your-wallet';
const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings'; const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings';
const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension'; const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension';
const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome'; const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome';
const ONBOARDING_METAMETRICS = '/onboarding/metametrics';
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'; const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction';
const CONFIRM_SEND_ETHER_PATH = '/send-ether'; const CONFIRM_SEND_ETHER_PATH = '/send-ether';
@ -227,4 +228,5 @@ export {
ONBOARDING_UNLOCK_ROUTE, ONBOARDING_UNLOCK_ROUTE,
ONBOARDING_PIN_EXTENSION_ROUTE, ONBOARDING_PIN_EXTENSION_ROUTE,
ONBOARDING_WELCOME_ROUTE, ONBOARDING_WELCOME_ROUTE,
ONBOARDING_METAMETRICS,
}; };

@ -10,11 +10,6 @@ import {
export default function Authenticated(props) { export default function Authenticated(props) {
const { isUnlocked, completedOnboarding } = props; const { isUnlocked, completedOnboarding } = props;
switch (true) { switch (true) {
// For ONBOARDING_V2 dev purposes,
// Remove when ONBOARDING_V2 dev complete
case process.env.ONBOARDING_V2 === true:
return <Redirect to={{ pathname: ONBOARDING_ROUTE }} />;
case isUnlocked && completedOnboarding: case isUnlocked && completedOnboarding:
return <Route {...props} />; return <Route {...props} />;
case !completedOnboarding: case !completedOnboarding:

@ -11,6 +11,7 @@ import {
TRANSACTION_ENVELOPE_TYPES, TRANSACTION_ENVELOPE_TYPES,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { addCurrencies } from '../../../shared/modules/conversion.utils'; import { addCurrencies } from '../../../shared/modules/conversion.utils';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
import fetchWithCache from './fetch-with-cache'; import fetchWithCache from './fetch-with-cache';
const hstInterface = new ethers.utils.Interface(abi); const hstInterface = new ethers.utils.Interface(abi);
@ -144,10 +145,8 @@ export function getLatestSubmittedTxWithNonce(
} }
export async function isSmartContractAddress(address) { export async function isSmartContractAddress(address) {
const code = await global.eth.getCode(address); const { isContractCode } = readAddressAsContract(global.eth, address);
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0' return isContractCode;
const codeIsEmpty = !code || code === '0x' || code === '0x0';
return !codeIsEmpty;
} }
export function sumHexes(...args) { export function sumHexes(...args) {

@ -14,6 +14,7 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box'; import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
export default class ConfirmApproveContent extends Component { export default class ConfirmApproveContent extends Component {
static contextTypes = { static contextTypes = {
@ -44,6 +45,8 @@ export default class ConfirmApproveContent extends Component {
nextNonce: PropTypes.number, nextNonce: PropTypes.number,
showCustomizeNonceModal: PropTypes.func, showCustomizeNonceModal: PropTypes.func,
warning: PropTypes.string, warning: PropTypes.string,
txData: PropTypes.object,
ledgerWalletRequiredHidConnection: PropTypes.bool,
}; };
state = { state = {
@ -238,6 +241,8 @@ export default class ConfirmApproveContent extends Component {
tokenBalance, tokenBalance,
useNonceField, useNonceField,
warning, warning,
txData,
ledgerWalletRequiredHidConnection,
} = this.props; } = this.props;
const { showFullTxDetails } = this.state; const { showFullTxDetails } = this.state;
@ -346,6 +351,14 @@ export default class ConfirmApproveContent extends Component {
})} })}
</div> </div>
{ledgerWalletRequiredHidConnection ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
</div>
) : null}
{showFullTxDetails ? ( {showFullTxDetails ? (
<div className="confirm-approve-content__full-tx-content"> <div className="confirm-approve-content__full-tx-content">
<div className="confirm-approve-content__permission"> <div className="confirm-approve-content__permission">

@ -161,6 +161,11 @@
} }
} }
&__ledger-instruction-wrapper {
padding-left: 10px;
padding-right: 10px;
}
&__transaction-details-content { &__transaction-details-content {
display: flex; display: flex;
flex-flow: row; flex-flow: row;

@ -24,6 +24,7 @@ import {
getUseNonceField, getUseNonceField,
getCustomNonceValue, getCustomNonceValue,
getNextSuggestedNonce, getNextSuggestedNonce,
doesAddressRequireLedgerHidConnection,
} from '../../selectors'; } from '../../selectors';
import { useApproveTransaction } from '../../hooks/useApproveTransaction'; import { useApproveTransaction } from '../../hooks/useApproveTransaction';
@ -35,12 +36,18 @@ import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { getCustomTxParamsData } from './confirm-approve.util'; import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content'; import ConfirmApproveContent from './confirm-approve-content';
const doesAddressRequireLedgerHidConnectionByFromAddress = (address) => (
state,
) => {
return doesAddressRequireLedgerHidConnection(state, address);
};
export default function ConfirmApprove() { export default function ConfirmApprove() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { id: paramsTransactionId } = useParams(); const { id: paramsTransactionId } = useParams();
const { const {
id: transactionId, id: transactionId,
txParams: { to: tokenAddress, data } = {}, txParams: { to: tokenAddress, data, from } = {},
} = useSelector(txDataSelector); } = useSelector(txDataSelector);
const currentCurrency = useSelector(getCurrentCurrency); const currentCurrency = useSelector(getCurrentCurrency);
@ -52,6 +59,10 @@ export default function ConfirmApprove() {
const nextNonce = useSelector(getNextSuggestedNonce); const nextNonce = useSelector(getNextSuggestedNonce);
const customNonceValue = useSelector(getCustomNonceValue); const customNonceValue = useSelector(getCustomNonceValue);
const ledgerWalletRequiredHidConnection = useSelector(
doesAddressRequireLedgerHidConnectionByFromAddress(from),
);
const transaction = const transaction =
currentNetworkTxList.find( currentNetworkTxList.find(
({ id }) => id === (Number(paramsTransactionId) || transactionId), ({ id }) => id === (Number(paramsTransactionId) || transactionId),
@ -207,6 +218,10 @@ export default function ConfirmApprove() {
) )
} }
warning={submitWarning} warning={submitWarning}
txData={transaction}
ledgerWalletRequiredHidConnection={
ledgerWalletRequiredHidConnection
}
/> />
{showCustomizeGasPopover && ( {showCustomizeGasPopover && (
<EditGasPopover <EditGasPopover

@ -35,12 +35,11 @@ import TransactionDetailItem from '../../components/app/transaction-detail-item/
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip'; import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat'; import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasTiming from '../../components/app/gas-timing/gas-timing.component'; import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import Dialog from '../../components/ui/dialog'; import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import { import {
COLORS, COLORS,
FONT_STYLE, FONT_STYLE,
FONT_WEIGHT,
TYPOGRAPHY, TYPOGRAPHY,
} from '../../helpers/constants/design-system'; } from '../../helpers/constants/design-system';
import { import {
@ -51,6 +50,7 @@ import {
} from '../../store/actions'; } from '../../store/actions';
import Typography from '../../components/ui/typography/typography'; import Typography from '../../components/ui/typography/typography';
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
const renderHeartBeatIfNotInTest = () => const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />; process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />;
@ -126,9 +126,9 @@ export default class ConfirmTransactionBase extends Component {
isMainnet: PropTypes.bool, isMainnet: PropTypes.bool,
gasFeeIsCustom: PropTypes.bool, gasFeeIsCustom: PropTypes.bool,
showLedgerSteps: PropTypes.bool.isRequired, showLedgerSteps: PropTypes.bool.isRequired,
isFirefox: PropTypes.bool.isRequired,
nativeCurrency: PropTypes.string, nativeCurrency: PropTypes.string,
supportsEIP1559: PropTypes.bool, supportsEIP1559: PropTypes.bool,
hardwareWalletRequiresConnection: PropTypes.bool,
}; };
state = { state = {
@ -232,7 +232,7 @@ export default class ConfirmTransactionBase extends Component {
}; };
} }
if (hexToDecimal(customGas.gasLimit) < 21000) { if (hexToDecimal(customGas.gasLimit) < Number(MIN_GAS_LIMIT_DEC)) {
return { return {
valid: false, valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY, errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
@ -309,7 +309,6 @@ export default class ConfirmTransactionBase extends Component {
maxPriorityFeePerGas, maxPriorityFeePerGas,
isMainnet, isMainnet,
showLedgerSteps, showLedgerSteps,
isFirefox,
supportsEIP1559, supportsEIP1559,
} = this.props; } = this.props;
const { t } = this.context; const { t } = this.context;
@ -404,46 +403,6 @@ export default class ConfirmTransactionBase extends Component {
</div> </div>
) : null; ) : null;
const renderLedgerLiveStep = (text, show = true) => {
return (
show && (
<Typography
boxProps={{ margin: 0 }}
color={COLORS.PRIMARY3}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H7}
>
{text}
</Typography>
)
);
};
const ledgerInstructionField = showLedgerSteps ? (
<div>
<div className="confirm-detail-row">
<Dialog type="message">
<div className="ledger-live-dialog">
{renderLedgerLiveStep(t('ledgerLiveDialogHeader'))}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepOne')}`,
!isFirefox,
)}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepTwo')}`,
!isFirefox,
)}
{renderLedgerLiveStep(`- ${t('ledgerLiveDialogStepThree')}`)}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepFour')}`,
Boolean(txData.txParams?.data),
)}
</div>
</Dialog>
</div>
</div>
) : null;
return ( return (
<div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__details">
<TransactionDetail <TransactionDetail
@ -573,7 +532,11 @@ export default class ConfirmTransactionBase extends Component {
]} ]}
/> />
{nonceField} {nonceField}
{ledgerInstructionField} {showLedgerSteps ? (
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
) : null}
</div> </div>
); );
} }
@ -915,6 +878,7 @@ export default class ConfirmTransactionBase extends Component {
gasIsLoading, gasIsLoading,
gasFeeIsCustom, gasFeeIsCustom,
nativeCurrency, nativeCurrency,
hardwareWalletRequiresConnection,
} = this.props; } = this.props;
const { const {
submitting, submitting,
@ -980,7 +944,12 @@ export default class ConfirmTransactionBase extends Component {
lastTx={lastTx} lastTx={lastTx}
ofText={ofText} ofText={ofText}
requestsWaitingText={requestsWaitingText} requestsWaitingText={requestsWaitingText}
disabled={!valid || submitting || (gasIsLoading && !gasFeeIsCustom)} disabled={
!valid ||
submitting ||
hardwareWalletRequiresConnection ||
(gasIsLoading && !gasFeeIsCustom)
}
onEdit={() => this.handleEdit()} onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()} onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()} onCancel={() => this.handleCancel()}

@ -28,24 +28,24 @@ import {
getShouldShowFiat, getShouldShowFiat,
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
getPreferences, getPreferences,
getHardwareWalletType, doesAddressRequireLedgerHidConnection,
getUseTokenDetection, getUseTokenDetection,
getTokenList, getTokenList,
} from '../../selectors'; } from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
transactionMatchesNetwork, isAddressLedger,
txParamsAreDappSuggested,
} from '../../../shared/modules/transaction.utils';
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
import { getPlatform } from '../../../app/scripts/lib/util';
import { PLATFORM_FIREFOX } from '../../../shared/constants/app';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import {
updateTransactionGasFees, updateTransactionGasFees,
getIsGasEstimatesLoading, getIsGasEstimatesLoading,
getNativeCurrency, getNativeCurrency,
} from '../../ducks/metamask/metamask'; } from '../../ducks/metamask/metamask';
import {
transactionMatchesNetwork,
txParamsAreDappSuggested,
} from '../../../shared/modules/transaction.utils';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app'; import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util'; import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import ConfirmTransactionBase from './confirm-transaction-base.component'; import ConfirmTransactionBase from './confirm-transaction-base.component';
@ -170,10 +170,14 @@ const mapStateToProps = (state, ownProps) => {
const gasFeeIsCustom = const gasFeeIsCustom =
fullTxData.userFeeLevel === 'custom' || fullTxData.userFeeLevel === 'custom' ||
txParamsAreDappSuggested(fullTxData); txParamsAreDappSuggested(fullTxData);
const showLedgerSteps = getHardwareWalletType(state) === KEYRING_TYPES.LEDGER; const fromAddressIsLedger = isAddressLedger(state, fromAddress);
const isFirefox = getPlatform() === PLATFORM_FIREFOX;
const nativeCurrency = getNativeCurrency(state); const nativeCurrency = getNativeCurrency(state);
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
state,
fromAddress,
);
return { return {
balance, balance,
fromAddress, fromAddress,
@ -219,9 +223,9 @@ const mapStateToProps = (state, ownProps) => {
maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas, maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas,
baseFeePerGas: gasEstimationObject.baseFeePerGas, baseFeePerGas: gasEstimationObject.baseFeePerGas,
gasFeeIsCustom, gasFeeIsCustom,
showLedgerSteps, showLedgerSteps: fromAddressIsLedger,
isFirefox,
nativeCurrency, nativeCurrency,
hardwareWalletRequiresConnection,
}; };
}; };

@ -273,7 +273,7 @@ class AccountList extends Component {
{this.renderPagination()} {this.renderPagination()}
{this.renderButtons()} {this.renderButtons()}
{this.renderForgetDevice()} {this.renderForgetDevice()}
{showPopover && this.renderSelectPathPopover()} {showPopover ? this.renderSelectPathPopover() : null}
</div> </div>
); );
} }

@ -11,6 +11,7 @@ import {
import { formatBalance } from '../../../helpers/utils/util'; import { formatBalance } from '../../../helpers/utils/util';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { SECOND } from '../../../../shared/constants/time'; import { SECOND } from '../../../../shared/constants/time';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import SelectHardware from './select-hardware'; import SelectHardware from './select-hardware';
import AccountList from './account-list'; import AccountList from './account-list';
@ -26,6 +27,10 @@ const HD_PATHS = [
]; ];
class ConnectHardwareForm extends Component { class ConnectHardwareForm extends Component {
static contextTypes = {
t: PropTypes.func,
};
state = { state = {
error: null, error: null,
selectedAccounts: [], selectedAccounts: [],
@ -106,7 +111,7 @@ class ConnectHardwareForm extends Component {
getPage = (device, page, hdPath) => { getPage = (device, page, hdPath) => {
this.props this.props
.connectHardware(device, page, hdPath) .connectHardware(device, page, hdPath, this.context.t)
.then((accounts) => { .then((accounts) => {
if (accounts.length) { if (accounts.length) {
// If we just loaded the accounts for the first time // If we just loaded the accounts for the first time
@ -262,7 +267,7 @@ class ConnectHardwareForm extends Component {
<SelectHardware <SelectHardware
connectToHardwareWallet={this.connectToHardwareWallet} connectToHardwareWallet={this.connectToHardwareWallet}
browserSupported={this.state.browserSupported} browserSupported={this.state.browserSupported}
useLedgerLive={this.props.useLedgerLive} ledgerTransportType={this.props.ledgerTransportType}
/> />
); );
} }
@ -313,7 +318,7 @@ ConnectHardwareForm.propTypes = {
connectedAccounts: PropTypes.array.isRequired, connectedAccounts: PropTypes.array.isRequired,
defaultHdPaths: PropTypes.object, defaultHdPaths: PropTypes.object,
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
useLedgerLive: PropTypes.bool.isRequired, ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@ -323,7 +328,7 @@ const mapStateToProps = (state) => ({
connectedAccounts: getMetaMaskAccountsConnected(state), connectedAccounts: getMetaMaskAccountsConnected(state),
defaultHdPaths: state.appState.defaultHdPaths, defaultHdPaths: state.appState.defaultHdPaths,
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
useLedgerLive: state.metamask.useLedgerLive, ledgerTransportType: state.metamask.ledgerTransportType,
}); });
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
@ -331,8 +336,8 @@ const mapDispatchToProps = (dispatch) => {
setHardwareWalletDefaultHdPath: ({ device, path }) => { setHardwareWalletDefaultHdPath: ({ device, path }) => {
return dispatch(actions.setHardwareWalletDefaultHdPath({ device, path })); return dispatch(actions.setHardwareWalletDefaultHdPath({ device, path }));
}, },
connectHardware: (deviceName, page, hdPath) => { connectHardware: (deviceName, page, hdPath, t) => {
return dispatch(actions.connectHardware(deviceName, page, hdPath)); return dispatch(actions.connectHardware(deviceName, page, hdPath, t));
}, },
checkHardwareStatus: (deviceName, hdPath) => { checkHardwareStatus: (deviceName, hdPath) => {
return dispatch(actions.checkHardwareStatus(deviceName, hdPath)); return dispatch(actions.checkHardwareStatus(deviceName, hdPath));

@ -2,6 +2,7 @@ import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
export default class SelectHardware extends Component { export default class SelectHardware extends Component {
static contextTypes = { static contextTypes = {
@ -11,7 +12,7 @@ export default class SelectHardware extends Component {
static propTypes = { static propTypes = {
connectToHardwareWallet: PropTypes.func.isRequired, connectToHardwareWallet: PropTypes.func.isRequired,
browserSupported: PropTypes.bool.isRequired, browserSupported: PropTypes.bool.isRequired,
useLedgerLive: PropTypes.bool.isRequired, ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
}; };
state = { state = {
@ -136,7 +137,7 @@ export default class SelectHardware extends Component {
renderLedgerTutorialSteps() { renderLedgerTutorialSteps() {
const steps = []; const steps = [];
if (this.props.useLedgerLive) { if (this.props.ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE) {
steps.push({ steps.push({
title: this.context.t('step1LedgerWallet'), title: this.context.t('step1LedgerWallet'),
message: this.context.t('step1LedgerWalletMsg', [ message: this.context.t('step1LedgerWalletMsg', [
@ -235,7 +236,7 @@ export default class SelectHardware extends Component {
<div className="new-external-account-form"> <div className="new-external-account-form">
{this.renderHeader()} {this.renderHeader()}
{this.renderButtons()} {this.renderButtons()}
{this.state.selectedDevice && this.renderTutorialsteps()} {this.state.selectedDevice ? this.renderTutorialsteps() : null}
{this.renderContinueButton()} {this.renderContinueButton()}
</div> </div>
); );

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import SelectHardware from './select-hardware'; import SelectHardware from './select-hardware';
export default { export default {
@ -14,7 +15,7 @@ export const SelectHardwareComponent = () => {
connectToHardwareWallet={(selectedDevice) => connectToHardwareWallet={(selectedDevice) =>
action(`Continue connect to ${selectedDevice}`)() action(`Continue connect to ${selectedDevice}`)()
} }
useLedgerLive ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
/> />
); );
}; };
@ -23,7 +24,7 @@ export const BrowserNotSupported = () => {
<SelectHardware <SelectHardware
browserSupported={false} browserSupported={false}
connectToHardwareWallet={() => undefined} connectToHardwareWallet={() => undefined}
useLedgerLive ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
/> />
); );
}; };

@ -258,7 +258,9 @@ export default class ImportWithSeedPhrase extends PureComponent {
autoComplete="off" autoComplete="off"
/> />
)} )}
{seedPhraseError && <span className="error">{seedPhraseError}</span>} {seedPhraseError ? (
<span className="error">{seedPhraseError}</span>
) : null}
<div <div
className="first-time-flow__checkbox-container" className="first-time-flow__checkbox-container"
onClick={this.toggleShowSeedPhrase} onClick={this.toggleShowSeedPhrase}

@ -113,10 +113,18 @@
color: #1b344d; color: #1b344d;
} }
&__selector-typography {
line-height: 22px;
display: flex;
align-items: center;
color: #000;
margin-top: 18px;
}
&__input-wrapper { &__input-wrapper {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
margin-top: 30px; margin-top: 18px;
} }
&__input { &__input {

@ -163,6 +163,9 @@ class RestoreVaultPage extends Component {
<div className="import-account__selector-label"> <div className="import-account__selector-label">
{this.context.t('secretPhrase')} {this.context.t('secretPhrase')}
</div> </div>
<div className="import-account__selector-typography">
{this.context.t('secretPhraseWarning')}
</div>
<div className="import-account__input-wrapper"> <div className="import-account__input-wrapper">
<label className="import-account__input-label"> <label className="import-account__input-label">
{this.context.t('walletSeedRestore')} {this.context.t('walletSeedRestore')}

@ -6,6 +6,8 @@
@import 'creation-successful/index'; @import 'creation-successful/index';
@import 'welcome/index'; @import 'welcome/index';
@import 'import-srp/index'; @import 'import-srp/index';
@import 'pin-extension/index';
@import 'metametrics/index';
.onboarding-flow { .onboarding-flow {
width: 100%; width: 100%;

@ -0,0 +1,41 @@
.onboarding-metametrics {
width: 600px;
ul {
margin: 24px 0 0 0;
li {
padding-bottom: 20px;
display: flex;
}
}
.fa {
width: 16px;
}
.fa-check {
margin-inline-end: 12px;
color: #1acc56;
}
.fa-times {
margin-inline-end: 12px;
color: #d0021b;
}
&__terms a {
color: $Blue-500;
}
&__buttons {
margin: 24px auto 0 auto;
justify-content: space-between;
display: flex;
button {
margin-bottom: 24px;
width: 200px;
}
}
}

@ -0,0 +1,152 @@
import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import Typography from '../../../components/ui/typography/typography';
import {
TYPOGRAPHY,
FONT_WEIGHT,
TEXT_ALIGN,
COLORS,
} from '../../../helpers/constants/design-system';
import Button from '../../../components/ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { setParticipateInMetaMetrics } from '../../../store/actions';
import {
getFirstTimeFlowTypeRoute,
getFirstTimeFlowType,
getParticipateInMetaMetrics,
} from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
const firstTimeFlowTypeNameMap = {
create: 'Selected Create New Wallet',
import: 'Selected Import Wallet',
};
export default function OnboardingMetametrics() {
const t = useI18nContext();
const dispatch = useDispatch();
const history = useHistory();
const nextRoute = useSelector(getFirstTimeFlowTypeRoute);
const firstTimeFlowType = useSelector(getFirstTimeFlowType);
const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics);
const firstTimeSelectionMetaMetricsName =
firstTimeFlowTypeNameMap[firstTimeFlowType];
const metricsEvent = useContext(MetaMetricsContext);
const onConfirm = async () => {
const [, metaMetricsId] = await dispatch(setParticipateInMetaMetrics(true));
try {
if (!participateInMetaMetrics) {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt In',
},
isOptIn: true,
flushImmediately: true,
});
}
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import or Create',
name: firstTimeSelectionMetaMetricsName,
},
isOptIn: true,
metaMetricsId,
flushImmediately: true,
});
} finally {
history.push(nextRoute);
}
};
const onCancel = async () => {
await dispatch(setParticipateInMetaMetrics(false));
try {
if (!participateInMetaMetrics) {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt Out',
},
isOptIn: true,
flushImmediately: true,
});
}
} finally {
history.push(nextRoute);
}
};
return (
<div className="onboarding-metametrics">
<Typography
variant={TYPOGRAPHY.H2}
align={TEXT_ALIGN.CENTER}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('metametricsTitle')}
</Typography>
<Typography align={TEXT_ALIGN.CENTER}>
{t('metametricsOptInDescription2')}
</Typography>
<ul>
<li>
<i className="fa fa-check" />
{t('metametricsCommitmentsAllowOptOut2')}
</li>
<li>
<i className="fa fa-check" />
{t('metametricsCommitmentsSendAnonymizedEvents')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverCollect')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverIP')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverSell')}
</li>
</ul>
<Typography
color={COLORS.UI4}
align={TEXT_ALIGN.CENTER}
variant={TYPOGRAPHY.H6}
className="onboarding-metametrics__terms"
>
{t('gdprMessage', [
<a
key="metametrics-bottom-text-wrapper"
href="https://metamask.io/privacy.html"
target="_blank"
rel="noopener noreferrer"
>
{t('gdprMessagePrivacyPolicy')}
</a>,
])}
</Typography>
<div className="onboarding-metametrics__buttons">
<Button type="secondary" onClick={onCancel}>
{t('noThanks')}
</Button>
<Button type="primary" onClick={onConfirm}>
{t('affirmAgree')}
</Button>
</div>
</div>
);
}

@ -0,0 +1,9 @@
import React from 'react';
import OnboardingMetametrics from './metametrics';
export default {
title: 'Onboarding',
id: __filename,
};
export const OnboardingComponent = () => <OnboardingMetametrics />;

@ -13,6 +13,8 @@ import {
ONBOARDING_PRIVACY_SETTINGS_ROUTE, ONBOARDING_PRIVACY_SETTINGS_ROUTE,
ONBOARDING_COMPLETION_ROUTE, ONBOARDING_COMPLETION_ROUTE,
ONBOARDING_IMPORT_WITH_SRP_ROUTE, ONBOARDING_IMPORT_WITH_SRP_ROUTE,
ONBOARDING_PIN_EXTENSION_ROUTE,
ONBOARDING_METAMETRICS,
} from '../../helpers/constants/routes'; } from '../../helpers/constants/routes';
import { import {
getCompletedOnboarding, getCompletedOnboarding,
@ -37,6 +39,8 @@ import PrivacySettings from './privacy-settings/privacy-settings';
import CreationSuccessful from './creation-successful/creation-successful'; import CreationSuccessful from './creation-successful/creation-successful';
import OnboardingWelcome from './welcome/welcome'; import OnboardingWelcome from './welcome/welcome';
import ImportSRP from './import-srp/import-srp'; import ImportSRP from './import-srp/import-srp';
import OnboardingPinExtension from './pin-extension/pin-extension';
import MetaMetricsComponent from './metametrics/metametrics';
export default function OnboardingFlow() { export default function OnboardingFlow() {
const [secretRecoveryPhrase, setSecretRecoveryPhrase] = useState(''); const [secretRecoveryPhrase, setSecretRecoveryPhrase] = useState('');
@ -51,13 +55,6 @@ export default function OnboardingFlow() {
const nextRoute = useSelector(getFirstTimeFlowTypeRoute); const nextRoute = useSelector(getFirstTimeFlowTypeRoute);
useEffect(() => { useEffect(() => {
// For ONBOARDING_V2 dev purposes,
// Remove when ONBOARDING_V2 dev complete
if (process.env.ONBOARDING_V2) {
history.push(ONBOARDING_IMPORT_WITH_SRP_ROUTE);
return;
}
if (completedOnboarding && seedPhraseBackedUp) { if (completedOnboarding && seedPhraseBackedUp) {
history.push(DEFAULT_ROUTE); history.push(DEFAULT_ROUTE);
return; return;
@ -156,6 +153,14 @@ export default function OnboardingFlow() {
path={ONBOARDING_WELCOME_ROUTE} path={ONBOARDING_WELCOME_ROUTE}
component={OnboardingWelcome} component={OnboardingWelcome}
/> />
<Route
path={ONBOARDING_PIN_EXTENSION_ROUTE}
component={OnboardingPinExtension}
/>
<Route
path={ONBOARDING_METAMETRICS}
component={MetaMetricsComponent}
/>
<Route exact path="*" component={OnboardingFlowSwitch} /> <Route exact path="*" component={OnboardingFlowSwitch} />
</Switch> </Switch>
</div> </div>

@ -0,0 +1,23 @@
.onboarding-pin-extension {
max-width: 800px;
.control-dots .dot {
background: $ui-2;
box-shadow: none;
&.selected {
background: $ui-4;
}
}
&__diagram {
margin: 24px auto;
width: 799px;
height: 320px;
}
&__buttons {
max-width: 50%;
margin: 40px auto 0 auto;
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Carousel } from 'react-responsive-carousel';
import Typography from '../../../components/ui/typography/typography';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Button from '../../../components/ui/button';
import {
TYPOGRAPHY,
FONT_WEIGHT,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import OnboardingPinBillboard from './pin-billboard';
export default function OnboardingPinExtension() {
const t = useI18nContext();
const [selectedIndex, setSelectedIndex] = useState(0);
const history = useHistory();
return (
<div className="onboarding-pin-extension">
<Typography
variant={TYPOGRAPHY.H2}
align={TEXT_ALIGN.CENTER}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('onboardingPinExtensionTitle')}
</Typography>
<Carousel
selectedItem={selectedIndex}
showThumbs={false}
showStatus={false}
showArrows={false}
>
<div>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription')}
</Typography>
<div className="onboarding-pin-extension__diagram">
<OnboardingPinBillboard />
</div>
</div>
<div>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription2')}
</Typography>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription3')}
</Typography>
<img
src="/images/onboarding-pin-browser.svg"
width="799"
height="320"
alt=""
/>
</div>
</Carousel>
<div className="onboarding-pin-extension__buttons">
<Button
type="primary"
onClick={() => {
if (selectedIndex === 0) {
setSelectedIndex(1);
} else {
history.push(DEFAULT_ROUTE);
}
}}
>
{selectedIndex === 0 ? t('next') : t('done')}
</Button>
</div>
</div>
);
}

@ -0,0 +1,9 @@
import React from 'react';
import OnboardingPinExtension from './pin-extension';
export default {
title: 'Onboarding',
id: __filename,
};
export const OnboardingComponent = () => <OnboardingPinExtension />;

@ -10,6 +10,7 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { import {
setCompletedOnboarding,
setFeatureFlag, setFeatureFlag,
setUsePhishDetect, setUsePhishDetect,
setUseTokenDetection, setUseTokenDetection,
@ -33,6 +34,7 @@ export default function PrivacySettings() {
); );
dispatch(setUsePhishDetect(usePhishingDetection)); dispatch(setUsePhishDetect(usePhishingDetection));
dispatch(setUseTokenDetection(turnOnTokenDetection)); dispatch(setUseTokenDetection(turnOnTokenDetection));
dispatch(setCompletedOnboarding());
history.push(ONBOARDING_PIN_EXTENSION_ROUTE); history.push(ONBOARDING_PIN_EXTENSION_ROUTE);
}; };

@ -2,8 +2,10 @@ import React from 'react';
import { fireEvent } from '@testing-library/react'; import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import * as actions from '../../../store/actions'; import {
import { renderWithProvider } from '../../../../test/jest'; renderWithProvider,
setBackgroundConnection,
} from '../../../../test/jest';
import PrivacySettings from './privacy-settings'; import PrivacySettings from './privacy-settings';
describe('Privacy Settings Onboarding View', () => { describe('Privacy Settings Onboarding View', () => {
@ -19,11 +21,15 @@ describe('Privacy Settings Onboarding View', () => {
const setFeatureFlagStub = jest.fn(); const setFeatureFlagStub = jest.fn();
const setUsePhishDetectStub = jest.fn(); const setUsePhishDetectStub = jest.fn();
const setUseTokenDetectionStub = jest.fn(); const setUseTokenDetectionStub = jest.fn();
const completeOnboardingStub = jest
.fn()
.mockImplementation(() => Promise.resolve());
actions._setBackgroundConnection({ setBackgroundConnection({
setFeatureFlag: setFeatureFlagStub, setFeatureFlag: setFeatureFlagStub,
setUsePhishDetect: setUsePhishDetectStub, setUsePhishDetect: setUsePhishDetectStub,
setUseTokenDetection: setUseTokenDetectionStub, setUseTokenDetection: setUseTokenDetectionStub,
completeOnboarding: completeOnboardingStub,
}); });
it('should update preferences', () => { it('should update preferences', () => {

@ -13,7 +13,7 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { setFirstTimeFlowType } from '../../../store/actions'; import { setFirstTimeFlowType } from '../../../store/actions';
import { INITIALIZE_METAMETRICS_OPT_IN_ROUTE } from '../../../helpers/constants/routes'; import { ONBOARDING_METAMETRICS } from '../../../helpers/constants/routes';
export default function OnboardingWelcome() { export default function OnboardingWelcome() {
const t = useI18nContext(); const t = useI18nContext();
@ -23,12 +23,12 @@ export default function OnboardingWelcome() {
const onCreateClick = () => { const onCreateClick = () => {
dispatch(setFirstTimeFlowType('create')); dispatch(setFirstTimeFlowType('create'));
history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE); history.push(ONBOARDING_METAMETRICS);
}; };
const onImportClick = () => { const onImportClick = () => {
dispatch(setFirstTimeFlowType('import')); dispatch(setFirstTimeFlowType('import'));
history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE); history.push(ONBOARDING_METAMETRICS);
}; };
return ( return (

@ -340,8 +340,8 @@ export default class Routes extends Component {
/> />
<AccountMenu /> <AccountMenu />
<div className="main-container-wrapper"> <div className="main-container-wrapper">
{isLoading && <Loading loadingMessage={loadMessage} />} {isLoading ? <Loading loadingMessage={loadMessage} /> : null}
{!isLoading && isNetworkLoading && <LoadingNetwork />} {!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
{this.renderRoutes()} {this.renderRoutes()}
</div> </div>
{isUnlocked ? <Alerts history={this.props.history} /> : null} {isUnlocked ? <Alerts history={this.props.history} /> : null}

@ -50,17 +50,20 @@ export default class SendContent extends Component {
return ( return (
<PageContainerContent> <PageContainerContent>
<div className="send-v2__form"> <div className="send-v2__form">
{gasError && this.renderError(gasError)} {gasError ? this.renderError(gasError) : null}
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)} {isEthGasPrice
{isAssetSendable === false && ? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
this.renderError(UNSENDABLE_ASSET_ERROR_KEY)} : null}
{error && this.renderError(error)} {isAssetSendable === false
{warning && this.renderWarning()} ? this.renderError(UNSENDABLE_ASSET_ERROR_KEY)
: null}
{error ? this.renderError(error) : null}
{warning ? this.renderWarning() : null}
{this.maybeRenderAddContact()} {this.maybeRenderAddContact()}
<SendAssetRow /> <SendAssetRow />
<SendAmountRow /> <SendAmountRow />
{networkOrAccountNotSupports1559 && <SendGasRow />} {networkOrAccountNotSupports1559 ? <SendGasRow /> : null}
{this.props.showHexData && <SendHexDataRow />} {this.props.showHexData ? <SendHexDataRow /> : null}
</div> </div>
</PageContainerContent> </PageContainerContent>
); );

@ -30,7 +30,7 @@ export default class SendRowWrapper extends Component {
<div className="send-v2__form-field-container"> <div className="send-v2__form-field-container">
<div className="send-v2__form-field">{formField}</div> <div className="send-v2__form-field">{formField}</div>
<div> <div>
{showError && <SendRowErrorMessage errorType={errorType} />} {showError ? <SendRowErrorMessage errorType={errorType} /> : null}
</div> </div>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@ export default class SendRowWrapper extends Component {
<div className="send-v2__form-row"> <div className="send-v2__form-row">
<div className="send-v2__form-label"> <div className="send-v2__form-label">
{label} {label}
{showError && <SendRowErrorMessage errorType={errorType} />} {showError ? <SendRowErrorMessage errorType={errorType} /> : null}
{customLabelContent} {customLabelContent}
</div> </div>
<div className="send-v2__form-field">{formField}</div> <div className="send-v2__form-field">{formField}</div>

@ -6,9 +6,12 @@ import ToggleButton from '../../../components/ui/toggle-button';
import TextField from '../../../components/ui/text-field'; import TextField from '../../../components/ui/text-field';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'; import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes';
import Dropdown from '../../../components/ui/dropdown';
import { getPlatform } from '../../../../app/scripts/lib/util'; import {
import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../../../shared/constants/hardware-wallets';
export default class AdvancedTab extends PureComponent { export default class AdvancedTab extends PureComponent {
static contextTypes = { static contextTypes = {
@ -36,10 +39,11 @@ export default class AdvancedTab extends PureComponent {
threeBoxDisabled: PropTypes.bool.isRequired, threeBoxDisabled: PropTypes.bool.isRequired,
setIpfsGateway: PropTypes.func.isRequired, setIpfsGateway: PropTypes.func.isRequired,
ipfsGateway: PropTypes.string.isRequired, ipfsGateway: PropTypes.string.isRequired,
useLedgerLive: PropTypes.bool.isRequired, ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
setLedgerLivePreference: PropTypes.func.isRequired, setLedgerLivePreference: PropTypes.func.isRequired,
setDismissSeedBackUpReminder: PropTypes.func.isRequired, setDismissSeedBackUpReminder: PropTypes.func.isRequired,
dismissSeedBackUpReminder: PropTypes.bool.isRequired, dismissSeedBackUpReminder: PropTypes.bool.isRequired,
userHasALedgerAccount: PropTypes.bool.isRequired,
}; };
state = { state = {
@ -393,24 +397,77 @@ export default class AdvancedTab extends PureComponent {
renderLedgerLiveControl() { renderLedgerLiveControl() {
const { t } = this.context; const { t } = this.context;
const { useLedgerLive, setLedgerLivePreference } = this.props; const {
ledgerTransportType,
setLedgerLivePreference,
userHasALedgerAccount,
} = this.props;
const LEDGER_TRANSPORT_NAMES = {
LIVE: t('ledgerLive'),
WEBHID: t('webhid'),
U2F: t('u2f'),
};
const transportTypeOptions = [
{
name: LEDGER_TRANSPORT_NAMES.LIVE,
value: LEDGER_TRANSPORT_TYPES.LIVE,
},
{
name: LEDGER_TRANSPORT_NAMES.U2F,
value: LEDGER_TRANSPORT_TYPES.U2F,
},
];
if (window.navigator.hid) {
transportTypeOptions.push({
name: LEDGER_TRANSPORT_NAMES.WEBHID,
value: LEDGER_TRANSPORT_TYPES.WEBHID,
});
}
const recommendedLedgerOption = window.navigator.hid
? LEDGER_TRANSPORT_NAMES.WEBHID
: LEDGER_TRANSPORT_NAMES.U2F;
return ( return (
<div className="settings-page__content-row"> <div className="settings-page__content-row">
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('ledgerLiveAdvancedSetting')}</span> <span>{t('preferredLedgerConnectionType')}</span>
<div className="settings-page__content-description"> <div className="settings-page__content-description">
{t('ledgerLiveAdvancedSettingDescription')} {t('ledgerConnectionPreferenceDescription', [
recommendedLedgerOption,
<Button
key="ledger-connection-settings-learn-more"
type="link"
href="https://metamask.zendesk.com/hc/en-us/articles/360020394612-How-to-connect-a-Trezor-or-Ledger-Hardware-Wallet"
target="_blank"
rel="noopener noreferrer"
className="settings-page__inline-link"
>
{t('learnMore')}
</Button>,
])}
</div> </div>
</div> </div>
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<div className="settings-page__content-item-col"> <div className="settings-page__content-item-col">
<ToggleButton <Dropdown
value={useLedgerLive} id="select-ledger-transport-type"
onToggle={(value) => setLedgerLivePreference(!value)} options={transportTypeOptions}
offLabel={t('off')} selectedOption={ledgerTransportType}
onLabel={t('on')} onChange={async (transportType) => {
disabled={getPlatform() === PLATFORM_FIREFOX} setLedgerLivePreference(transportType);
if (
transportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
userHasALedgerAccount
) {
await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
}
}}
/> />
</div> </div>
</div> </div>
@ -533,7 +590,7 @@ export default class AdvancedTab extends PureComponent {
return ( return (
<div className="settings-page__body"> <div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>} {warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderStateLogs()} {this.renderStateLogs()}
{this.renderMobileSync()} {this.renderMobileSync()}
{this.renderResetAccount()} {this.renderResetAccount()}

@ -2,6 +2,7 @@ import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import TextField from '../../../components/ui/text-field'; import TextField from '../../../components/ui/text-field';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import AdvancedTab from './advanced-tab.component'; import AdvancedTab from './advanced-tab.component';
describe('AdvancedTab Component', () => { describe('AdvancedTab Component', () => {
@ -15,7 +16,7 @@ describe('AdvancedTab Component', () => {
setThreeBoxSyncingPermission={() => undefined} setThreeBoxSyncingPermission={() => undefined}
threeBoxDisabled threeBoxDisabled
threeBoxSyncingAllowed={false} threeBoxSyncingAllowed={false}
useLedgerLive={false} ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
setLedgerLivePreference={() => undefined} setLedgerLivePreference={() => undefined}
setDismissSeedBackUpReminder={() => undefined} setDismissSeedBackUpReminder={() => undefined}
dismissSeedBackUpReminder={false} dismissSeedBackUpReminder={false}
@ -41,7 +42,7 @@ describe('AdvancedTab Component', () => {
setThreeBoxSyncingPermission={() => undefined} setThreeBoxSyncingPermission={() => undefined}
threeBoxDisabled threeBoxDisabled
threeBoxSyncingAllowed={false} threeBoxSyncingAllowed={false}
useLedgerLive={false} ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
setLedgerLivePreference={() => undefined} setLedgerLivePreference={() => undefined}
setDismissSeedBackUpReminder={() => undefined} setDismissSeedBackUpReminder={() => undefined}
dismissSeedBackUpReminder={false} dismissSeedBackUpReminder={false}

@ -15,6 +15,7 @@ import {
setDismissSeedBackUpReminder, setDismissSeedBackUpReminder,
} from '../../../store/actions'; } from '../../../store/actions';
import { getPreferences } from '../../../selectors'; import { getPreferences } from '../../../selectors';
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
import AdvancedTab from './advanced-tab.component'; import AdvancedTab from './advanced-tab.component';
export const mapStateToProps = (state) => { export const mapStateToProps = (state) => {
@ -28,11 +29,13 @@ export const mapStateToProps = (state) => {
threeBoxDisabled, threeBoxDisabled,
useNonceField, useNonceField,
ipfsGateway, ipfsGateway,
useLedgerLive, ledgerTransportType,
dismissSeedBackUpReminder, dismissSeedBackUpReminder,
} = metamask; } = metamask;
const { showFiatInTestnets, autoLockTimeLimit } = getPreferences(state); const { showFiatInTestnets, autoLockTimeLimit } = getPreferences(state);
const userHasALedgerAccount = doesUserHaveALedgerAccount(state);
return { return {
warning, warning,
sendHexData, sendHexData,
@ -43,8 +46,9 @@ export const mapStateToProps = (state) => {
threeBoxDisabled, threeBoxDisabled,
useNonceField, useNonceField,
ipfsGateway, ipfsGateway,
useLedgerLive, ledgerTransportType,
dismissSeedBackUpReminder, dismissSeedBackUpReminder,
userHasALedgerAccount,
}; };
}; };

@ -136,7 +136,9 @@ export default class ContactListTab extends Component {
<div className="address-book-wrapper"> <div className="address-book-wrapper">
{this.renderAddressBookContent()} {this.renderAddressBookContent()}
{this.renderContactContent()} {this.renderContactContent()}
{!addingContact && addressBook.length > 0 && this.renderAddButton()} {!addingContact && addressBook.length > 0
? this.renderAddButton()
: null}
</div> </div>
); );
} }

@ -25,8 +25,6 @@ export default class EditContact extends PureComponent {
memo: PropTypes.string, memo: PropTypes.string,
viewRoute: PropTypes.string, viewRoute: PropTypes.string,
listRoute: PropTypes.string, listRoute: PropTypes.string,
setAccountLabel: PropTypes.func,
showingMyAccounts: PropTypes.bool.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -52,8 +50,6 @@ export default class EditContact extends PureComponent {
memo, memo,
name, name,
removeFromAddressBook, removeFromAddressBook,
setAccountLabel,
showingMyAccounts,
viewRoute, viewRoute,
} = this.props; } = this.props;
@ -65,18 +61,16 @@ export default class EditContact extends PureComponent {
<div className="settings-page__content-row address-book__edit-contact"> <div className="settings-page__content-row address-book__edit-contact">
<div className="settings-page__header address-book__header--edit"> <div className="settings-page__header address-book__header--edit">
<Identicon address={address} diameter={60} /> <Identicon address={address} diameter={60} />
{showingMyAccounts ? null : ( <Button
<Button type="link"
type="link" className="settings-page__address-book-button"
className="settings-page__address-book-button" onClick={async () => {
onClick={async () => { await removeFromAddressBook(chainId, address);
await removeFromAddressBook(chainId, address); history.push(listRoute);
history.push(listRoute); }}
}} >
> {t('deleteAccount')}
{t('deleteAccount')} </Button>
</Button>
)}
</div> </div>
<div className="address-book__edit-contact__content"> <div className="address-book__edit-contact__content">
<div className="address-book__view-contact__group"> <div className="address-book__view-contact__group">
@ -157,12 +151,6 @@ export default class EditContact extends PureComponent {
this.state.newName || name, this.state.newName || name,
this.state.newMemo || memo, this.state.newMemo || memo,
); );
if (showingMyAccounts) {
setAccountLabel(
this.state.newAddress,
this.state.newName || name,
);
}
history.push(listRoute); history.push(listRoute);
} else { } else {
this.setState({ error: this.context.t('invalidAddress') }); this.setState({ error: this.context.t('invalidAddress') });
@ -174,9 +162,6 @@ export default class EditContact extends PureComponent {
this.state.newName || name, this.state.newName || name,
this.state.newMemo || memo, this.state.newMemo || memo,
); );
if (showingMyAccounts) {
setAccountLabel(address, this.state.newName || name);
}
history.push(listRoute); history.push(listRoute);
} }
}} }}

@ -9,7 +9,6 @@ import {
import { import {
addToAddressBook, addToAddressBook,
removeFromAddressBook, removeFromAddressBook,
setAccountLabel,
} from '../../../../store/actions'; } from '../../../../store/actions';
import EditContact from './edit-contact.component'; import EditContact from './edit-contact.component';
@ -44,8 +43,6 @@ const mapDispatchToProps = (dispatch) => {
dispatch(addToAddressBook(recipient, nickname, memo)), dispatch(addToAddressBook(recipient, nickname, memo)),
removeFromAddressBook: (chainId, addressToRemove) => removeFromAddressBook: (chainId, addressToRemove) =>
dispatch(removeFromAddressBook(chainId, addressToRemove)), dispatch(removeFromAddressBook(chainId, addressToRemove)),
setAccountLabel: (address, label) =>
dispatch(setAccountLabel(address, label)),
}; };
}; };

@ -233,6 +233,13 @@
margin-left: 1.875rem; margin-left: 1.875rem;
} }
&__inline-link {
@include H6;
display: initial;
padding: 0;
}
&--selected { &--selected {
.settings-page { .settings-page {
&__content { &__content {

@ -244,7 +244,7 @@ export default class NetworksTab extends PureComponent {
return ( return (
<div className="networks-tab__body"> <div className="networks-tab__body">
{isFullScreen && this.renderSubHeader()} {isFullScreen ? this.renderSubHeader() : null}
<div className="networks-tab__content"> <div className="networks-tab__content">
{this.renderNetworksTabContent()} {this.renderNetworksTabContent()}
{!isFullScreen && !shouldRenderNetworkForm ? ( {!isFullScreen && !shouldRenderNetworkForm ? (

@ -146,7 +146,7 @@ export default class SecurityTab extends PureComponent {
return ( return (
<div className="settings-page__body"> <div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>} {warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderSeedWords()} {this.renderSeedWords()}
{this.renderIncomingTransactionsOptIn()} {this.renderIncomingTransactionsOptIn()}
{this.renderPhishingDetectionToggle()} {this.renderPhishingDetectionToggle()}

@ -212,7 +212,7 @@ export default class SettingsTab extends PureComponent {
return ( return (
<div className="settings-page__body"> <div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>} {warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderCurrentConversion()} {this.renderCurrentConversion()}
{this.renderUsePrimaryCurrencyOptions()} {this.renderUsePrimaryCurrencyOptions()}
{this.renderCurrentLocale()} {this.renderCurrentLocale()}

@ -272,7 +272,7 @@ export default function AwaitingSwap({
<div className="awaiting-swap__main-descrption">{descriptionText}</div> <div className="awaiting-swap__main-descrption">{descriptionText}</div>
{content} {content}
</div> </div>
{!errorKey && swapComplete && <MakeAnotherSwap />} {!errorKey && swapComplete ? <MakeAnotherSwap /> : null}
<SwapsFooter <SwapsFooter
onSubmit={async () => { onSubmit={async () => {
if (errorKey === OFFLINE_FOR_MAINTENANCE) { if (errorKey === OFFLINE_FOR_MAINTENANCE) {

@ -52,13 +52,16 @@ export default function ItemList({
// If there is a token for import based on a contract address, it's the only one in the list. // If there is a token for import based on a contract address, it's the only one in the list.
const hasTokenForImport = results.length === 1 && results[0].notImported; const hasTokenForImport = results.length === 1 && results[0].notImported;
const placeholder = Placeholder ? (
<Placeholder searchQuery={searchQuery} />
) : null;
return results.length === 0 ? ( return results.length === 0 ? (
Placeholder && <Placeholder searchQuery={searchQuery} /> placeholder
) : ( ) : (
<div className="searchable-item-list"> <div className="searchable-item-list">
{listTitle && ( {listTitle ? (
<div className="searchable-item-list__title">{listTitle}</div> <div className="searchable-item-list__title">{listTitle}</div>
)} ) : null}
<div <div
className={classnames( className={classnames(
'searchable-item-list__list-container', 'searchable-item-list__list-container',
@ -100,43 +103,43 @@ export default function ItemList({
onKeyUp={(e) => e.key === 'Enter' && onClick()} onKeyUp={(e) => e.key === 'Enter' && onClick()}
key={`searchable-item-list-item-${i}`} key={`searchable-item-list-item-${i}`}
> >
{(iconUrl || primaryLabel) && ( {iconUrl || primaryLabel ? (
<UrlIcon url={iconUrl} name={primaryLabel} /> <UrlIcon url={iconUrl} name={primaryLabel} />
)} ) : null}
{!(iconUrl || primaryLabel) && identiconAddress && ( {!(iconUrl || primaryLabel) && identiconAddress ? (
<div className="searchable-item-list__identicon"> <div className="searchable-item-list__identicon">
<Identicon address={identiconAddress} diameter={24} /> <Identicon address={identiconAddress} diameter={24} />
</div> </div>
)} ) : null}
{IconComponent && <IconComponent />} {IconComponent ? <IconComponent /> : null}
<div className="searchable-item-list__labels"> <div className="searchable-item-list__labels">
<div className="searchable-item-list__item-labels"> <div className="searchable-item-list__item-labels">
{primaryLabel && ( {primaryLabel ? (
<span className="searchable-item-list__primary-label"> <span className="searchable-item-list__primary-label">
{primaryLabel} {primaryLabel}
</span> </span>
)} ) : null}
{secondaryLabel && ( {secondaryLabel ? (
<span className="searchable-item-list__secondary-label"> <span className="searchable-item-list__secondary-label">
{secondaryLabel} {secondaryLabel}
</span> </span>
)} ) : null}
</div> </div>
{!hideRightLabels && {!hideRightLabels &&
(rightPrimaryLabel || rightSecondaryLabel) && ( (rightPrimaryLabel || rightSecondaryLabel) ? (
<div className="searchable-item-list__right-labels"> <div className="searchable-item-list__right-labels">
{rightPrimaryLabel && ( {rightPrimaryLabel ? (
<span className="searchable-item-list__right-primary-label"> <span className="searchable-item-list__right-primary-label">
{rightPrimaryLabel} {rightPrimaryLabel}
</span> </span>
)} ) : null}
{rightSecondaryLabel && ( {rightSecondaryLabel ? (
<span className="searchable-item-list__right-secondary-label"> <span className="searchable-item-list__right-secondary-label">
{rightSecondaryLabel} {rightSecondaryLabel}
</span> </span>
)} ) : null}
</div> </div>
)} ) : null}
</div> </div>
{result.notImported && ( {result.notImported && (
<Button type="confirm" onClick={onClick}> <Button type="confirm" onClick={onClick}>

@ -1,4 +1,3 @@
import { stripHexPrefix } from 'ethereumjs-util';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addHexPrefix } from '../../app/scripts/lib/util'; import { addHexPrefix } from '../../app/scripts/lib/util';
import { import {
@ -8,7 +7,11 @@ import {
NETWORK_TYPE_RPC, NETWORK_TYPE_RPC,
NATIVE_CURRENCY_TOKEN_IMAGE_MAP, NATIVE_CURRENCY_TOKEN_IMAGE_MAP,
} from '../../shared/constants/network'; } from '../../shared/constants/network';
import { KEYRING_TYPES } from '../../shared/constants/hardware-wallets'; import {
KEYRING_TYPES,
WEBHID_CONNECTED_STATUSES,
LEDGER_TRANSPORT_TYPES,
} from '../../shared/constants/hardware-wallets';
import { import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP, SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
@ -36,7 +39,11 @@ import {
getConversionRate, getConversionRate,
isNotEIP1559Network, isNotEIP1559Network,
isEIP1559Network, isEIP1559Network,
getLedgerTransportType,
isAddressLedger,
findKeyringForAddress,
} from '../ducks/metamask/metamask'; } from '../ducks/metamask/metamask';
import { getLedgerWebHidConnectedStatus } from '../ducks/app/app';
/** /**
* One of the only remaining valid uses of selecting the network subkey of the * One of the only remaining valid uses of selecting the network subkey of the
@ -77,18 +84,15 @@ export function getCurrentKeyring(state) {
return null; return null;
} }
const simpleAddress = stripHexPrefix(identity.address).toLowerCase(); const keyring = findKeyringForAddress(state, identity.address);
const keyring = state.metamask.keyrings.find((kr) => {
return (
kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
);
});
return keyring; return keyring;
} }
export function getParticipateInMetaMetrics(state) {
return Boolean(state.metamask.participateInMetaMetrics);
}
export function isEIP1559Account(state) { export function isEIP1559Account(state) {
// Trezor does not support 1559 at this time // Trezor does not support 1559 at this time
const currentKeyring = getCurrentKeyring(state); const currentKeyring = getCurrentKeyring(state);
@ -642,3 +646,16 @@ export function getUseTokenDetection(state) {
export function getTokenList(state) { export function getTokenList(state) {
return state.metamask.tokenList; return state.metamask.tokenList;
} }
export function doesAddressRequireLedgerHidConnection(state, address) {
const addressIsLedger = isAddressLedger(state, address);
const transportTypePreferenceIsWebHID =
getLedgerTransportType(state) === LEDGER_TRANSPORT_TYPES.WEBHID;
const webHidIsNotConnected =
getLedgerWebHidConnectedStatus(state) !==
WEBHID_CONNECTED_STATUSES.CONNECTED;
return (
addressIsLedger && transportTypePreferenceIsWebHID && webHidIsNotConnected
);
}

@ -77,6 +77,10 @@ export const COMPLETE_ONBOARDING = 'COMPLETE_ONBOARDING';
export const SET_MOUSE_USER_STATE = 'SET_MOUSE_USER_STATE'; export const SET_MOUSE_USER_STATE = 'SET_MOUSE_USER_STATE';
// Ledger
export const SET_WEBHID_CONNECTED_STATUS = 'SET_WEBHID_CONNECTED_STATUS';
// Network // Network
export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS'; export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS';
export const CLEAR_PENDING_TOKENS = 'CLEAR_PENDING_TOKENS'; export const CLEAR_PENDING_TOKENS = 'CLEAR_PENDING_TOKENS';

@ -28,6 +28,10 @@ import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import {
LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../shared/constants/hardware-wallets';
import * as actionConstants from './actionConstants'; import * as actionConstants from './actionConstants';
let background = null; let background = null;
@ -395,15 +399,31 @@ export function forgetDevice(deviceName) {
}; };
} }
export function connectHardware(deviceName, page, hdPath) { export function connectHardware(deviceName, page, hdPath, t) {
log.debug(`background.connectHardware`, deviceName, page, hdPath); log.debug(`background.connectHardware`, deviceName, page, hdPath);
return async (dispatch) => { return async (dispatch, getState) => {
dispatch( dispatch(
showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`), showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`),
); );
let accounts; let accounts;
try { try {
const { ledgerTransportType } = getState().metamask;
if (
deviceName === 'ledger' &&
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID
) {
const connectedDevices = await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
const userApprovedWebHidConnection = connectedDevices.some(
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
if (!userApprovedWebHidConnection) {
throw new Error(t('ledgerWebHIDNotConnectedErrorMessage'));
}
}
accounts = await promisifiedBackground.connectHardware( accounts = await promisifiedBackground.connectHardware(
deviceName, deviceName,
page, page,
@ -2745,7 +2765,7 @@ export function getCurrentWindowTab() {
export function setLedgerLivePreference(value) { export function setLedgerLivePreference(value) {
return async (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
await promisifiedBackground.setLedgerLivePreference(value); await promisifiedBackground.setLedgerTransportPreference(value);
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
}; };
} }

@ -2821,10 +2821,10 @@
resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81" resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81"
integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw== integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw==
"@metamask/eth-ledger-bridge-keyring@^0.7.0": "@metamask/eth-ledger-bridge-keyring@^0.9.0":
version "0.7.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.7.0.tgz#7d80e1e3dfab91ba2b6a1a2a5e352320e948b568" resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.9.0.tgz#42e98e7dfeaaa08e7c9ceff261facddd7320df80"
integrity sha512-0UOEb/c3/fkatDK+se3gOHaGQ0RTRLbG5DqsoeowZ/JcO4wcMxBhOiIgOY4domOqUTekKKVPNC7Pc0mHpM9sAQ== integrity sha512-EuNKvodbdJxQPzr+zAE5TE1iKUzuIRWKeVaYoYwpi18RjjtSQMKmZcb3VXY8hmQu+Fj4Ld/ujj22qSYjYAjtPg==
dependencies: dependencies:
"@ethereumjs/tx" "^3.2.0" "@ethereumjs/tx" "^3.2.0"
eth-sig-util "^2.0.0" eth-sig-util "^2.0.0"

Loading…
Cancel
Save