Merge remote-tracking branch 'origin/develop' into Version-v8.0.0

* origin/develop: (58 commits)
  Fix site icon fallback letter (#8815)
  add hover style to list-item (#8813)
  Fix site icon size (#8814)
  Consolidate connected account alerts (#8802)
  lowercase web3
  Use markdown-to-jsx@6.11.4 (#8809)
  Update app/_locales/en/messages.json
  Update app/_locales/en/messages.json
  also remove 'dapp' from descriptions
  remove all user-facing instances of 'dapp'
  update button styling on home/asset page (#8800)
  Fix handling of permissions of removed accounts (#8803)
  Clear permssions during createNewVaultAndRestore (#8804)
  Hide token transfers on ETH asset page (#8799)
  Fix account name editing (#8801)
  Fix connect flow account list height (#8798)
  Update color of menu item icons (#8797)
  Update "Connected accounts" empty description (#8796)
  Stop reporting failed transactions to Sentry (#8795)
  Omit state snapshot from Sentry errors (#8794)
  ...
feature/default_network_editable
Mark Stacey 5 years ago
commit 584ff9ec8a
  1. 1
      .eslintrc.js
  2. 3
      app/_locales/am/messages.json
  3. 3
      app/_locales/ar/messages.json
  4. 3
      app/_locales/bg/messages.json
  5. 3
      app/_locales/bn/messages.json
  6. 3
      app/_locales/ca/messages.json
  7. 3
      app/_locales/da/messages.json
  8. 3
      app/_locales/de/messages.json
  9. 3
      app/_locales/el/messages.json
  10. 103
      app/_locales/en/messages.json
  11. 3
      app/_locales/es/messages.json
  12. 3
      app/_locales/es_419/messages.json
  13. 3
      app/_locales/et/messages.json
  14. 3
      app/_locales/fa/messages.json
  15. 3
      app/_locales/fi/messages.json
  16. 3
      app/_locales/fil/messages.json
  17. 3
      app/_locales/fr/messages.json
  18. 3
      app/_locales/he/messages.json
  19. 3
      app/_locales/hi/messages.json
  20. 3
      app/_locales/hr/messages.json
  21. 3
      app/_locales/ht/messages.json
  22. 3
      app/_locales/hu/messages.json
  23. 3
      app/_locales/id/messages.json
  24. 9
      app/_locales/it/messages.json
  25. 3
      app/_locales/kn/messages.json
  26. 3
      app/_locales/ko/messages.json
  27. 3
      app/_locales/lt/messages.json
  28. 3
      app/_locales/lv/messages.json
  29. 3
      app/_locales/ms/messages.json
  30. 3
      app/_locales/no/messages.json
  31. 3
      app/_locales/pl/messages.json
  32. 3
      app/_locales/pt_BR/messages.json
  33. 3
      app/_locales/ro/messages.json
  34. 3
      app/_locales/ru/messages.json
  35. 3
      app/_locales/sk/messages.json
  36. 3
      app/_locales/sl/messages.json
  37. 3
      app/_locales/sr/messages.json
  38. 3
      app/_locales/sv/messages.json
  39. 3
      app/_locales/sw/messages.json
  40. 3
      app/_locales/uk/messages.json
  41. 3
      app/_locales/zh_CN/messages.json
  42. 3
      app/_locales/zh_TW/messages.json
  43. 2
      app/images/icons/connected-sites.svg
  44. 2
      app/images/icons/disconnect.svg
  45. 16
      app/scripts/background.js
  46. 19
      app/scripts/controllers/alert.js
  47. 18
      app/scripts/controllers/permissions/index.js
  48. 3
      app/scripts/controllers/token-rates.js
  49. 2
      app/scripts/controllers/transactions/tx-gas-utils.js
  50. 14
      app/scripts/lib/reportFailedTxToSentry.js
  51. 8
      app/scripts/lib/setupSentry.js
  52. 8
      app/scripts/metamask-controller.js
  53. 14
      app/scripts/ui.js
  54. 3
      development/states/confirm-sig-requests.json
  55. 3
      development/states/currency-localization.json
  56. 3
      development/states/tx-list-items.json
  57. 2
      package.json
  58. 2
      test/e2e/address-book.spec.js
  59. 8
      test/e2e/from-import-ui.spec.js
  60. 2
      test/e2e/metamask-responsive-ui.spec.js
  61. 2
      test/e2e/metamask-ui.spec.js
  62. 8
      test/e2e/send-edit.spec.js
  63. 2
      test/e2e/tests/simple-send.spec.js
  64. 12
      test/e2e/threebox.spec.js
  65. 6
      test/e2e/web3.spec.js
  66. 5
      test/unit/app/controllers/metamask-controller-test.js
  67. 106
      test/unit/app/controllers/permissions/permissions-controller-test.js
  68. 2
      test/unit/app/controllers/permissions/permissions-log-controller-test.js
  69. 10
      test/unit/app/controllers/token-rates-controller.js
  70. 13
      test/unit/ui/app/actions.spec.js
  71. 4
      ui/app/components/app/account-menu/account-menu.component.js
  72. 60
      ui/app/components/app/add-token-button/add-token-button.component.js
  73. 20
      ui/app/components/app/add-token-button/index.scss
  74. 7
      ui/app/components/app/alerts/alerts.js
  75. 2
      ui/app/components/app/alerts/alerts.scss
  76. 1
      ui/app/components/app/alerts/switch-to-connected-alert/index.js
  77. 121
      ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.js
  78. 66
      ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.scss
  79. 123
      ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js
  80. 36
      ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss
  81. 11
      ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js
  82. 1
      ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js
  83. 133
      ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js
  84. 88
      ui/app/components/app/connected-accounts-list/index.scss
  85. 2
      ui/app/components/app/connected-accounts-permissions/connected-accounts-permissions.component.js
  86. 1
      ui/app/components/app/connected-accounts-permissions/index.js
  87. 64
      ui/app/components/app/connected-accounts-permissions/index.scss
  88. 4
      ui/app/components/app/connected-sites-list/connected-sites-list.component.js
  89. 8
      ui/app/components/app/index.scss
  90. 2
      ui/app/components/app/menu-bar/index.scss
  91. 115
      ui/app/components/app/permission-page-container/index.scss
  92. 88
      ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js
  93. 37
      ui/app/components/app/permission-page-container/permission-page-container.component.js
  94. 7
      ui/app/components/app/permissions-connect-header/permissions-connect-header.component.js
  95. 39
      ui/app/components/app/token-cell/token-cell.js
  96. 6
      ui/app/components/app/token-cell/token-cell.test.js
  97. 10
      ui/app/components/app/token-list/token-list.js
  98. 1
      ui/app/components/app/transaction-icon/index.js
  99. 58
      ui/app/components/app/transaction-icon/transaction-icon.js
  100. 25
      ui/app/components/app/transaction-list-item/index.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -23,6 +23,7 @@ module.exports = {
'@metamask/eslint-config/config/nodejs', '@metamask/eslint-config/config/nodejs',
'@metamask/eslint-config/config/mocha', '@metamask/eslint-config/config/mocha',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react-hooks/recommended',
], ],
plugins: [ plugins: [

@ -648,9 +648,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "የ MetaMask ስሪት" "message": "የ MetaMask ስሪት"
}, },
"missingYourTokens": {
"message": "ተለዋጭ ስሞችዎን አላዩም?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "እርስዎ መሆንዎትን ለማረጋገጥ እባከዎ የይለፍ ቃልዎን ያስገቡ!" "message": "እርስዎ መሆንዎትን ለማረጋገጥ እባከዎ የይለፍ ቃልዎን ያስገቡ!"
}, },

@ -644,9 +644,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "إصدار MetaMask " "message": "إصدار MetaMask "
}, },
"missingYourTokens": {
"message": "لا تجد رموزك؟"
},
"mobileSyncText": { "mobileSyncText": {
"message": "يرجى إدخال كلمة مرورك لتأكيد هويتك!" "message": "يرجى إدخال كلمة مرورك لتأكيد هويتك!"
}, },

@ -647,9 +647,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Версия на MetaMask" "message": "Версия на MetaMask"
}, },
"missingYourTokens": {
"message": "Не виждате жетоните си?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Моля, въведете вашата парола, за да потвърдите, че сте вие!" "message": "Моля, въведете вашата парола, за да потвърдите, че сте вие!"
}, },

@ -651,9 +651,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask সকরণ" "message": "MetaMask সকরণ"
}, },
"missingYourTokens": {
"message": "আপনর টনগিখতন ন?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "এটি আপনিিিত করত অনরহ কর আপনর পসওয়ড লিন!" "message": "এটি আপনিিিত করত অনরহ কর আপনর পসওয়ড লিন!"
}, },

@ -635,9 +635,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versió MetaMask" "message": "Versió MetaMask"
}, },
"missingYourTokens": {
"message": "No veus els teus tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Si us plau, introdueix la teva contrasenya per confirmar que ets tu!" "message": "Si us plau, introdueix la teva contrasenya per confirmar que ets tu!"
}, },

@ -635,9 +635,6 @@
"metamaskDescription": { "metamaskDescription": {
"message": "Som forbinder dig til Ethereum og de decentraliserede internet." "message": "Som forbinder dig til Ethereum og de decentraliserede internet."
}, },
"missingYourTokens": {
"message": "Kan du ikke se dine tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Indtast din adgangskode for at bekræfte, at det er dig!" "message": "Indtast din adgangskode for at bekræfte, at det er dig!"
}, },

@ -627,9 +627,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask-Version" "message": "MetaMask-Version"
}, },
"missingYourTokens": {
"message": "Sie können Ihre Token nicht sehen?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Bitte geben Sie Ihr Passwort ein, um Ihre Identität zu verifizieren!" "message": "Bitte geben Sie Ihr Passwort ein, um Ihre Identität zu verifizieren!"
}, },

@ -648,9 +648,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Έκδοση MetaMask " "message": "Έκδοση MetaMask "
}, },
"missingYourTokens": {
"message": "Δεν βλέπετε τα διακριτικά σας;"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Παρακαλούμε δώστε τον κωδικό πρόσβασής σας για να επιβεβαιώσετε ότι είστε εσείς!" "message": "Παρακαλούμε δώστε τον κωδικό πρόσβασής σας για να επιβεβαιώσετε ότι είστε εσείς!"
}, },

@ -26,13 +26,10 @@
"description": "$1 is the number of accounts" "description": "$1 is the number of accounts"
}, },
"connectedAccountsEmptyDescription": { "connectedAccountsEmptyDescription": {
"message": "MetaMask is not connected this site. To connect to a decentralized app (dapp), find the connect button on their site." "message": "MetaMask is not connected this site. To connect to a web3 site, find the connect button on their site."
}, },
"primary": { "currentAccountNotConnected": {
"message": "Primary" "message": "Your current account is not connected"
},
"lastActive": {
"message": "Last active"
}, },
"switchToThisAccount": { "switchToThisAccount": {
"message": "Switch to this account" "message": "Switch to this account"
@ -73,12 +70,6 @@
"showIncomingTransactionsDescription": { "showIncomingTransactionsDescription": {
"message": "Select this to use Etherscan to show incoming transactions in the transactions list" "message": "Select this to use Etherscan to show incoming transactions in the transactions list"
}, },
"cancelling": {
"message": "Cancelling..."
},
"cancelledConnectionWithMetaMask": {
"message": "Cancelled Connection With MetaMask"
},
"chartOnlyAvailableEth": { "chartOnlyAvailableEth": {
"message": "Chart only available on Ethereum networks." "message": "Chart only available on Ethereum networks."
}, },
@ -88,12 +79,9 @@
"connectWithMetaMask": { "connectWithMetaMask": {
"message": "Connect With MetaMask" "message": "Connect With MetaMask"
}, },
"connectingWithMetaMask": {
"message": "Connecting With MetaMask..."
},
"connectTo": { "connectTo": {
"message": "Connect to $1", "message": "Connect to $1",
"description": "$1 is the name/origin of a site/dapp that the user can connect to metamask" "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask"
}, },
"connectToAll": { "connectToAll": {
"message": "Connect to all your $1", "message": "Connect to all your $1",
@ -109,7 +97,7 @@
}, },
"connectToMultipleNumberOfAccounts": { "connectToMultipleNumberOfAccounts": {
"message": "$1 accounts", "message": "$1 accounts",
"description": "$1 is the number of accounts to which the site/dapp is asking to connect; this will substitute $1 in connectToMultiple" "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple"
}, },
"contractInteraction": { "contractInteraction": {
"message": "Contract Interaction" "message": "Contract Interaction"
@ -148,6 +136,12 @@
"accountSelectionRequired": { "accountSelectionRequired": {
"message": "You need to select an account!" "message": "You need to select an account!"
}, },
"active": {
"message": "Active"
},
"activity": {
"message": "Activity"
},
"activityLog": { "activityLog": {
"message": "activity log" "message": "activity log"
}, },
@ -193,17 +187,11 @@
"alertsSettingsDescription": { "alertsSettingsDescription": {
"message": "Enable or disable each alert" "message": "Enable or disable each alert"
}, },
"alertSettingsSwitchToConnected": {
"message": "Opening popup with an unconnected account selected"
},
"alertSettingsSwitchToConnectedDescription": {
"message": "This alert is shown when you open the popup with an unconnected account selected."
},
"alertSettingsUnconnectedAccount": { "alertSettingsUnconnectedAccount": {
"message": "Switching to an unconnected account" "message": "Browsing a website with an unconnected account selected"
}, },
"alertSettingsUnconnectedAccountDescription": { "alertSettingsUnconnectedAccountDescription": {
"message": "This alert is shown in the popup when you switch from a connected account to an unconnected account." "message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected."
}, },
"allowOriginSpendToken": { "allowOriginSpendToken": {
"message": "Allow $1 to spend your $2?", "message": "Allow $1 to spend your $2?",
@ -242,6 +230,9 @@
"asset": { "asset": {
"message": "Asset" "message": "Asset"
}, },
"assets": {
"message": "Assets"
},
"attemptingConnect": { "attemptingConnect": {
"message": "Attempting to connect to blockchain." "message": "Attempting to connect to blockchain."
}, },
@ -370,7 +361,7 @@
"message": "Confirmed" "message": "Confirmed"
}, },
"confirmPassword": { "confirmPassword": {
"message": "Confirm Password" "message": "Confirm password"
}, },
"confirmSecretBackupPhrase": { "confirmSecretBackupPhrase": {
"message": "Confirm your Secret Backup Phrase" "message": "Confirm your Secret Backup Phrase"
@ -784,10 +775,10 @@
"message": " Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts " "message": " Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts "
}, },
"importAccountSeedPhrase": { "importAccountSeedPhrase": {
"message": "Import an Account with Seed Phrase" "message": "Import an account with seed phrase"
}, },
"importWallet": { "importWallet": {
"message": "Import Wallet" "message": "Import wallet"
}, },
"importYourExisting": { "importYourExisting": {
"message": "Import your existing wallet using a 12 word seed phrase" "message": "Import your existing wallet using a 12 word seed phrase"
@ -929,9 +920,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask Version" "message": "MetaMask Version"
}, },
"missingYourTokens": {
"message": "Don't see your tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Please enter your password to confirm it's you!" "message": "Please enter your password to confirm it's you!"
}, },
@ -986,7 +974,7 @@
"message": "New Contract" "message": "New Contract"
}, },
"newPassword": { "newPassword": {
"message": "New Password (min 8 chars)" "message": "New password (min 8 chars)"
}, },
"newNetwork": { "newNetwork": {
"message": "New Network" "message": "New Network"
@ -997,9 +985,6 @@
"noAlreadyHaveSeed": { "noAlreadyHaveSeed": {
"message": "No, I already have a seed phrase" "message": "No, I already have a seed phrase"
}, },
"notConnected": {
"message": "Not connected"
},
"protectYourKeys": { "protectYourKeys": {
"message": "Protect Your Keys!" "message": "Protect Your Keys!"
}, },
@ -1015,6 +1000,9 @@
"onlyConnectTrust": { "onlyConnectTrust": {
"message": "Only connect with sites you trust." "message": "Only connect with sites you trust."
}, },
"onlyAddTrustedNetworks": {
"message": "A malicious Ethereum network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust."
},
"optionalChainId": { "optionalChainId": {
"message": "ChainID (optional)" "message": "ChainID (optional)"
}, },
@ -1090,7 +1078,7 @@
"description": "For importing an account from a private key" "description": "For importing an account from a private key"
}, },
"pending": { "pending": {
"message": "pending" "message": "Pending"
}, },
"permissionCheckedIconDescription": { "permissionCheckedIconDescription": {
"message": "You have approved this permission" "message": "You have approved this permission"
@ -1281,6 +1269,12 @@
"seedPhrasePlaceholder": { "seedPhrasePlaceholder": {
"message": "Separate each word with a single space" "message": "Separate each word with a single space"
}, },
"seedPhrasePlaceholderPaste": {
"message": "Paste seed phrase from clipboard"
},
"showSeedPhrase": {
"message": "Show seed phrase"
},
"seedPhraseReq": { "seedPhraseReq": {
"message": "Seed phrases contain 12, 15, 18, 21, or 24 words" "message": "Seed phrases contain 12, 15, 18, 21, or 24 words"
}, },
@ -1305,6 +1299,9 @@
"selectType": { "selectType": {
"message": "Select Type" "message": "Select Type"
}, },
"buy": {
"message": "Buy"
},
"send": { "send": {
"message": "Send" "message": "Send"
}, },
@ -1455,10 +1452,10 @@
"message": "Select the account you want to view. You can only choose one at a time." "message": "Select the account you want to view. You can only choose one at a time."
}, },
"step3HardwareWallet": { "step3HardwareWallet": {
"message": "3. Start using dApps and more!" "message": "3. Start using web3 sites and more!"
}, },
"step3HardwareWalletMsg": { "step3HardwareWalletMsg": {
"message": "Use your hardware account like you would with any Ethereum account. Connect to dApps, send ETH, buy and store ERC20 tokens and non-fungible tokens like CryptoKitties." "message": "Use your hardware account like you would with any Ethereum account. Connect to web3 sites, send ETH, buy and store ERC20 tokens and non-fungible tokens like CryptoKitties."
}, },
"storePhrase": { "storePhrase": {
"message": "Store this phrase in a password manager like 1Password." "message": "Store this phrase in a password manager like 1Password."
@ -1472,16 +1469,6 @@
"supportCenter": { "supportCenter": {
"message": "Visit our Support Center" "message": "Visit our Support Center"
}, },
"switchAccounts": {
"message": "Switch accounts"
},
"switchToConnectedAlertMultipleAccountsDescription": {
"message": "This account is not connected. Switch to a connected account?"
},
"switchToConnectedAlertSingleAccountDescription": {
"message": "This account is not connected. Switch to a connected account ($1)?",
"description": "$1 will be replaced by the name of the connected account"
},
"symbol": { "symbol": {
"message": "Symbol" "message": "Symbol"
}, },
@ -1600,6 +1587,9 @@
"transactionTime": { "transactionTime": {
"message": "Transaction Time" "message": "Transaction Time"
}, },
"showTransactionTimeDescription": {
"message": "Select this to display pending transaction time estimates in the activity tab while on the Main Ethereum Network. Note: estimates are approximations based on network conditions."
},
"transfer": { "transfer": {
"message": "Transfer" "message": "Transfer"
}, },
@ -1626,10 +1616,7 @@
"unapproved": { "unapproved": {
"message": "Unapproved" "message": "Unapproved"
}, },
"unconnectedAccountAlertDescription": { "alertDisableTooltip": {
"message": "$1 is not connected to $2."
},
"unconnectedAccountAlertDisableTooltip": {
"message": "This can be changed in \"Settings > Alerts\"" "message": "This can be changed in \"Settings > Alerts\""
}, },
"units": { "units": {
@ -1702,7 +1689,7 @@
"message": "Visit our web site" "message": "Visit our web site"
}, },
"walletSeed": { "walletSeed": {
"message": "Wallet Seed" "message": "Seed phrase"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome Back!" "message": "Welcome Back!"
@ -1746,7 +1733,7 @@
}, },
"decryptMessageNotice": { "decryptMessageNotice": {
"message": "$1 would like to read this message to complete your action", "message": "$1 would like to read this message to complete your action",
"description": "$1 is website or dapp name" "description": "$1 is the web3 site name"
}, },
"decryptMetamask": { "decryptMetamask": {
"message": "Decrypt message" "message": "Decrypt message"
@ -1766,12 +1753,6 @@
}, },
"encryptionPublicKeyNotice": { "encryptionPublicKeyNotice": {
"message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.", "message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.",
"description": "$1 is website or dapp name" "description": "$1 is the web3 site name"
},
"thisSite": {
"message": "this site"
},
"thisAccount": {
"message": "This account"
} }
} }

@ -527,9 +527,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versión de MetaMask" "message": "Versión de MetaMask"
}, },
"missingYourTokens": {
"message": "¿No ves tus tokens?"
},
"myAccounts": { "myAccounts": {
"message": "Mis cuentas" "message": "Mis cuentas"
}, },

@ -636,9 +636,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versión de MetaMask" "message": "Versión de MetaMask"
}, },
"missingYourTokens": {
"message": "¿No puedes ver tus tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Ingresa tu contraseña para confirmar que eres tú." "message": "Ingresa tu contraseña para confirmar que eres tú."
}, },

@ -641,9 +641,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMaski versioon" "message": "MetaMaski versioon"
}, },
"missingYourTokens": {
"message": "Te ei näe oma lube?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Sisestage parool, et saaksime teid tuvastada!" "message": "Sisestage parool, et saaksime teid tuvastada!"
}, },

@ -651,9 +651,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "نسخه MetaMask" "message": "نسخه MetaMask"
}, },
"missingYourTokens": {
"message": "آیا رمزیاب های تان را نمیبیند؟"
},
"mobileSyncText": { "mobileSyncText": {
"message": "لطفًا رمز عبور را وارد نمایید تا تأیید شود که خود شما هستید!" "message": "لطفًا رمز عبور را وارد نمایید تا تأیید شود که خود شما هستید!"
}, },

@ -648,9 +648,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask-versio" "message": "MetaMask-versio"
}, },
"missingYourTokens": {
"message": "Etkö näe tietueitasi?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Vahvista henkilöllisyytesi antamalla salasanasi!" "message": "Vahvista henkilöllisyytesi antamalla salasanasi!"
}, },

@ -589,9 +589,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Bersyon ng MetaMask" "message": "Bersyon ng MetaMask"
}, },
"missingYourTokens": {
"message": "Hindi mo ba nakikita ang iyong mga token?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Pakilagay ang iyong password para kumpirmahing ikaw ito!" "message": "Pakilagay ang iyong password para kumpirmahing ikaw ito!"
}, },

@ -633,9 +633,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Version de MetaMask" "message": "Version de MetaMask"
}, },
"missingYourTokens": {
"message": "Vous ne voyez pas vos jetons ?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Veuillez entrer votre mot de passe pour confirmer que c'est bien vous !" "message": "Veuillez entrer votre mot de passe pour confirmer que c'est bien vous !"
}, },

@ -648,9 +648,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "גרסת MetaMask" "message": "גרסת MetaMask"
}, },
"missingYourTokens": {
"message": "אין באפשרותך לראות את הטוקנים שלך?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "נא להזין את הססמה שלך כדי לאשר שזה/זו אכן את/ה!" "message": "נא להזין את הססמה שלך כדי לאשר שזה/זו אכן את/ה!"
}, },

@ -648,9 +648,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask ककरण" "message": "MetaMask ककरण"
}, },
"missingYourTokens": {
"message": "अपनकन कख नह रह?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "यह आप ह इसकि करनिए कपय अपनसवरड दरज कर!" "message": "यह आप ह इसकि करनिए कपय अपनसवरड दरज कर!"
}, },

@ -644,9 +644,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Inačica usluge MetaMask" "message": "Inačica usluge MetaMask"
}, },
"missingYourTokens": {
"message": "Ne vidite svoje tokene?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Upišite svoju lozinku kako biste potvrdili da ste to vi!" "message": "Upišite svoju lozinku kako biste potvrdili da ste to vi!"
}, },

@ -383,9 +383,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask Vèsyon" "message": "MetaMask Vèsyon"
}, },
"missingYourTokens": {
"message": "Ou pa wè token ou a?"
},
"myAccounts": { "myAccounts": {
"message": "Kont mwen" "message": "Kont mwen"
}, },

@ -644,9 +644,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask verzió" "message": "MetaMask verzió"
}, },
"missingYourTokens": {
"message": "Nem látja az érméit?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Kérünk írd be jelszavad, hogy igazold kiléted!" "message": "Kérünk írd be jelszavad, hogy igazold kiléted!"
}, },

@ -632,9 +632,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versi MetaMask" "message": "Versi MetaMask"
}, },
"missingYourTokens": {
"message": "Tidak melihat token Anda?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Harap masukkan sandi untuk mengonfirmasi ini memang Anda!" "message": "Harap masukkan sandi untuk mengonfirmasi ini memang Anda!"
}, },

@ -5,18 +5,12 @@
"showIncomingTransactionsDescription": { "showIncomingTransactionsDescription": {
"message": "Usa Etherscan per visualizzare le transazioni in ingresso nella lista delle transazioni" "message": "Usa Etherscan per visualizzare le transazioni in ingresso nella lista delle transazioni"
}, },
"cancelledConnectionWithMetaMask": {
"message": "Transazioni Annullate con MetaMask"
},
"chartOnlyAvailableEth": { "chartOnlyAvailableEth": {
"message": "Grafico disponibile solo per le reti Ethereum." "message": "Grafico disponibile solo per le reti Ethereum."
}, },
"connectedSites": { "connectedSites": {
"message": "Siti connessi" "message": "Siti connessi"
}, },
"connectingWithMetaMask": {
"message": "Connettendo con MetaMask..."
},
"connectTo": { "connectTo": {
"message": "Collegati a $1", "message": "Collegati a $1",
"description": "$1 is the name/origin of a site/dapp that the user can connect to metamask" "description": "$1 is the name/origin of a site/dapp that the user can connect to metamask"
@ -789,9 +783,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "versione di MetaMask" "message": "versione di MetaMask"
}, },
"missingYourTokens": {
"message": "Non vedi i tuoi token?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Per favore inserisci la password per confermare che sei te!" "message": "Per favore inserisci la password per confermare che sei te!"
}, },

@ -651,9 +651,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask ಆವಿ" "message": "MetaMask ಆವಿ"
}, },
"missingYourTokens": {
"message": "ನಿಮ ಟಕನಗಳಿಿಲವ?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "ಇದದನ ಖಚಿತಪಡಿಸಲ ದಯವಿಿಮ ಪವರ ಅನ ನಮಿಿ!" "message": "ಇದದನ ಖಚಿತಪಡಿಸಲ ದಯವಿಿಮ ಪವರ ಅನ ನಮಿಿ!"
}, },

@ -645,9 +645,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "메타마스크 버전" "message": "메타마스크 버전"
}, },
"missingYourTokens": {
"message": "당신의 토큰이 보이지 않나요?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "본인 여부를 확인하기 위해 비밀번호를 입력해 주세요!" "message": "본인 여부를 확인하기 위해 비밀번호를 입력해 주세요!"
}, },

@ -651,9 +651,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "„MetaMask“ versija" "message": "„MetaMask“ versija"
}, },
"missingYourTokens": {
"message": "Nematote savo žetonų?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Įveskite savo slaptažodį, kad patvirtintumėte, jog tai tikrai jūs!" "message": "Įveskite savo slaptažodį, kad patvirtintumėte, jog tai tikrai jūs!"
}, },

@ -647,9 +647,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask versija" "message": "MetaMask versija"
}, },
"missingYourTokens": {
"message": "Neredzat savus marķierus?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Lūdzu, ievadiet paroli, lai apstiprinātu, ka tas esat jūs." "message": "Lūdzu, ievadiet paroli, lai apstiprinātu, ka tas esat jūs."
}, },

@ -625,9 +625,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versi MetaMask" "message": "Versi MetaMask"
}, },
"missingYourTokens": {
"message": "Tidak nampak token anda?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Sila masukkan kata laluan anda untuk mengesahkan ini memang anda!" "message": "Sila masukkan kata laluan anda untuk mengesahkan ini memang anda!"
}, },

@ -641,9 +641,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask-versjon " "message": "MetaMask-versjon "
}, },
"missingYourTokens": {
"message": "Ser du ikke sjetongene dine?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Vennligst skriv inn passordet ditt for å bekrefte at det er deg!" "message": "Vennligst skriv inn passordet ditt for å bekrefte at det er deg!"
}, },

@ -645,9 +645,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Wersja MetaMask" "message": "Wersja MetaMask"
}, },
"missingYourTokens": {
"message": "Nie widzisz swoich token?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Wpisz hasło, aby potwierdzić, że to Ty!" "message": "Wpisz hasło, aby potwierdzić, że to Ty!"
}, },

@ -639,9 +639,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versão do MetaMask" "message": "Versão do MetaMask"
}, },
"missingYourTokens": {
"message": "Não está vendo seus tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Insira sua senha para confirmar que é você!" "message": "Insira sua senha para confirmar que é você!"
}, },

@ -638,9 +638,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Versiune MetaMask" "message": "Versiune MetaMask"
}, },
"missingYourTokens": {
"message": "Nu vă vedeți token-urile?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Vă rugăm introduceți parola pentru a vă confirma identitatea!" "message": "Vă rugăm introduceți parola pentru a vă confirma identitatea!"
}, },

@ -868,9 +868,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Версия MetaMask" "message": "Версия MetaMask"
}, },
"missingYourTokens": {
"message": "Не видите свои токены?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Пожалуйста, введите ваш пароль, чтобы подтвердить, что это вы!" "message": "Пожалуйста, введите ваш пароль, чтобы подтвердить, что это вы!"
}, },

@ -620,9 +620,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Verzia MetaMask" "message": "Verzia MetaMask"
}, },
"missingYourTokens": {
"message": "Nevidíte svoje tokeny?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Zadajte svoje heslo a potvrďte, že ste to vy!" "message": "Zadajte svoje heslo a potvrďte, že ste to vy!"
}, },

@ -636,9 +636,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Različica" "message": "Različica"
}, },
"missingYourTokens": {
"message": "Ne vidite vaših žetonov?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Vnesite geslo in potrdite, da ste to vi." "message": "Vnesite geslo in potrdite, da ste to vi."
}, },

@ -642,9 +642,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask verzija" "message": "MetaMask verzija"
}, },
"missingYourTokens": {
"message": "Ne vidite svoje tokene?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Molimo vas unesite šifru kako biste potvrdili da ste to vi!" "message": "Molimo vas unesite šifru kako biste potvrdili da ste to vi!"
}, },

@ -635,9 +635,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask-version" "message": "MetaMask-version"
}, },
"missingYourTokens": {
"message": "Ser du inte dina tokens?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Var vänlig ange ditt lösenord för att bekräfta att det är du!" "message": "Var vänlig ange ditt lösenord för att bekräfta att det är du!"
}, },

@ -629,9 +629,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Toleo la MetaMask" "message": "Toleo la MetaMask"
}, },
"missingYourTokens": {
"message": "Je, huoni vianzio vyako?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Tafadhali ingiza nenosiri lako ili kuthibitisha kuwa ni wewe!" "message": "Tafadhali ingiza nenosiri lako ili kuthibitisha kuwa ni wewe!"
}, },

@ -651,9 +651,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "Версія MetaMask" "message": "Версія MetaMask"
}, },
"missingYourTokens": {
"message": "Не бачите ваших токенів?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "Введіть пароль, щоб підтвердити свою особу!" "message": "Введіть пароль, щоб підтвердити свою особу!"
}, },

@ -639,9 +639,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask 版本" "message": "MetaMask 版本"
}, },
"missingYourTokens": {
"message": "无法看到您的代币?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "请输入密码确认个人身份!" "message": "请输入密码确认个人身份!"
}, },

@ -645,9 +645,6 @@
"metamaskVersion": { "metamaskVersion": {
"message": "MetaMask 版本" "message": "MetaMask 版本"
}, },
"missingYourTokens": {
"message": "看不到您的代幣?"
},
"mobileSyncText": { "mobileSyncText": {
"message": "請輸入密碼確認身份!" "message": "請輸入密碼確認身份!"
}, },

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="#24292E"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="#6A737D"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

@ -1,3 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9717 11.5576C13.6494 10.5858 14.0469 9.40401 14.0469 8.12939C14.0469 4.81569 11.3606 2.12939 8.04688 2.12939C6.77226 2.12939 5.59048 2.52684 4.61869 3.20457L6.70652 5.2924C7.09817 5.10496 7.53683 5 8 5C9.65685 5 11 6.34315 11 8C11 8.46317 10.895 8.90183 10.7076 9.29348L12.9717 11.5576ZM12.9987 14.413L14.142 15.5563C14.5325 15.9468 15.1657 15.9468 15.5562 15.5563C15.9468 15.1658 15.9468 14.5326 15.5562 14.1421L14.4026 12.9885C15.434 11.6415 16.0469 9.957 16.0469 8.12939C16.0469 3.71112 12.4652 0.129395 8.04688 0.129395C6.21927 0.129395 4.5348 0.742241 3.18776 1.77364L2.1212 0.707079C1.73067 0.316555 1.09751 0.316555 0.706986 0.707079C0.316461 1.0976 0.316461 1.73077 0.706986 2.12129L1.76322 3.17753C0.688345 4.53962 0.046875 6.25959 0.046875 8.12939C0.046875 12.5477 3.6286 16.1294 8.04688 16.1294C9.91667 16.1294 11.6367 15.4879 12.9987 14.413ZM11.5713 12.9857C10.5818 13.7051 9.36389 14.1294 8.04688 14.1294C4.73317 14.1294 2.04688 11.4431 2.04688 8.12939C2.04688 6.81238 2.47121 5.59447 3.19062 4.60493L5.29234 6.70665C5.10494 7.09827 5 7.53688 5 8C5 9.65685 6.34315 11 8 11C8.46312 11 8.90173 10.8951 9.29335 10.7077L11.5713 12.9857Z" fill="#24292E"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M12.9717 11.5576C13.6494 10.5858 14.0469 9.40401 14.0469 8.12939C14.0469 4.81569 11.3606 2.12939 8.04688 2.12939C6.77226 2.12939 5.59048 2.52684 4.61869 3.20457L6.70652 5.2924C7.09817 5.10496 7.53683 5 8 5C9.65685 5 11 6.34315 11 8C11 8.46317 10.895 8.90183 10.7076 9.29348L12.9717 11.5576ZM12.9987 14.413L14.142 15.5563C14.5325 15.9468 15.1657 15.9468 15.5562 15.5563C15.9468 15.1658 15.9468 14.5326 15.5562 14.1421L14.4026 12.9885C15.434 11.6415 16.0469 9.957 16.0469 8.12939C16.0469 3.71112 12.4652 0.129395 8.04688 0.129395C6.21927 0.129395 4.5348 0.742241 3.18776 1.77364L2.1212 0.707079C1.73067 0.316555 1.09751 0.316555 0.706986 0.707079C0.316461 1.0976 0.316461 1.73077 0.706986 2.12129L1.76322 3.17753C0.688345 4.53962 0.046875 6.25959 0.046875 8.12939C0.046875 12.5477 3.6286 16.1294 8.04688 16.1294C9.91667 16.1294 11.6367 15.4879 12.9987 14.413ZM11.5713 12.9857C10.5818 13.7051 9.36389 14.1294 8.04688 14.1294C4.73317 14.1294 2.04688 11.4431 2.04688 8.12939C2.04688 6.81238 2.47121 5.59447 3.19062 4.60493L5.29234 6.70665C5.10494 7.09827 5 7.53688 5 8C5 9.65685 6.34315 11 8 11C8.46312 11 8.90173 10.8951 9.29335 10.7077L11.5713 12.9857Z" fill="#6A737D"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -30,7 +30,6 @@ import NotificationManager from './lib/notification-manager.js'
import MetamaskController from './metamask-controller' import MetamaskController from './metamask-controller'
import rawFirstTimeState from './first-time-state' import rawFirstTimeState from './first-time-state'
import setupSentry from './lib/setupSentry' import setupSentry from './lib/setupSentry'
import reportFailedTxToSentry from './lib/reportFailedTxToSentry'
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code' import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code'
import getObjStructure from './lib/getObjStructure' import getObjStructure from './lib/getObjStructure'
import setupEnsIpfsResolver from './lib/ens-ipfs/setup' import setupEnsIpfsResolver from './lib/ens-ipfs/setup'
@ -105,10 +104,8 @@ initialize().catch(log.error)
* @property {Object} contractExchangeRates - Info about current token prices. * @property {Object} contractExchangeRates - Info about current token prices.
* @property {Array} tokens - Tokens held by the current user, including their balances. * @property {Array} tokens - Tokens held by the current user, including their balances.
* @property {Object} send - TODO: Document * @property {Object} send - TODO: Document
* @property {Object} coinOptions - TODO: Document
* @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon. * @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon.
* @property {Object} featureFlags - An object for optional feature flags. * @property {Object} featureFlags - An object for optional feature flags.
* @property {string} networkEndpointType - TODO: Document
* @property {boolean} welcomeScreen - True if welcome screen should be shown. * @property {boolean} welcomeScreen - True if welcome screen should be shown.
* @property {string} currentLocale - A locale string matching the user's preferred display language. * @property {string} currentLocale - A locale string matching the user's preferred display language.
* @property {Object} provider - The current selected network provider. * @property {Object} provider - The current selected network provider.
@ -255,19 +252,6 @@ function setupController (initState, initLangCode) {
provider: controller.provider, provider: controller.provider,
}) })
// report failed transactions to Sentry
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') {
return
}
const txMeta = controller.txController.txStateManager.getTx(txId)
try {
reportFailedTxToSentry({ sentry, txMeta })
} catch (e) {
console.error(e)
}
})
// setup state persistence // setup state persistence
pump( pump(
asStream(controller.store), asStream(controller.store),

@ -12,7 +12,6 @@ import ObservableStore from 'obs-store'
*/ */
export const ALERT_TYPES = { export const ALERT_TYPES = {
switchToConnected: 'switchToConnected',
unconnectedAccount: 'unconnectedAccount', unconnectedAccount: 'unconnectedAccount',
} }
@ -25,7 +24,7 @@ const defaultState = {
}, },
{} {}
), ),
switchToConnectedAlertShown: {}, unconnectedAccountAlertShownOrigins: {},
} }
/** /**
@ -44,7 +43,7 @@ export default class AlertController {
defaultState, defaultState,
initState, initState,
{ {
switchToConnectedAlertShown: {}, unconnectedAccountAlertShownOrigins: {},
} }
) )
this.store = new ObservableStore(state) this.store = new ObservableStore(state)
@ -54,9 +53,9 @@ export default class AlertController {
preferencesStore.subscribe(({ selectedAddress }) => { preferencesStore.subscribe(({ selectedAddress }) => {
const currentState = this.store.getState() const currentState = this.store.getState()
if (currentState.switchToConnectedAlertShown && this.selectedAddress !== selectedAddress) { if (currentState.unconnectedAccountAlertShownOrigins && this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress this.selectedAddress = selectedAddress
this.store.updateState({ switchToConnectedAlertShown: {} }) this.store.updateState({ unconnectedAccountAlertShownOrigins: {} })
} }
}) })
} }
@ -72,10 +71,10 @@ export default class AlertController {
* Sets the "switch to connected" alert as shown for the given origin * Sets the "switch to connected" alert as shown for the given origin
* @param {string} origin - The origin the alert has been shown for * @param {string} origin - The origin the alert has been shown for
*/ */
setSwitchToConnectedAlertShown (origin) { setUnconnectedAccountAlertShown (origin) {
let { switchToConnectedAlertShown } = this.store.getState() let { unconnectedAccountAlertShownOrigins } = this.store.getState()
switchToConnectedAlertShown = { ...switchToConnectedAlertShown } unconnectedAccountAlertShownOrigins = { ...unconnectedAccountAlertShownOrigins }
switchToConnectedAlertShown[origin] = true unconnectedAccountAlertShownOrigins[origin] = true
this.store.updateState({ switchToConnectedAlertShown }) this.store.updateState({ unconnectedAccountAlertShownOrigins })
} }
} }

@ -341,6 +341,24 @@ export class PermissionsController {
this.notifyAccountsChanged(origin, newPermittedAccounts) this.notifyAccountsChanged(origin, newPermittedAccounts)
} }
/**
* Remove all permissions associated with a particular account. Any eth_accounts
* permissions left with no permitted accounts will be removed as well.
*
* Throws error if the account is invalid, or if the update fails.
*
* @param {string} account - The account to remove.
*/
async removeAllAccountPermissions (account) {
this.validatePermittedAccounts([account])
const domains = this.permissions.getDomains()
const connectedOrigins = Object.keys(domains)
.filter((origin) => this._getPermittedAccounts(origin).includes(account))
await Promise.all(connectedOrigins.map((origin) => this.removePermittedAccount(origin, account)))
}
/** /**
* Finalizes a permissions request. Throws if request validation fails. * Finalizes a permissions request. Throws if request validation fails.
* Clones the passed-in parameters to prevent inadvertent modification. * Clones the passed-in parameters to prevent inadvertent modification.

@ -27,9 +27,6 @@ export default class TokenRatesController {
* Updates exchange rates for all tokens * Updates exchange rates for all tokens
*/ */
async updateExchangeRates () { async updateExchangeRates () {
if (!this.isActive) {
return
}
const contractExchangeRates = {} const contractExchangeRates = {}
const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth' const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth'
const pairs = this._tokens.map((token) => token.address).join(',') const pairs = this._tokens.map((token) => token.address).join(',')

@ -37,7 +37,7 @@ export default class TxGasUtil {
let estimatedGasHex = bnToHex(saferGasLimitBN) let estimatedGasHex = bnToHex(saferGasLimitBN)
let simulationFails let simulationFails
try { try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) estimatedGasHex = await this.estimateTxGas(txMeta)
} catch (error) { } catch (error) {
log.warn(error) log.warn(error)
simulationFails = { simulationFails = {

@ -1,14 +0,0 @@
import extractEthjsErrorMessage from './extractEthjsErrorMessage'
//
// utility for formatting failed transaction messages
// for sending to sentry
//
export default function reportFailedTxToSentry ({ sentry, txMeta }) {
const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
sentry.captureMessage(errorMessage, {
// "extra" key is required by Sentry
extra: { txMeta },
})
}

@ -8,8 +8,7 @@ const METAMASK_ENVIRONMENT = process.env.METAMASK_ENVIRONMENT
const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
export default function setupSentry (opts) { export default function setupSentry ({ release }) {
const { release, getState } = opts
let sentryTarget let sentryTarget
// detect brave // detect brave
const isBrave = Boolean(window.chrome.ipcRenderer) const isBrave = Boolean(window.chrome.ipcRenderer)
@ -44,11 +43,6 @@ export default function setupSentry (opts) {
simplifyErrorMessages(report) simplifyErrorMessages(report)
// modify report urls // modify report urls
rewriteReportUrls(report) rewriteReportUrls(report)
// append app state
if (getState) {
const appState = getState()
report.extra.appState = appState
}
} catch (err) { } catch (err) {
console.warn(err) console.warn(err)
} }

@ -545,7 +545,7 @@ export default class MetamaskController extends EventEmitter {
// alert controller // alert controller
setAlertEnabledness: nodeify(alertController.setAlertEnabledness, alertController), setAlertEnabledness: nodeify(alertController.setAlertEnabledness, alertController),
setSwitchToConnectedAlertShown: nodeify(this.alertController.setSwitchToConnectedAlertShown, this.alertController), setUnconnectedAccountAlertShown: nodeify(this.alertController.setUnconnectedAccountAlertShown, this.alertController),
// 3Box // 3Box
setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController), setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController),
@ -620,6 +620,10 @@ export default class MetamaskController extends EventEmitter {
// clear known identities // clear known identities
this.preferencesController.setAddresses([]) this.preferencesController.setAddresses([])
// clear permissions
this.permissionsController.clearPermissions()
// create new vault // create new vault
const vault = await keyringController.createNewVaultAndRestore(password, seed) const vault = await keyringController.createNewVaultAndRestore(password, seed)
@ -993,6 +997,8 @@ export default class MetamaskController extends EventEmitter {
* *
*/ */
async removeAccount (address) { async removeAccount (address) {
// Remove all associated permissions
await this.permissionsController.removeAllAccountPermissions(address)
// Remove account from the preferences controller // Remove account from the preferences controller
this.preferencesController.removeAddress(address) this.preferencesController.removeAddress(address)
// Remove account from the account tracker controller // Remove account from the account tracker controller

@ -4,6 +4,7 @@ import './lib/freezeGlobals'
// polyfills // polyfills
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
import '@formatjs/intl-relativetimeformat/polyfill'
import PortStream from 'extension-port-stream' import PortStream from 'extension-port-stream'
import { getEnvironmentType } from './lib/util' import { getEnvironmentType } from './lib/util'
@ -35,18 +36,7 @@ async function start () {
// setup sentry error reporting // setup sentry error reporting
const release = global.platform.getVersion() const release = global.platform.getVersion()
setupSentry({ release, getState }) setupSentry({ release })
// provide app state to append to error logs
function getState () {
// get app state
const state = window.getCleanAppState
? window.getCleanAppState()
: {}
// remove unnecessary data
delete state.localeMessages
// return state to be added to request
return state
}
// identify window type (popup, notification) // identify window type (popup, notification)
const windowType = getEnvironmentType() const windowType = getEnvironmentType()

@ -522,9 +522,6 @@
"priceAndTimeEstimatesLastRetrieved": 1541527901281, "priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {} "errors": {}
}, },
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": { "unconnectedAccount": {
"state": "CLOSED" "state": "CLOSED"
}, },

@ -473,9 +473,6 @@
"priceAndTimeEstimatesLastRetrieved": 1541527901281, "priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {} "errors": {}
}, },
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": { "unconnectedAccount": {
"state": "CLOSED" "state": "CLOSED"
}, },

@ -1295,9 +1295,6 @@
"errors": {} "errors": {}
}, },
"confirmTransaction": {}, "confirmTransaction": {},
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": { "unconnectedAccount": {
"state": "CLOSED" "state": "CLOSED"
} }

@ -71,6 +71,7 @@
"3box": "^1.10.2", "3box": "^1.10.2",
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"@download/blockies": "^1.0.3", "@download/blockies": "^1.0.3",
"@formatjs/intl-relativetimeformat": "^5.2.6",
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"@material-ui/core": "1.0.0", "@material-ui/core": "1.0.0",
"@metamask/controllers": "^2.0.0", "@metamask/controllers": "^2.0.0",
@ -223,6 +224,7 @@
"eslint-plugin-json": "^1.2.0", "eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^6.2.2", "eslint-plugin-mocha": "^6.2.2",
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.18.3",
"eslint-plugin-react-hooks": "^4.0.4",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fast-glob": "^3.2.2", "fast-glob": "^3.2.2",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",

@ -195,7 +195,7 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.wait(async () => { await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1 return confirmedTxes.length === 1

@ -63,7 +63,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('clicks the "Import Wallet" option', async function () { it('clicks the "Import Wallet" option', async function () {
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`))
await driver.delay(largeDelayMs) await driver.delay(largeDelayMs)
}) })
@ -73,7 +73,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('imports a seed phrase', async function () { it('imports a seed phrase', async function () {
const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -82,7 +82,7 @@ describe('Using MetaMask with an existing account', function () {
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) const [confirmPassword] = await driver.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
await driver.clickElement(By.css('.first-time-flow__checkbox')) await driver.clickElement(By.css('.first-time-flow__terms'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -215,7 +215,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.wait(async () => { await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1 return confirmedTxes.length === 1

@ -211,7 +211,7 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.wait(async () => { await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1 return confirmedTxes.length === 1

@ -266,7 +266,7 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.wait(async () => { await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1 return confirmedTxes.length === 1

@ -61,7 +61,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('clicks the "Import Wallet" option', async function () { it('clicks the "Import Wallet" option', async function () {
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`))
await driver.delay(largeDelayMs) await driver.delay(largeDelayMs)
}) })
@ -71,7 +71,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('imports a seed phrase', async function () { it('imports a seed phrase', async function () {
const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -80,7 +80,7 @@ describe('Using MetaMask with an existing account', function () {
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) const [confirmPassword] = await driver.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
await driver.clickElement(By.css('.first-time-flow__checkbox')) await driver.clickElement(By.css('.first-time-flow__terms'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -197,7 +197,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.wait(async () => { await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1 return confirmedTxes.length === 1

@ -22,7 +22,7 @@ describe('MetaMask Browser Extension', function () {
await amountField.sendKeys('1') await amountField.sendKeys('1')
await driver.clickElement(By.css('[data-testid="page-container-footer-next"]')) await driver.clickElement(By.css('[data-testid="page-container-footer-next"]'))
await driver.clickElement(By.css('[data-testid="page-container-footer-next"]')) await driver.clickElement(By.css('[data-testid="page-container-footer-next"]'))
await driver.clickElement(By.css('[data-testid="home__history-tab"]')) await driver.clickElement(By.css('[data-testid="home__activity-tab"]'))
await driver.findElement(By.css('.transaction-list-item')) await driver.findElement(By.css('.transaction-list-item'))
}) })
}) })

@ -64,7 +64,7 @@ describe('MetaMask', function () {
}) })
it('clicks the "Import Wallet" option', async function () { it('clicks the "Import Wallet" option', async function () {
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`))
await driver.delay(largeDelayMs) await driver.delay(largeDelayMs)
}) })
@ -74,7 +74,7 @@ describe('MetaMask', function () {
}) })
it('imports a seed phrase', async function () { it('imports a seed phrase', async function () {
const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -83,7 +83,7 @@ describe('MetaMask', function () {
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) const [confirmPassword] = await driver.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
await driver.clickElement(By.css('.first-time-flow__checkbox')) await driver.clickElement(By.css('.first-time-flow__terms'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -170,7 +170,7 @@ describe('MetaMask', function () {
}) })
it('clicks the "Import Wallet" option', async function () { it('clicks the "Import Wallet" option', async function () {
await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`))
await driver2.delay(largeDelayMs) await driver2.delay(largeDelayMs)
}) })
@ -180,7 +180,7 @@ describe('MetaMask', function () {
}) })
it('imports a seed phrase', async function () { it('imports a seed phrase', async function () {
const [seedTextArea] = await driver2.findElements(By.css('textarea.first-time-flow__textarea')) const [seedTextArea] = await driver2.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await driver2.delay(regularDelayMs) await driver2.delay(regularDelayMs)
@ -189,7 +189,7 @@ describe('MetaMask', function () {
const [confirmPassword] = await driver2.findElements(By.id('confirm-password')) const [confirmPassword] = await driver2.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
await driver2.clickElement(By.css('.first-time-flow__checkbox')) await driver2.clickElement(By.css('.first-time-flow__terms'))
await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import')]`))
await driver2.delay(regularDelayMs) await driver2.delay(regularDelayMs)

@ -60,7 +60,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('clicks the "Import Wallet" option', async function () { it('clicks the "Import Wallet" option', async function () {
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`))
await driver.delay(largeDelayMs) await driver.delay(largeDelayMs)
}) })
@ -70,7 +70,7 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('imports a seed phrase', async function () { it('imports a seed phrase', async function () {
const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -79,7 +79,7 @@ describe('Using MetaMask with an existing account', function () {
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) const [confirmPassword] = await driver.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
await driver.clickElement(By.css('.first-time-flow__checkbox')) await driver.clickElement(By.css('.first-time-flow__terms'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)

@ -599,6 +599,7 @@ describe('MetaMaskController', function () {
sinon.stub(metamaskController.preferencesController, 'removeAddress') sinon.stub(metamaskController.preferencesController, 'removeAddress')
sinon.stub(metamaskController.accountTracker, 'removeAccount') sinon.stub(metamaskController.accountTracker, 'removeAccount')
sinon.stub(metamaskController.keyringController, 'removeAccount') sinon.stub(metamaskController.keyringController, 'removeAccount')
sinon.stub(metamaskController.permissionsController, 'removeAllAccountPermissions')
ret = await metamaskController.removeAccount(addressToRemove) ret = await metamaskController.removeAccount(addressToRemove)
@ -608,6 +609,7 @@ describe('MetaMaskController', function () {
metamaskController.keyringController.removeAccount.restore() metamaskController.keyringController.removeAccount.restore()
metamaskController.accountTracker.removeAccount.restore() metamaskController.accountTracker.removeAccount.restore()
metamaskController.preferencesController.removeAddress.restore() metamaskController.preferencesController.removeAddress.restore()
metamaskController.permissionsController.removeAllAccountPermissions.restore()
}) })
it('should call preferencesController.removeAddress', async function () { it('should call preferencesController.removeAddress', async function () {
@ -619,6 +621,9 @@ describe('MetaMaskController', function () {
it('should call keyringController.removeAccount', async function () { it('should call keyringController.removeAccount', async function () {
assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove)) assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove))
}) })
it('should call permissionsController.removeAllAccountPermissions', async function () {
assert(metamaskController.permissionsController.removeAllAccountPermissions.calledWith(addressToRemove))
})
it('should return address', async function () { it('should return address', async function () {
assert.equal(ret, '0x1') assert.equal(ret, '0x1')
}) })

@ -660,6 +660,112 @@ describe('permissions controller', function () {
}) })
}) })
describe('removeAllAccountPermissions', function () {
let permController, notifications
beforeEach(function () {
notifications = initNotifications()
permController = initPermController(notifications)
grantPermissions(
permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
)
grantPermissions(
permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
)
grantPermissions(
permController, DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
)
})
it('should throw if account is not a string', async function () {
await assert.rejects(
() => permController.removeAllAccountPermissions({}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param'
)
})
it('should throw if given account is not in keyring', async function () {
await assert.rejects(
() => permController.removeAllAccountPermissions(DUMMY_ACCOUNT),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account'
)
})
it('should remove permitted account from single origin', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.a.permitted[1])
const accounts = await permController._getPermittedAccounts(DOMAINS.a.origin)
assert.deepEqual(
accounts, ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]),
'origin should have correct accounts'
)
assert.deepEqual(
notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification'
)
})
it('should permitted account from multiple origins', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0])
const bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual(
bAccounts, [],
'first origin should no accounts'
)
const cAccounts = await permController.getAccounts(DOMAINS.c.origin)
assert.deepEqual(
cAccounts, [],
'second origin no accounts'
)
assert.deepEqual(
notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(),
'first origin should have correct notification'
)
assert.deepEqual(
notifications[DOMAINS.c.origin][0],
NOTIFICATIONS.removedAccounts(),
'second origin should have correct notification'
)
})
it('should remove eth_accounts permission if removing only permitted account', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0])
const accounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual(
accounts, [],
'origin should have no accounts'
)
const permission = await permController.permissions.getPermission(
DOMAINS.b.origin, PERM_NAMES.eth_accounts
)
assert.equal(permission, undefined, 'origin should not have eth_accounts permission')
assert.deepEqual(
notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(),
'origin should have correct notification'
)
})
})
describe('finalizePermissionsRequest', function () { describe('finalizePermissionsRequest', function () {
let permController let permController

@ -58,6 +58,8 @@ const initMiddleware = (permLog) => {
} }
const initClock = () => { const initClock = () => {
// useFakeTimers, is in fact, not a react-hook
// eslint-disable-next-line
clock = useFakeTimers(1) clock = useFakeTimers(1)
} }

@ -6,18 +6,20 @@ import ObservableStore from 'obs-store'
describe('TokenRatesController', function () { describe('TokenRatesController', function () {
it('should listen for preferences store updates', function () { it('should listen for preferences store updates', function () {
const preferences = new ObservableStore({ tokens: [] }) const preferences = new ObservableStore({ tokens: [] })
const controller = new TokenRatesController({ preferences })
preferences.putState({ tokens: ['foo'] }) preferences.putState({ tokens: ['foo'] })
const controller = new TokenRatesController({ preferences })
assert.deepEqual(controller._tokens, ['foo']) assert.deepEqual(controller._tokens, ['foo'])
}) })
it('should poll on correct interval', async function () { it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval') const stub = sinon.stub(global, 'setInterval')
const rateController = new TokenRatesController() // eslint-disable-line no-new const preferences = new ObservableStore({ tokens: [] })
rateController.start(1337) preferences.putState({ tokens: ['foo'] })
const controller = new TokenRatesController({ preferences })
controller.start(1337)
assert.strictEqual(stub.getCall(0).args[1], 1337) assert.strictEqual(stub.getCall(0).args[1], 1337)
stub.restore() stub.restore()
rateController.stop() controller.stop()
}) })
}) })

@ -221,10 +221,10 @@ describe('Actions', function () {
}) })
describe('#removeAccount', function () { describe('#removeAccount', function () {
let removeAccountSpy let removeAccountStub
afterEach(function () { afterEach(function () {
removeAccountSpy.restore() removeAccountStub.restore()
}) })
it('calls removeAccount in background and expect actions to show account', async function () { it('calls removeAccount in background and expect actions to show account', async function () {
@ -238,10 +238,11 @@ describe('Actions', function () {
'SHOW_ACCOUNTS_PAGE', 'SHOW_ACCOUNTS_PAGE',
] ]
removeAccountSpy = sinon.spy(background, 'removeAccount') removeAccountStub = sinon.stub(background, 'removeAccount')
removeAccountStub.callsFake((_, callback) => callback())
await store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')) await store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'))
assert(removeAccountSpy.calledOnce) assert(removeAccountStub.calledOnce)
const actionTypes = store const actionTypes = store
.getActions() .getActions()
.map((action) => action.type) .map((action) => action.type)
@ -257,8 +258,8 @@ describe('Actions', function () {
'HIDE_LOADING_INDICATION', 'HIDE_LOADING_INDICATION',
] ]
removeAccountSpy = sinon.stub(background, 'removeAccount') removeAccountStub = sinon.stub(background, 'removeAccount')
removeAccountSpy.callsFake((_, callback) => { removeAccountStub.callsFake((_, callback) => {
callback(new Error('error')) callback(new Error('error'))
}) })

@ -8,7 +8,7 @@ import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
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 IconWithFallBack from '../../ui/icon-with-fallback' import SiteIcon from '../../ui/site-icon'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../../helpers/constants/common' import { PRIMARY } from '../../../helpers/constants/common'
import { import {
@ -178,7 +178,7 @@ export default class AccountMenu extends Component {
{ iconAndNameForOpenDomain { iconAndNameForOpenDomain
? ( ? (
<div className="account-menu__icon-list"> <div className="account-menu__icon-list">
<IconWithFallBack icon={iconAndNameForOpenDomain.icon} name={iconAndNameForOpenDomain.name} /> <SiteIcon icon={iconAndNameForOpenDomain.icon} name={iconAndNameForOpenDomain.name} size={32} />
</div> </div>
) )
: null : null

@ -1,33 +1,35 @@
import PropTypes from 'prop-types' import React from 'react'
import React, { PureComponent } from 'react' import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useHistory } from 'react-router-dom'
import { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes'
import Button from '../../ui/button'
export default class AddTokenButton extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static defaultProps = { export default function AddTokenButton () {
onClick: () => {}, const addTokenEvent = useMetricEvent({
} eventOpts: {
category: 'Navigation',
action: 'Token Menu',
name: 'Clicked "Add Token"',
},
})
const t = useI18nContext()
const history = useHistory()
static propTypes = { return (
onClick: PropTypes.func, <div className="add-token-button">
} <Button
className="add-token-button__button"
render () { type="secondary"
const { t } = this.context rounded
const { onClick } = this.props onClick={() => {
history.push(ADD_TOKEN_ROUTE)
return ( addTokenEvent()
<div className="add-token-button"> }}
<span className="add-token-button__description">{t('missingYourTokens')}</span> >
<button {t('addToken')}
className="add-token-button__button" </Button>
onClick={onClick} </div>
> )
{t('addToken')}
</button>
</div>
)
}
} }

@ -1,22 +1,6 @@
.add-token-button { .add-token-button {
width: 100%;
height: 90px;
padding: 20px;
text-align: center;
font-size: 1rem;
&__button { &__button {
background-color: transparent; max-width: 200px;
color: $Blue-500; margin: 16px auto;
display: inline-block;
font-size: 1rem;
&:hover {
color: $Blue-400;
}
&:active {
color: $Blue-600;
}
} }
} }

@ -2,22 +2,15 @@ import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import UnconnectedAccountAlert from './unconnected-account-alert' import UnconnectedAccountAlert from './unconnected-account-alert'
import SwitchToConnectedAlert from './switch-to-connected-alert'
import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account' import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account'
import { alertIsOpen as switchToConnectedAlertIsOpen } from '../../../ducks/alerts/switch-to-connected'
const Alerts = () => { const Alerts = () => {
const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen) const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen)
const _switchToConnectedAlertIsOpen = useSelector(switchToConnectedAlertIsOpen)
if (_unconnectedAccountAlertIsOpen) { if (_unconnectedAccountAlertIsOpen) {
return ( return (
<UnconnectedAccountAlert /> <UnconnectedAccountAlert />
) )
} else if (_switchToConnectedAlertIsOpen) {
return (
<SwitchToConnectedAlert />
)
} }
return null return null

@ -1,3 +1 @@
@import './unconnected-account-alert/unconnected-account-alert'; @import './unconnected-account-alert/unconnected-account-alert';
@import './switch-to-connected-alert/switch-to-connected-alert';

@ -1 +0,0 @@
export { default } from './switch-to-connected-alert'

@ -1,121 +0,0 @@
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
ALERT_STATE,
switchToAccount,
dismissAlert,
dismissAndDisableAlert,
getAlertState,
} from '../../../../ducks/alerts/switch-to-connected'
import { getPermittedIdentitiesForCurrentTab } from '../../../../selectors'
import Popover from '../../../ui/popover'
import Button from '../../../ui/button'
import Dropdown from '../../../ui/dropdown'
import Checkbox from '../../../ui/check-box'
import Tooltip from '../../../ui/tooltip-v2'
import { useI18nContext } from '../../../../hooks/useI18nContext'
const {
ERROR,
LOADING,
} = ALERT_STATE
const SwitchToUnconnectedAccountAlert = () => {
const t = useI18nContext()
const dispatch = useDispatch()
const alertState = useSelector(getAlertState)
const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab)
const [accountToSwitchTo, setAccountToSwitchTo] = useState(connectedAccounts[0].address)
const [dontShowThisAgain, setDontShowThisAgain] = useState(false)
const onClose = async () => {
return dontShowThisAgain
? await dispatch(dismissAndDisableAlert())
: dispatch(dismissAlert())
}
const options = connectedAccounts.map((account) => {
return { name: account.name, value: account.address }
})
return (
<Popover
contentClassName="switch-to-connected-alert__content"
footer={(
<>
{
alertState === ERROR
? (
<div className="switch-to-connected-alert__error">
{ t('failureMessage') }
</div>
)
: null
}
<div className="switch-to-connected-alert__footer-buttons">
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
>
{ t('dismiss') }
</Button>
<Button
disabled={alertState === LOADING || alertState === ERROR || dontShowThisAgain}
onClick={() => dispatch(switchToAccount(accountToSwitchTo))}
type="primary"
>
{ t('switchAccounts') }
</Button>
</div>
</>
)}
footerClassName="switch-to-connected-alert__footer"
onClose={onClose}
subtitle={
connectedAccounts.length > 1
? t('switchToConnectedAlertMultipleAccountsDescription')
: t('switchToConnectedAlertSingleAccountDescription', [connectedAccounts[0].name])
}
title={t('notConnected')}
>
{
connectedAccounts.length > 1
? (
<Dropdown
className="switch-to-connected-alert__dropdown"
title="Switch to account"
onChange={(address) => setAccountToSwitchTo(address)}
options={options}
selectedOption={accountToSwitchTo}
/>
)
: null
}
<div className="switch-to-connected-alert__checkbox-wrapper">
<Checkbox
id="switchToConnected_dontShowThisAgain"
checked={dontShowThisAgain}
className="switch-to-connected-alert__checkbox"
onClick={() => setDontShowThisAgain((checked) => !checked)}
/>
<label
className="switch-to-connected-alert__checkbox-label"
htmlFor="switchToConnected_dontShowThisAgain"
>
{ t('dontShowThisAgain') }
<Tooltip
position="top"
title={t('unconnectedAccountAlertDisableTooltip')}
wrapperClassName="switch-to-connected-alert__checkbox-label-tooltip"
>
<i className="fa fa-info-circle" />
</Tooltip>
</label>
</div>
</Popover>
)
}
export default SwitchToUnconnectedAccountAlert

@ -1,66 +0,0 @@
.switch-to-connected-alert {
&__footer {
flex-direction: column;
:only-child {
margin: 0;
}
}
&__footer-buttons {
display: flex;
flex-direction: row;
button:first-child {
margin-right: 24px;
}
button {
font-size: 14px;
line-height: 20px;
padding: 8px;
}
}
&__error {
margin-bottom: 16px;
padding: 16px;
font-size: 14px;
border: 1px solid #D73A49;
background: #F8EAE8;
border-radius: 3px;
}
&__content {
align-items: center;
padding: 0 24px 24px 24px;
}
&__dropdown {
background-color: white;
width: 100%;
margin-bottom: 24px;
}
&__checkbox-wrapper {
display: flex;
flex-direction: row;
width: 100%;
}
&__checkbox {
margin-right: 8px;
}
&__checkbox-label {
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
color: $Grey-500;
display: flex;
}
&__checkbox-label-tooltip {
margin-left: 8px;
}
}

@ -7,12 +7,20 @@ import {
dismissAlert, dismissAlert,
dismissAndDisableAlert, dismissAndDisableAlert,
getAlertState, getAlertState,
switchToAccount,
} from '../../../../ducks/alerts/unconnected-account' } from '../../../../ducks/alerts/unconnected-account'
import {
getOriginOfCurrentTab,
getPermittedIdentitiesForCurrentTab,
getSelectedAddress,
getSelectedIdentity,
} from '../../../../selectors'
import { isExtensionUrl } from '../../../../helpers/utils/util'
import Popover from '../../../ui/popover' import Popover from '../../../ui/popover'
import Button from '../../../ui/button' import Button from '../../../ui/button'
import Checkbox from '../../../ui/check-box' import Checkbox from '../../../ui/check-box'
import Tooltip from '../../../ui/tooltip-v2' import Tooltip from '../../../ui/tooltip-v2'
import { getSelectedIdentity, getOriginOfCurrentTab } from '../../../../selectors' import ConnectedAccountsList from '../../connected-accounts-list'
import { useI18nContext } from '../../../../hooks/useI18nContext' import { useI18nContext } from '../../../../hooks/useI18nContext'
const { const {
@ -20,12 +28,14 @@ const {
LOADING, LOADING,
} = ALERT_STATE } = ALERT_STATE
const SwitchToUnconnectedAccountAlert = () => { const UnconnectedAccountAlert = () => {
const t = useI18nContext() const t = useI18nContext()
const dispatch = useDispatch() const dispatch = useDispatch()
const alertState = useSelector(getAlertState) const alertState = useSelector(getAlertState)
const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab)
const origin = useSelector(getOriginOfCurrentTab) const origin = useSelector(getOriginOfCurrentTab)
const selectedIdentity = useSelector(getSelectedIdentity) const selectedIdentity = useSelector(getSelectedIdentity)
const selectedAddress = useSelector(getSelectedAddress)
const [dontShowThisAgain, setDontShowThisAgain] = useState(false) const [dontShowThisAgain, setDontShowThisAgain] = useState(false)
const onClose = async () => { const onClose = async () => {
@ -34,67 +44,70 @@ const SwitchToUnconnectedAccountAlert = () => {
: dispatch(dismissAlert()) : dispatch(dismissAlert())
} }
const accountName = selectedIdentity?.name || t('thisAccount') const footer = (
const siteName = origin || t('thisSite') <>
{
alertState === ERROR
? (
<div className="unconnected-account-alert__error">
{ t('failureMessage') }
</div>
)
: null
}
<div className="unconnected-account-alert__footer-row">
<div className="unconnected-account-alert__checkbox-wrapper">
<Checkbox
id="unconnectedAccount_dontShowThisAgain"
checked={dontShowThisAgain}
className="unconnected-account-alert__checkbox"
onClick={() => setDontShowThisAgain((checked) => !checked)}
/>
<label
className="unconnected-account-alert__checkbox-label"
htmlFor="unconnectedAccount_dontShowThisAgain"
>
{ t('dontShowThisAgain') }
<Tooltip
position="top"
title={t('alertDisableTooltip')}
wrapperClassName="unconnected-account-alert__checkbox-label-tooltip"
>
<i className="fa fa-info-circle" />
</Tooltip>
</label>
</div>
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
className="unconnected-account-alert__dismiss-button"
>
{ t('dismiss') }
</Button>
</div>
</>
)
return ( return (
<Popover <Popover
contentClassName="unconnected-account-alert__content" title={isExtensionUrl(origin) ? t('currentExtension') : new URL(origin).host}
title={t('notConnected')} subtitle={t('currentAccountNotConnected')}
subtitle={t('unconnectedAccountAlertDescription', [ accountName, siteName ])}
onClose={onClose} onClose={onClose}
footer={( contentClassName="unconnected-account-alert__content"
<>
{
alertState === ERROR
? (
<div className="unconnected-account-alert__error">
{ t('failureMessage') }
</div>
)
: null
}
<div className="unconnected-account-alert__footer-buttons">
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
>
{ t('dismiss') }
</Button>
<Button
disabled={alertState === LOADING || alertState === ERROR || dontShowThisAgain }
onClick={() => dispatch(connectAccount())}
type="primary"
>
{ t('connect') }
</Button>
</div>
</>
)}
footerClassName="unconnected-account-alert__footer" footerClassName="unconnected-account-alert__footer"
footer={footer}
> >
<Checkbox <ConnectedAccountsList
id="unconnectedAccount_dontShowThisAgain" accountToConnect={selectedIdentity}
checked={dontShowThisAgain} connectAccount={() => dispatch(connectAccount(selectedAddress))}
className="unconnected-account-alert__checkbox" connectedAccounts={connectedAccounts}
onClick={() => setDontShowThisAgain((checked) => !checked)} selectedAddress={selectedAddress}
setSelectedAddress={(address) => dispatch(switchToAccount(address))}
shouldRenderListOptions={false}
/> />
<label
className="unconnected-account-alert__checkbox-label"
htmlFor="unconnectedAccount_dontShowThisAgain"
>
{ t('dontShowThisAgain') }
<Tooltip
position="top"
title={t('unconnectedAccountAlertDisableTooltip')}
wrapperClassName="unconnected-account-alert__checkbox-label-tooltip"
>
<i className="fa fa-info-circle" />
</Tooltip>
</label>
</Popover> </Popover>
) )
} }
export default SwitchToUnconnectedAccountAlert export default UnconnectedAccountAlert

@ -1,25 +1,28 @@
.unconnected-account-alert { .unconnected-account-alert {
&__content {
border-radius: 0;
}
&__footer { &__footer {
flex-direction: column; flex-direction: column;
:only-child { > :only-child {
margin: 0; margin: 0;
} }
} }
&__footer-buttons { &__footer-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
}
button:first-child { &__dismiss-button {
margin-right: 24px; background: #037DD6;
} color: white;
height: 40px;
button { width: 100px;
font-size: 14px; border: 0;
line-height: 20px; border-radius: 100px;
padding: 8px;
}
} }
&__error { &__error {
@ -31,21 +34,24 @@
border-radius: 3px; border-radius: 3px;
} }
&__content { &__checkbox-wrapper {
width: 100%;
display: flex;
flex-direction: row; flex-direction: row;
padding: 0 24px 24px 24px; align-items: center;
} }
&__checkbox { &__checkbox {
margin-right: 8px; margin-right: 8px;
padding-top: 1px; // better alignment with rest of content
} }
&__checkbox-label { &__checkbox-label {
font-size: 14px; display: flex;
font-size: 12px;
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
color: $Grey-500; color: $Grey-500;
display: flex;
} }
&__checkbox-label-tooltip { &__checkbox-label-tooltip {

@ -12,13 +12,15 @@ export default class ConnectedAccountsListItem extends PureComponent {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
className: PropTypes.string, className: PropTypes.string,
name: PropTypes.node.isRequired, name: PropTypes.node.isRequired,
status: PropTypes.node.isRequired, status: PropTypes.string,
action: PropTypes.node,
options: PropTypes.node, options: PropTypes.node,
} }
static defaultProps = { static defaultProps = {
className: null, className: null,
options: null, options: null,
action: null,
} }
render () { render () {
@ -27,6 +29,7 @@ export default class ConnectedAccountsListItem extends PureComponent {
className, className,
name, name,
status, status,
action,
options, options,
} = this.props } = this.props
@ -39,18 +42,20 @@ export default class ConnectedAccountsListItem extends PureComponent {
diameter={32} diameter={32}
/> />
<div> <div>
<p> <p className="connected-accounts-list__account-name">
<strong className="connected-accounts-list__account-name">{name}</strong> <strong>{name}</strong>
</p> </p>
{ {
status status
? ( ? (
<p className="connected-accounts-list__account-status"> <p className="connected-accounts-list__account-status">
&nbsp;&nbsp;
{status} {status}
</p> </p>
) )
: null : null
} }
{action}
</div> </div>
</div> </div>
{options} {options}

@ -1 +0,0 @@
export { default } from './connected-accounts-list-permissions.component'

@ -1,7 +1,5 @@
import { DateTime } from 'luxon'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import ConnectedAccountsListPermissions from './connected-accounts-list-permissions'
import ConnectedAccountsListItem from './connected-accounts-list-item' import ConnectedAccountsListItem from './connected-accounts-list-item'
import ConnectedAccountsListOptions from './connected-accounts-list-options' import ConnectedAccountsListOptions from './connected-accounts-list-options'
import { MenuItem } from '../../ui/menu' import { MenuItem } from '../../ui/menu'
@ -13,7 +11,6 @@ export default class ConnectedAccountsList extends PureComponent {
static defaultProps = { static defaultProps = {
accountToConnect: null, accountToConnect: null,
permissions: undefined,
} }
static propTypes = { static propTypes = {
@ -26,31 +23,35 @@ export default class ConnectedAccountsList extends PureComponent {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
lastActive: PropTypes.number, lastActive: PropTypes.number,
})).isRequired, })).isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({ connectAccount: PropTypes.func.isRequired,
key: PropTypes.string.isRequired,
})),
selectedAddress: PropTypes.string.isRequired, selectedAddress: PropTypes.string.isRequired,
addPermittedAccount: PropTypes.func.isRequired, removePermittedAccount: PropTypes.func,
removePermittedAccount: PropTypes.func.isRequired,
setSelectedAddress: PropTypes.func.isRequired, setSelectedAddress: PropTypes.func.isRequired,
shouldRenderListOptions: (props, propName, componentName) => {
if (typeof props[propName] !== 'boolean') {
return new Error(
`Warning: Failed prop type: '${propName}' of component '${componentName}' must be a boolean. Received: ${typeof props[propName]}`
)
} else if (props[propName] && !props['removePermittedAccount']) {
return new Error(
`Warning: Failed prop type: '${propName}' of component '${componentName}' requires prop 'removePermittedAccount'.`
)
}
},
} }
state = { state = {
accountWithOptionsShown: null, accountWithOptionsShown: null,
} }
connectAccount = () => {
this.props.addPermittedAccount(this.props.accountToConnect?.address)
}
disconnectAccount = () => { disconnectAccount = () => {
this.hideAccountOptions() this.hideAccountOptions()
this.props.removePermittedAccount(this.state.accountWithOptionsShown) this.props.removePermittedAccount(this.state.accountWithOptionsShown)
} }
switchAccount = () => { switchAccount = (address) => {
this.hideAccountOptions() this.hideAccountOptions()
this.props.setSelectedAddress(this.state.accountWithOptionsShown) this.props.setSelectedAddress(address)
} }
hideAccountOptions = () => { hideAccountOptions = () => {
@ -62,7 +63,7 @@ export default class ConnectedAccountsList extends PureComponent {
} }
renderUnconnectedAccount () { renderUnconnectedAccount () {
const { accountToConnect } = this.props const { accountToConnect, connectAccount } = this.props
const { t } = this.context const { t } = this.context
if (!accountToConnect) { if (!accountToConnect) {
@ -75,73 +76,87 @@ export default class ConnectedAccountsList extends PureComponent {
className="connected-accounts-list__row--highlight" className="connected-accounts-list__row--highlight"
address={address} address={address}
name={`${name} (…${address.substr(-4, 4)})`} name={`${name} (…${address.substr(-4, 4)})`}
status={( status={t('statusNotConnected')}
<> action={(
{t('statusNotConnected')} <a
&nbsp;&middot;&nbsp; className="connected-accounts-list__account-status-link"
<a className="connected-accounts-list__account-status-link" onClick={this.connectAccount}> onClick={() => connectAccount(accountToConnect.address)}
{t('connect')} >
</a> {t('connect')}
</> </a>
)} )}
/> />
) )
} }
render () { renderListItemOptions (address) {
const { connectedAccounts, permissions, selectedAddress } = this.props
const { accountWithOptionsShown } = this.state const { accountWithOptionsShown } = this.state
const { t } = this.context const { t } = this.context
return (
<ConnectedAccountsListOptions
onHideOptions={this.hideAccountOptions}
onShowOptions={this.showAccountOptions.bind(null, address)}
show={accountWithOptionsShown === address}
>
<MenuItem
iconClassName="disconnect-icon"
onClick={this.disconnectAccount}
>
{t('disconnectThisAccount')}
</MenuItem>
</ConnectedAccountsListOptions>
)
}
renderListItemAction (address) {
const { t } = this.context
return (
<a
className="connected-accounts-list__account-status-link"
onClick={() => this.switchAccount(address)}
>
{t('switchToThisAccount')}
</a>
)
}
render () {
const {
connectedAccounts,
selectedAddress,
shouldRenderListOptions,
} = this.props
const { t } = this.context
return ( return (
<> <>
<main className="connected-accounts-list"> <main className="connected-accounts-list">
{this.renderUnconnectedAccount()} {this.renderUnconnectedAccount()}
{ {
connectedAccounts.map(({ address, name, lastActive }, index) => { connectedAccounts.map(({ address, name }, index) => {
let status
if (index === 0) {
status = t('primary')
} else if (lastActive) {
status = `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`
}
return ( return (
<ConnectedAccountsListItem <ConnectedAccountsListItem
key={address} key={address}
address={address} address={address}
name={`${name} (…${address.substr(-4, 4)})`} name={`${name} (…${address.substr(-4, 4)})`}
status={status} status={index === 0 ? t('active') : null}
options={( options={
<ConnectedAccountsListOptions shouldRenderListOptions
onHideOptions={this.hideAccountOptions} ? this.renderListItemOptions(address)
onShowOptions={this.showAccountOptions.bind(null, address)} : null
show={accountWithOptionsShown === address} }
> action={
{ address !== selectedAddress
address === selectedAddress ? null : ( ? this.renderListItemAction(address)
<MenuItem : null
iconClassName="fas fa-random" }
onClick={this.switchAccount}
>
{t('switchToThisAccount')}
</MenuItem>
)
}
<MenuItem
iconClassName="disconnect-icon"
onClick={this.disconnectAccount}
>
{t('disconnectThisAccount')}
</MenuItem>
</ConnectedAccountsListOptions>
)}
/> />
) )
}) })
} }
</main> </main>
<ConnectedAccountsListPermissions permissions={permissions} />
</> </>
) )
} }

@ -8,28 +8,34 @@
} }
&__account-name { &__account-name {
display: inline;
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
} }
%account-status-typography {
font-size: 12px;
line-height: 17px;
padding-top: 4px;
}
&__account-status { &__account-status {
@extend %account-status-typography;
display: inline;
color: $Grey-500; color: $Grey-500;
} }
&__account-status-link { &__account-status-link {
@extend %account-status-typography;
display: block;
&, &:hover { &, &:hover {
color: $curious-blue; color: $curious-blue;
cursor: pointer; cursor: pointer;
} }
} }
&__account-status {
font-size: 12px;
line-height: 17px;
padding-top: 4px;
}
&__row { &__row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -40,10 +46,6 @@
border-top: 1px solid $geyser; border-top: 1px solid $geyser;
&:last-of-type {
border-bottom: 1px solid $geyser;
}
&--highlight { &--highlight {
background-color: $warning-light-yellow; background-color: $warning-light-yellow;
border: 1px solid $warning-yellow; border: 1px solid $warning-yellow;
@ -70,72 +72,6 @@
} }
} }
.connected-accounts-permissions {
display: flex;
flex-direction: column;
padding: 24px;
font-size: 12px;
line-height: 17px;
color: $Grey-500;
strong {
font-weight: bold;
}
p + p {
padding-top: 8px;
}
&__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 14px;
line-height: 20px;
color: #24292E;
button {
font-size: 16px;
line-height: 24px;
background: none;
padding: 0;
margin-left: 8px;
}
}
&__list {
padding-top: 8px;
}
&__list-item {
display: flex;
i {
display: block;
padding-right: 8px;
font-size: 18px;
color: $Grey-800;
}
}
&__list-container {
max-height: 0px;
overflow: hidden;
height: auto;
transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1);
&--expanded {
// arbitrarily set hard coded value for effect to work
max-height: 100px;
}
}
}
.tippy-tooltip.none-theme { .tippy-tooltip.none-theme {
background: none; background: none;
padding: 0; padding: 0;

@ -2,7 +2,7 @@ import classnames from 'classnames'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
export default class ConnectedAccountsListPermissions extends PureComponent { export default class ConnectedAccountsPermissions extends PureComponent {
static contextTypes = { static contextTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
} }

@ -0,0 +1 @@
export { default } from './connected-accounts-permissions.component'

@ -0,0 +1,64 @@
.connected-accounts-permissions {
display: flex;
flex-direction: column;
font-size: 12px;
line-height: 17px;
color: $Grey-500;
strong {
font-weight: bold;
}
p + p {
padding-top: 8px;
}
&__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 14px;
line-height: 20px;
color: #24292E;
button {
font-size: 16px;
line-height: 24px;
background: none;
padding: 0;
margin-left: 8px;
}
}
&__list {
padding-top: 8px;
}
&__list-item {
display: flex;
i {
display: block;
padding-right: 8px;
font-size: 18px;
color: $Grey-800;
}
}
&__list-container {
max-height: 0px;
overflow: hidden;
height: auto;
transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1);
&--expanded {
// arbitrarily set hard coded value for effect to work
max-height: 100px;
}
}
}

@ -1,6 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import IconWithFallBack from '../../ui/icon-with-fallback' import SiteIcon from '../../ui/site-icon'
import { stripHttpSchemes } from '../../../helpers/utils/util' import { stripHttpSchemes } from '../../../helpers/utils/util'
export default class ConnectedSitesList extends Component { export default class ConnectedSitesList extends Component {
@ -28,7 +28,7 @@ export default class ConnectedSitesList extends Component {
{ connectedDomains.map((domain) => ( { connectedDomains.map((domain) => (
<div key={domain.origin} className="connected-sites-list__content-row"> <div key={domain.origin} className="connected-sites-list__content-row">
<div className="connected-sites-list__domain-info"> <div className="connected-sites-list__domain-info">
<IconWithFallBack icon={domain.icon} name={domain.name} /> <SiteIcon icon={domain.icon} name={domain.name} size={32} />
<span className="connected-sites-list__domain-name" title={domain.extensionId || domain.origin}> <span className="connected-sites-list__domain-name" title={domain.extensionId || domain.origin}>
{this.getDomainDisplayName(domain)} {this.getDomainDisplayName(domain)}
</span> </span>

@ -88,10 +88,14 @@
@import 'connected-accounts-list/index'; @import 'connected-accounts-list/index';
@import '../ui/icon-with-fallback/index'; @import 'connected-accounts-permissions/index';
@import '../ui/icon/index'; @import '../ui/icon/index';
@import '../ui/icon-with-fallback/icon-with-fallback';
@import '../ui/icon-with-label/index';
@import '../ui/circle-icon/index'; @import '../ui/circle-icon/index';
@import '../ui/alert-circle-icon/index'; @import '../ui/alert-circle-icon/index';
@ -109,3 +113,5 @@
@import 'wallet-overview/index'; @import 'wallet-overview/index';
@import '../ui/account-mismatch-warning/index'; @import '../ui/account-mismatch-warning/index';
@import '../ui/icon-border/icon-border';

@ -21,7 +21,7 @@
.account-options-menu { .account-options-menu {
&__connected-sites:before { &__connected-sites:before {
content: ""; content: "";
background-image: url(/images/icons/connected-sites-black.svg); background-image: url(/images/icons/connected-sites.svg);
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;

@ -46,15 +46,6 @@
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
&--redirect {
margin-top: 140px;
width: 100%;
display: flex;
align-items: center;
padding-top: 8px;
height: 144px;
}
a, a:hover { a, a:hover {
color: $dodger-blue; color: $dodger-blue;
} }
@ -94,10 +85,6 @@
@extend %content-text; @extend %content-text;
line-height: 20px; line-height: 20px;
color: #6A737D; color: #6A737D;
&--redirect {
text-align: center;
}
} }
&__permissions-container { &__permissions-container {
@ -147,105 +134,3 @@
font-weight: bold; font-weight: bold;
} }
} }
.permission-result {
@extend %header--24;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
color: $Black-100;
&__icons {
display: flex;
}
&__center-icon {
display: flex;
position: relative;
justify-content: center;
align-items: center;
font-size: 12px;
}
h1 {
font-size: 14px;
line-height: 18px;
padding: 8px 0 0;
}
h2 {
font-size: 12px;
line-height: 17px;
color: #6A737D;
padding: 0;
}
&__check {
width: 40px;
height: 40px;
background: white url("/images/permissions-check.svg") no-repeat;
position: absolute;
}
&__reject {
position: absolute;
background: white;
display: flex;
justify-content: center;
align-items: center;
i {
color: #D73A49;
transform: scale(3);
}
}
&__identicon, .icon-with-fallback__identicon {
width: 32px;
height: 32px;
&--default {
background-color: #777A87;
color: white;
width: 64px;
height: 64px;
border-radius: 32px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
}
&__identicon-container, .icon-with-fallback__identicon-container {
height: auto;
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 64px;
width: 64px;
}
&__identicon-border, .icon-with-fallback__identicon-border {
height: 64px;
width: 64px;
border-radius: 50%;
border: 1px solid white;
background: #FFFFFF;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25);
}
&__identicon-border {
display: flex;
justify-content: center;
align-items: center;
}
.icon-with-fallback__identicon-border {
position: absolute;
}
}

@ -1,9 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import IconWithFallBack from '../../../ui/icon-with-fallback'
import PermissionsConnectHeader from '../../permissions-connect-header' import PermissionsConnectHeader from '../../permissions-connect-header'
import Tooltip from '../../../ui/tooltip-v2' import Tooltip from '../../../ui/tooltip-v2'
import classnames from 'classnames'
export default class PermissionPageContainerContent extends PureComponent { export default class PermissionPageContainerContent extends PureComponent {
@ -13,13 +11,9 @@ export default class PermissionPageContainerContent extends PureComponent {
onPermissionToggle: PropTypes.func.isRequired, onPermissionToggle: PropTypes.func.isRequired,
selectedIdentities: PropTypes.array, selectedIdentities: PropTypes.array,
allIdentitiesSelected: PropTypes.bool, allIdentitiesSelected: PropTypes.bool,
redirect: PropTypes.bool,
permissionRejected: PropTypes.bool,
} }
static defaultProps = { static defaultProps = {
redirect: null,
permissionRejected: null,
selectedIdentities: [], selectedIdentities: [],
allIdentitiesSelected: false, allIdentitiesSelected: false,
} }
@ -28,39 +22,6 @@ export default class PermissionPageContainerContent extends PureComponent {
t: PropTypes.func, t: PropTypes.func,
} }
renderBrokenLine () {
return (
<svg width="131" height="2" viewBox="0 0 131 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H134" stroke="#CDD1E4" strokeLinejoin="round" strokeDasharray="8 7" />
</svg>
)
}
renderRedirect () {
const { t } = this.context
const { permissionRejected, domainMetadata } = this.props
return (
<div className="permission-result">
{ permissionRejected ? t('cancelling') : t('connecting') }
<div className="permission-result__icons">
<IconWithFallBack icon={domainMetadata.icon} name={domainMetadata.name} />
<div className="permission-result__center-icon">
{ permissionRejected
? <span className="permission-result__reject" ><i className="fa fa-times-circle" /></span>
: <span className="permission-result__check" />
}
{ this.renderBrokenLine() }
</div>
<div className="permission-result__identicon-container">
<div className="permission-result__identicon-border">
<img src="/images/logo/metamask-fox.svg" />
</div>
</div>
</div>
</div>
)
}
renderRequestedPermissions () { renderRequestedPermissions () {
const { const {
selectedPermissions, onPermissionToggle, selectedPermissions, onPermissionToggle,
@ -134,14 +95,10 @@ export default class PermissionPageContainerContent extends PureComponent {
} }
getTitle () { getTitle () {
const { domainMetadata, redirect, permissionRejected, selectedIdentities, allIdentitiesSelected } = this.props const { domainMetadata, selectedIdentities, allIdentitiesSelected } = this.props
const { t } = this.context const { t } = this.context
if (redirect && permissionRejected) { if (domainMetadata.extensionId) {
return t('cancelledConnectionWithMetaMask')
} else if (redirect) {
return t('connectingWithMetaMask')
} else if (domainMetadata.extensionId) {
return t('externalExtension', [domainMetadata.extensionId]) return t('externalExtension', [domainMetadata.extensionId])
} else if (allIdentitiesSelected) { } else if (allIdentitiesSelected) {
return t( return t(
@ -166,36 +123,27 @@ export default class PermissionPageContainerContent extends PureComponent {
} }
render () { render () {
const { domainMetadata, redirect } = this.props const { domainMetadata } = this.props
const { t } = this.context const { t } = this.context
const title = this.getTitle() const title = this.getTitle()
return ( return (
<div <div className="permission-approval-container__content">
className={classnames('permission-approval-container__content', { <div className="permission-approval-container__content-container">
'permission-approval-container__content--redirect': redirect, <PermissionsConnectHeader
})} icon={domainMetadata.icon}
> iconName={domainMetadata.name}
{ !redirect headerTitle={title}
? ( headerText={ domainMetadata.extensionId
<div className="permission-approval-container__content-container"> ? t('allowExternalExtensionTo', [domainMetadata.extensionId])
<PermissionsConnectHeader : t('allowThisSiteTo')
icon={domainMetadata.icon} }
iconName={domainMetadata.origin} />
headerTitle={title} <section className="permission-approval-container__permissions-container">
headerText={ domainMetadata.extensionId { this.renderRequestedPermissions() }
? t('allowExternalExtensionTo', [domainMetadata.extensionId]) </section>
: t('allowThisSiteTo') </div>
}
/>
<section className="permission-approval-container__permissions-container">
{ this.renderRequestedPermissions() }
</section>
</div>
)
: this.renderRedirect()
}
</div> </div>
) )
} }

@ -13,15 +13,11 @@ export default class PermissionPageContainer extends Component {
selectedIdentities: PropTypes.array, selectedIdentities: PropTypes.array,
allIdentitiesSelected: PropTypes.bool, allIdentitiesSelected: PropTypes.bool,
request: PropTypes.object, request: PropTypes.object,
redirect: PropTypes.bool,
permissionRejected: PropTypes.bool,
requestMetadata: PropTypes.object, requestMetadata: PropTypes.object,
targetDomainMetadata: PropTypes.object.isRequired, targetDomainMetadata: PropTypes.object.isRequired,
} }
static defaultProps = { static defaultProps = {
redirect: null,
permissionRejected: null,
request: {}, request: {},
requestMetadata: {}, requestMetadata: {},
selectedIdentities: [], selectedIdentities: [],
@ -116,8 +112,6 @@ export default class PermissionPageContainer extends Component {
requestMetadata, requestMetadata,
targetDomainMetadata, targetDomainMetadata,
selectedIdentities, selectedIdentities,
redirect,
permissionRejected,
allIdentitiesSelected, allIdentitiesSelected,
} = this.props } = this.props
@ -129,27 +123,20 @@ export default class PermissionPageContainer extends Component {
selectedPermissions={this.state.selectedPermissions} selectedPermissions={this.state.selectedPermissions}
onPermissionToggle={this.onPermissionToggle} onPermissionToggle={this.onPermissionToggle}
selectedIdentities={selectedIdentities} selectedIdentities={selectedIdentities}
redirect={redirect}
permissionRejected={permissionRejected}
allIdentitiesSelected={allIdentitiesSelected} allIdentitiesSelected={allIdentitiesSelected}
/> />
{ !redirect <div className="permission-approval-container__footers">
? ( <PermissionsConnectFooter />
<div className="permission-approval-container__footers"> <PageContainerFooter
<PermissionsConnectFooter /> cancelButtonType="default"
<PageContainerFooter onCancel={() => this.onCancel()}
cancelButtonType="default" cancelText={this.context.t('cancel')}
onCancel={() => this.onCancel()} onSubmit={() => this.onSubmit()}
cancelText={this.context.t('cancel')} submitText={this.context.t('connect')}
onSubmit={() => this.onSubmit()} submitButtonType="confirm"
submitText={this.context.t('connect')} buttonSizeLarge={false}
submitButtonType="confirm" />
buttonSizeLarge={false} </div>
/>
</div>
)
: null
}
</div> </div>
) )
} }

@ -1,17 +1,18 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { Component } from 'react' import React, { Component } from 'react'
import IconWithFallBack from '../../ui/icon-with-fallback' import SiteIcon from '../../ui/site-icon'
export default class PermissionsConnectHeader extends Component { export default class PermissionsConnectHeader extends Component {
static propTypes = { static propTypes = {
icon: PropTypes.string, icon: PropTypes.string,
iconName: PropTypes.string.isRequired, iconName: PropTypes.string,
headerTitle: PropTypes.node, headerTitle: PropTypes.node,
headerText: PropTypes.string, headerText: PropTypes.string,
} }
static defaultProps = { static defaultProps = {
icon: null, icon: null,
iconName: '',
headerTitle: '', headerTitle: '',
headerText: '', headerText: '',
} }
@ -21,7 +22,7 @@ export default class PermissionsConnectHeader extends Component {
return ( return (
<div className="permissions-connect-header__icon"> <div className="permissions-connect-header__icon">
<IconWithFallBack icon={ icon } name={ iconName } /> <SiteIcon icon={ icon } name={ iconName } size={64} />
<div className="permissions-connect-header__text">{iconName}</div> <div className="permissions-connect-header__text">{iconName}</div>
</div> </div>
) )

@ -1,47 +1,18 @@
import classnames from 'classnames' import classnames from 'classnames'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util'
import AssetListItem from '../asset-list-item' import AssetListItem from '../asset-list-item'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getSelectedAddress } from '../../../selectors' import { getSelectedAddress } from '../../../selectors'
import { useI18nContext } from '../../../hooks/useI18nContext' import { useI18nContext } from '../../../hooks/useI18nContext'
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util' import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'
export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) { export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) {
const contractExchangeRates = useSelector(getTokenExchangeRates)
const conversionRate = useSelector(getConversionRate)
const currentCurrency = useSelector(getCurrentCurrency)
const userAddress = useSelector(getSelectedAddress) const userAddress = useSelector(getSelectedAddress)
const t = useI18nContext() const t = useI18nContext()
let currentTokenToFiatRate const formattedFiat = useTokenFiatAmount(address, string, symbol)
let currentTokenInFiat
let formattedFiat = ''
// if the conversionRate is 0 eg: currently unknown
// or the contract exchange rate is currently unknown
// the effective currentTokenToFiatRate is 0 and erroneous.
// Skipping this entire block will result in fiat not being
// shown to the user, instead of a fiat value of 0 for a non-zero
// token amount.
if (conversionRate > 0 && contractExchangeRates[address]) {
currentTokenToFiatRate = multiplyCurrencies(
contractExchangeRates[address],
conversionRate
)
currentTokenInFiat = conversionUtil(string, {
fromNumericBase: 'dec',
fromCurrency: symbol,
toCurrency: currentCurrency.toUpperCase(),
numberOfDecimals: 2,
conversionRate: currentTokenToFiatRate,
})
formattedFiat = `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}`
}
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
const warning = outdatedBalance const warning = outdatedBalance
? ( ? (
@ -68,7 +39,7 @@ export default function TokenCell ({ address, outdatedBalance, symbol, string, i
tokenImage={image} tokenImage={image}
warning={warning} warning={warning}
primary={`${string || 0} ${symbol}`} primary={`${string || 0} ${symbol}`}
secondary={showFiat ? formattedFiat : undefined} secondary={formattedFiat}
/> />
) )

@ -20,6 +20,12 @@ describe('Token Cell', function () {
'0xAnotherToken': 0.015, '0xAnotherToken': 0.015,
}, },
conversionRate: 7.00, conversionRate: 7.00,
preferences: {},
provider: {
chainId: '1',
ticker: 'ETH',
type: 'mainnet',
},
}, },
appState: { appState: {
sidebar: { sidebar: {

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import contracts from 'eth-contract-metadata'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import TokenCell from '../token-cell' import TokenCell from '../token-cell'
@ -10,15 +9,6 @@ import { useSelector } from 'react-redux'
import { getAssetImages } from '../../../selectors' import { getAssetImages } from '../../../selectors'
import { getTokens } from '../../../ducks/metamask/metamask' import { getTokens } from '../../../ducks/metamask/metamask'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
export default function TokenList ({ onTokenClick }) { export default function TokenList ({ onTokenClick }) {
const t = useI18nContext() const t = useI18nContext()
const assetImages = useSelector(getAssetImages) const assetImages = useSelector(getAssetImages)

@ -0,0 +1 @@
export { default } from './transaction-icon'

@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import Approve from '../../ui/icon/approve-icon.component'
import Interaction from '../../ui/icon/interaction-icon.component'
import Receive from '../../ui/icon/receive-icon.component'
import Send from '../../ui/icon/send-icon.component'
import Sign from '../../ui/icon/sign-icon.component'
import {
TRANSACTION_CATEGORY_APPROVAL,
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
TRANSACTION_CATEGORY_INTERACTION,
TRANSACTION_CATEGORY_SEND,
TRANSACTION_CATEGORY_RECEIVE,
UNAPPROVED_STATUS,
FAILED_STATUS,
REJECTED_STATUS,
CANCELLED_STATUS,
DROPPED_STATUS,
SUBMITTED_STATUS,
APPROVED_STATUS,
} from '../../../helpers/constants/transactions'
const ICON_MAP = {
[TRANSACTION_CATEGORY_APPROVAL]: Approve,
[TRANSACTION_CATEGORY_INTERACTION]: Interaction,
[TRANSACTION_CATEGORY_SEND]: Send,
[TRANSACTION_CATEGORY_SIGNATURE_REQUEST]: Sign,
[TRANSACTION_CATEGORY_RECEIVE]: Receive,
}
const FAIL_COLOR = '#D73A49'
const PENDING_COLOR = '#6A737D'
const OK_COLOR = '#2F80ED'
const COLOR_MAP = {
[SUBMITTED_STATUS]: PENDING_COLOR,
[UNAPPROVED_STATUS]: PENDING_COLOR,
[APPROVED_STATUS]: PENDING_COLOR,
[FAILED_STATUS]: FAIL_COLOR,
[REJECTED_STATUS]: FAIL_COLOR,
[CANCELLED_STATUS]: FAIL_COLOR,
[DROPPED_STATUS]: FAIL_COLOR,
}
export default function TransactionIcon ({ status, category }) {
const color = COLOR_MAP[status] || OK_COLOR
const Icon = ICON_MAP[category]
return <Icon color={color} size={28} />
}
TransactionIcon.propTypes = {
status: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
}

@ -1,10 +1,4 @@
.transaction-list-item { .transaction-list-item {
cursor: pointer;
&:hover {
background-color: $Grey-000;
}
&__primary-currency { &__primary-currency {
color: $Black-100; color: $Black-100;
} }
@ -15,29 +9,14 @@
color: $Grey-500; color: $Grey-500;
} }
&--pending { &--unconfirmed {
color: $Grey-500; color: $Grey-500;
} }
&--pending &__primary-currency { &--unconfirmed &__primary-currency {
color: $Grey-500; color: $Grey-500;
} }
&__status {
&--unapproved {
color: $flamingo;
}
&--failed {
color: $valencia;
}
&--cancelled {
color: $valencia;
}
&--queued {
color: $Grey-500;
}
}
&__pending-actions { &__pending-actions {
padding-top: 12px; padding-top: 12px;
display: flex; display: flex;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save