Implement asset page (#8696)

A new page has been created for viewing assets. This replaces the old
`selectedToken` state, which previously would augment the home page
to show token-specific information.

The new asset page shows the standard token overview as seen previously
on the home page, plus a history filtered to show just transactions
relevant to that token.

The actions that were available in the old token list menu have been
moved to a "Token Options" menu that mirrors the "Account Options"
menu.

The `selectedTokenAddress` state has been removed, as it is no longer
being used for anything.

`getMetaMetricState` has been renamed to `getBackgroundMetaMetricState`
because its sole purpose is extracting data from the background state
to send metrics from the background. It's not really a selector, but
it was convenient for it to use the same selectors the UI uses to
extract background data, so I left it there for now.

A new Redux store has been added to track state related to browser history.
The most recent "overview" page (i.e. the home page or the asset page) is
currently being tracked, so that actions taken from the asset page can return
the user back to the asset page when the action has finished.
feature/default_network_editable
Mark Stacey 5 years ago committed by GitHub
parent ec2e5c848b
commit df85ab6e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/_locales/am/messages.json
  2. 3
      app/_locales/ar/messages.json
  3. 3
      app/_locales/bg/messages.json
  4. 3
      app/_locales/bn/messages.json
  5. 3
      app/_locales/ca/messages.json
  6. 3
      app/_locales/cs/messages.json
  7. 3
      app/_locales/da/messages.json
  8. 3
      app/_locales/de/messages.json
  9. 3
      app/_locales/el/messages.json
  10. 8
      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/hn/messages.json
  21. 3
      app/_locales/hr/messages.json
  22. 3
      app/_locales/ht/messages.json
  23. 3
      app/_locales/hu/messages.json
  24. 3
      app/_locales/id/messages.json
  25. 3
      app/_locales/it/messages.json
  26. 3
      app/_locales/ja/messages.json
  27. 3
      app/_locales/kn/messages.json
  28. 3
      app/_locales/ko/messages.json
  29. 3
      app/_locales/lt/messages.json
  30. 3
      app/_locales/lv/messages.json
  31. 3
      app/_locales/ms/messages.json
  32. 3
      app/_locales/nl/messages.json
  33. 3
      app/_locales/no/messages.json
  34. 3
      app/_locales/ph/messages.json
  35. 3
      app/_locales/pl/messages.json
  36. 3
      app/_locales/pt/messages.json
  37. 3
      app/_locales/pt_BR/messages.json
  38. 3
      app/_locales/ro/messages.json
  39. 3
      app/_locales/ru/messages.json
  40. 3
      app/_locales/sk/messages.json
  41. 3
      app/_locales/sl/messages.json
  42. 3
      app/_locales/sr/messages.json
  43. 3
      app/_locales/sv/messages.json
  44. 3
      app/_locales/sw/messages.json
  45. 3
      app/_locales/ta/messages.json
  46. 3
      app/_locales/th/messages.json
  47. 3
      app/_locales/tr/messages.json
  48. 3
      app/_locales/uk/messages.json
  49. 3
      app/_locales/vi/messages.json
  50. 3
      app/_locales/zh_CN/messages.json
  51. 3
      app/_locales/zh_TW/messages.json
  52. 1
      app/scripts/background.js
  53. 4
      app/scripts/lib/backend-metametrics.js
  54. 3
      development/states/confirm-sig-requests.json
  55. 3
      development/states/currency-localization.json
  56. 1
      test/data/mock-state.json
  57. 18
      test/e2e/metamask-ui.spec.js
  58. 9
      test/unit/ui/app/reducers/metamask.spec.js
  59. 12
      ui/app/components/app/asset-list-item/asset-list-item.js
  60. 11
      ui/app/components/app/asset-list-item/asset-list-item.scss
  61. 20
      ui/app/components/app/asset-list/asset-list.js
  62. 67
      ui/app/components/app/dropdowns/token-menu-dropdown.js
  63. 2
      ui/app/components/app/menu-bar/index.scss
  64. 12
      ui/app/components/app/signature-request-original/signature-request-original.component.js
  65. 2
      ui/app/components/app/signature-request-original/signature-request-original.container.js
  66. 30
      ui/app/components/app/token-cell/token-cell.component.js
  67. 1
      ui/app/components/app/token-cell/token-cell.container.js
  68. 4
      ui/app/components/app/token-cell/token-cell.scss
  69. 1
      ui/app/components/app/token-cell/token-cell.test.js
  70. 10
      ui/app/components/app/wallet-overview/eth-overview.js
  71. 8
      ui/app/components/app/wallet-overview/index.scss
  72. 14
      ui/app/components/app/wallet-overview/token-overview.js
  73. 10
      ui/app/components/app/wallet-overview/wallet-overview.js
  74. 4
      ui/app/contexts/metametrics.js
  75. 38
      ui/app/ducks/history/history.js
  76. 2
      ui/app/ducks/index.js
  77. 57
      ui/app/ducks/metamask/metamask.js
  78. 2
      ui/app/helpers/constants/routes.js
  79. 4
      ui/app/helpers/utils/util.js
  80. 7
      ui/app/pages/add-token/add-token.component.js
  81. 6
      ui/app/pages/add-token/add-token.container.js
  82. 1
      ui/app/pages/add-token/tests/add-token.test.js
  83. 65
      ui/app/pages/asset/asset.js
  84. 45
      ui/app/pages/asset/asset.scss
  85. 25
      ui/app/pages/asset/components/asset-breadcrumb.js
  86. 26
      ui/app/pages/asset/components/asset-navigation.js
  87. 59
      ui/app/pages/asset/components/token-options.js
  88. 1
      ui/app/pages/asset/index.js
  89. 12
      ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
  90. 6
      ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
  91. 16
      ui/app/pages/confirm-add-token/confirm-add-token.component.js
  92. 6
      ui/app/pages/confirm-add-token/confirm-add-token.container.js
  93. 23
      ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
  94. 2
      ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.container.js
  95. 23
      ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
  96. 2
      ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js
  97. 15
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
  98. 2
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
  99. 13
      ui/app/pages/confirm-transaction/conf-tx.js
  100. 10
      ui/app/pages/confirm-transaction/confirm-transaction.component.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -519,9 +519,6 @@
"hide": {
"message": "ደብቅ"
},
"hideToken": {
"message": "ተለዋጭ ስም ደብቅ"
},
"hideTokenPrompt": {
"message": "ተለዋጭ ስም ይደበቅ?"
},

@ -515,9 +515,6 @@
"hide": {
"message": "إخفاء"
},
"hideToken": {
"message": "إخفاء الرمز"
},
"hideTokenPrompt": {
"message": "أتريد إخفاء العملة الرمزية؟"
},

@ -515,9 +515,6 @@
"hide": {
"message": "Скриване"
},
"hideToken": {
"message": "Скриване на жетон"
},
"hideTokenPrompt": {
"message": "Скриване на жетон?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "লন"
},
"hideToken": {
"message": "টন লন"
},
"hideTokenPrompt": {
"message": "টন লন?"
},

@ -506,9 +506,6 @@
"hide": {
"message": "Amaga"
},
"hideToken": {
"message": "Amagar Fitxa"
},
"hideTokenPrompt": {
"message": "Amagar fitxa?"
},

@ -197,9 +197,6 @@
"hide": {
"message": "Skrýt"
},
"hideToken": {
"message": "Skrýt token"
},
"hideTokenPrompt": {
"message": "Skrýt token?"
},

@ -512,9 +512,6 @@
"hide": {
"message": "Skjul"
},
"hideToken": {
"message": "Skjul token"
},
"hideTokenPrompt": {
"message": "Skjul Token?"
},

@ -504,9 +504,6 @@
"hide": {
"message": "Ausblenden"
},
"hideToken": {
"message": "Token ausblenden"
},
"hideTokenPrompt": {
"message": "Token ausblenden?"
},

@ -516,9 +516,6 @@
"hide": {
"message": "Απόκρυψη"
},
"hideToken": {
"message": "Απόκρυψη Token"
},
"hideTokenPrompt": {
"message": "Απόκρυψη του Token;"
},

@ -759,8 +759,9 @@
"hide": {
"message": "Hide"
},
"hideToken": {
"message": "Hide Token"
"hideTokenSymbol": {
"message": "Hide $1",
"description": "$1 is the symbol for a token (e.g. 'DAI')"
},
"hideTokenPrompt": {
"message": "Hide Token?"
@ -1544,6 +1545,9 @@
"tokenContractAddress": {
"message": "Token Contract Address"
},
"tokenOptions": {
"message": "Token options"
},
"tokenSymbol": {
"message": "Token Symbol"
},

@ -419,9 +419,6 @@
"hide": {
"message": "Ocultar"
},
"hideToken": {
"message": "Ocultar token"
},
"hideTokenPrompt": {
"message": "¿Ocultar token?"
},

@ -510,9 +510,6 @@
"hide": {
"message": "Ocultar"
},
"hideToken": {
"message": "Ocultar token"
},
"hideTokenPrompt": {
"message": "¿Ocultar token?"
},

@ -515,9 +515,6 @@
"hide": {
"message": "Peida"
},
"hideToken": {
"message": "Peida luba"
},
"hideTokenPrompt": {
"message": "Peida luba?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "عدم نمایش"
},
"hideToken": {
"message": "مخفی سازی رمزیاب"
},
"hideTokenPrompt": {
"message": "آیا رمزیاب مخفی شود؟"
},

@ -516,9 +516,6 @@
"hide": {
"message": "Piilota"
},
"hideToken": {
"message": "Piilota tietue"
},
"hideTokenPrompt": {
"message": "Piilotetaanko tietue?"
},

@ -479,9 +479,6 @@
"hide": {
"message": "Itago"
},
"hideToken": {
"message": "Itago ang Token"
},
"hideTokenPrompt": {
"message": "Itago ang Token?"
},

@ -504,9 +504,6 @@
"hide": {
"message": "Cacher"
},
"hideToken": {
"message": "Masquer le jeton"
},
"hideTokenPrompt": {
"message": "Masquer le jeton?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "הסתר"
},
"hideToken": {
"message": "הסתר טוקן"
},
"hideTokenPrompt": {
"message": "להסתיר טוקן?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "छ"
},
"hideToken": {
"message": "टकन छि?"
},
"hideTokenPrompt": {
"message": "टकन छि?"
},

@ -173,9 +173,6 @@
"hide": {
"message": "छ"
},
"hideToken": {
"message": "टकन छि"
},
"hideTokenPrompt": {
"message": "टकन छिn?"
},

@ -515,9 +515,6 @@
"hide": {
"message": "Sakrij preglednik"
},
"hideToken": {
"message": "Sakrij token"
},
"hideTokenPrompt": {
"message": "Sakriti token?"
},

@ -287,9 +287,6 @@
"hide": {
"message": "Kache"
},
"hideToken": {
"message": "Kache Token"
},
"hideTokenPrompt": {
"message": "Kache Token?"
},

@ -515,9 +515,6 @@
"hide": {
"message": "Elrejtés"
},
"hideToken": {
"message": "Token elrejtése"
},
"hideTokenPrompt": {
"message": "Elrejted a tokent?"
},

@ -506,9 +506,6 @@
"hide": {
"message": "Sembunyikan"
},
"hideToken": {
"message": "Sembunyikan Token"
},
"hideTokenPrompt": {
"message": "Sembunyikan Token?"
},

@ -633,9 +633,6 @@
"hide": {
"message": "Nascondi"
},
"hideToken": {
"message": "Nascondi Token"
},
"hideTokenPrompt": {
"message": "Nascondi Token?"
},

@ -242,9 +242,6 @@
"hide": {
"message": "隠す"
},
"hideToken": {
"message": "トークンを隠す"
},
"hideTokenPrompt": {
"message": "トークンを隠しますか?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "ಮರಿ"
},
"hideToken": {
"message": "ಟಕನ ಮರಿ"
},
"hideTokenPrompt": {
"message": "ಟಕನ ಮರ?"
},

@ -513,9 +513,6 @@
"hide": {
"message": "숨기기"
},
"hideToken": {
"message": "토큰 숨기기"
},
"hideTokenPrompt": {
"message": "토큰 숨기기?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "Slėpti"
},
"hideToken": {
"message": "Slėpti prieigos raktą"
},
"hideTokenPrompt": {
"message": "Slėpti prieigos raktą?"
},

@ -515,9 +515,6 @@
"hide": {
"message": "Slēpt"
},
"hideToken": {
"message": "Paslēpt marķieri"
},
"hideTokenPrompt": {
"message": "Paslēpt žetonu?"
},

@ -503,9 +503,6 @@
"hide": {
"message": "Sembunyikan"
},
"hideToken": {
"message": "Sembunyikan Token"
},
"hideTokenPrompt": {
"message": "Sembunyikan Token?"
},

@ -167,9 +167,6 @@
"hide": {
"message": "Verbergen"
},
"hideToken": {
"message": "Token verbergen"
},
"hideTokenPrompt": {
"message": "Token verbergen?"
},

@ -509,9 +509,6 @@
"hide": {
"message": "Skjul"
},
"hideToken": {
"message": "Skjul tokenet"
},
"hideTokenPrompt": {
"message": "Skjul sjetonger?"
},

@ -137,9 +137,6 @@
"hide": {
"message": "Itago"
},
"hideToken": {
"message": "Itago ang Token"
},
"hideTokenPrompt": {
"message": "Itago ang Token?"
},

@ -516,9 +516,6 @@
"hide": {
"message": "Schowaj"
},
"hideToken": {
"message": "Schowaj token"
},
"hideTokenPrompt": {
"message": "Schować token?"
},

@ -173,9 +173,6 @@
"hide": {
"message": "Ocultar"
},
"hideToken": {
"message": "Ocultar Token"
},
"hideTokenPrompt": {
"message": "Ocultar Token?"
},

@ -513,9 +513,6 @@
"hide": {
"message": "Ocultar"
},
"hideToken": {
"message": "Ocultar Token"
},
"hideTokenPrompt": {
"message": "Esconder token?"
},

@ -509,9 +509,6 @@
"hide": {
"message": "Ascunde"
},
"hideToken": {
"message": "Ascunde tokenul"
},
"hideTokenPrompt": {
"message": "Ascunde simbol?"
},

@ -203,9 +203,6 @@
"hide": {
"message": "Скрыть"
},
"hideToken": {
"message": "Скрыть токен"
},
"hideTokenPrompt": {
"message": "Скрыть токен?"
},

@ -507,9 +507,6 @@
"hide": {
"message": "Skrýt"
},
"hideToken": {
"message": "Skrýt token"
},
"hideTokenPrompt": {
"message": "Skrýt token?"
},

@ -510,9 +510,6 @@
"hide": {
"message": "Skrij"
},
"hideToken": {
"message": "Skrij žeton"
},
"hideTokenPrompt": {
"message": "Skrijem žeton?"
},

@ -516,9 +516,6 @@
"hide": {
"message": "Сакриј"
},
"hideToken": {
"message": "Sakrij token"
},
"hideTokenPrompt": {
"message": "Da li želite da sakrijete token?"
},

@ -509,9 +509,6 @@
"hide": {
"message": "Dölj"
},
"hideToken": {
"message": "Göm token"
},
"hideTokenPrompt": {
"message": "Göm token?"
},

@ -506,9 +506,6 @@
"hide": {
"message": "Ficha"
},
"hideToken": {
"message": "Ficha Kianzio"
},
"hideTokenPrompt": {
"message": "Ungependa Kianzio?"
},

@ -194,9 +194,6 @@
"hide": {
"message": "மற"
},
"hideToken": {
"message": "டகன மற"
},
"hideTokenPrompt": {
"message": "டகன மற?"
},

@ -248,9 +248,6 @@
"hide": {
"message": "ซอน"
},
"hideToken": {
"message": "ซอนโทเคน"
},
"hideTokenPrompt": {
"message": "ซอนโทเคนหรอไม?"
},

@ -200,9 +200,6 @@
"hide": {
"message": "Gizle"
},
"hideToken": {
"message": "Jetonu gizle"
},
"hideTokenPrompt": {
"message": "Jetonu gizle?"
},

@ -519,9 +519,6 @@
"hide": {
"message": "Сховати"
},
"hideToken": {
"message": "Приховати токен"
},
"hideTokenPrompt": {
"message": "Приховати токен?"
},

@ -149,9 +149,6 @@
"hide": {
"message": "Ẩn"
},
"hideToken": {
"message": "Ẩn mã token"
},
"hideTokenPrompt": {
"message": "Ẩn mã token?"
},

@ -513,9 +513,6 @@
"hide": {
"message": "隐藏"
},
"hideToken": {
"message": "隐藏代币"
},
"hideTokenPrompt": {
"message": "隐藏代币?"
},

@ -516,9 +516,6 @@
"hide": {
"message": "隱藏"
},
"hideToken": {
"message": "隱藏代幣"
},
"hideTokenPrompt": {
"message": "隱藏代幣?"
},

@ -102,7 +102,6 @@ initialize().catch(log.error)
* @property {Object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions.
* @property {Array} frequentRpcList - A list of frequently used RPCs, including custom user-provided ones.
* @property {Array} addressBook - A list of previously sent to addresses.
* @property {address} selectedTokenAddress - Used to indicate if a token is globally selected. Should be deprecated in favor of UI-centric token selection.
* @property {Object} contractExchangeRates - Info about current token prices.
* @property {Array} tokens - Tokens held by the current user, including their balances.
* @property {Object} send - TODO: Document

@ -1,4 +1,4 @@
import { getMetaMetricState } from '../../../ui/app/selectors'
import { getBackgroundMetaMetricState } from '../../../ui/app/selectors'
import { sendMetaMetricsEvent } from '../../../ui/app/helpers/utils/metametrics.util'
const inDevelopment = process.env.NODE_ENV === 'development'
@ -8,7 +8,7 @@ const METAMETRICS_TRACKING_URL = inDevelopment
: 'http://www.metamask.io/metametrics-prod'
export default function backEndMetaMetricsEvent (metaMaskState, eventData) {
const stateEventData = getMetaMetricState({ metamask: metaMaskState })
const stateEventData = getBackgroundMetaMetricState({ metamask: metaMaskState })
if (stateEventData.participateInMetaMetrics) {
sendMetaMetricsEvent({

@ -527,5 +527,8 @@
},
"unconnectedAccount": {
"state": "CLOSED"
},
"history": {
"mostRecentOverviewPage": "/"
}
}

@ -478,5 +478,8 @@
},
"unconnectedAccount": {
"state": "CLOSED"
},
"history": {
"mostRecentOverviewPage": "/"
}
}

@ -113,7 +113,6 @@
}
}
},
"selectedTokenAddress": "0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d",
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},

@ -903,7 +903,6 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
await driver.clickElement(By.css(`[data-testid="home__history-tab"]`))
await driver.wait(async () => {
const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
@ -996,13 +995,6 @@ describe('MetaMask', function () {
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Send\sTST/), 10000)
await driver.clickElement(By.css('[data-testid="home__asset-tab"]'))
await driver.clickElement(By.css('[data-testid="wallet-balance"]'))
await driver.clickElement(By.css('.token-cell'))
await driver.delay(1000)
const tokenBalanceAmount = await driver.findElements(By.css('.token-overview__primary-balance'))
await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000)
})
@ -1025,8 +1017,6 @@ describe('MetaMask', function () {
await driver.switchToWindow(extension)
await driver.delay(regularDelayMs)
await driver.clickElement(By.css('[data-testid="home__history-tab"]'))
await driver.wait(async () => {
const pendingTxes = await driver.findElements(By.css('.transaction-list__pending-transactions .transaction-list-item'))
return pendingTxes.length === 1
@ -1227,12 +1217,9 @@ describe('MetaMask', function () {
describe('Hide token', function () {
it('hides the token when clicked', async function () {
await driver.clickElement(By.css('[data-testid="home__asset-tab"]'))
await driver.clickElement(By.css('.token-cell__ellipsis'))
await driver.clickElement(By.css('[data-testid="token-options__button"]'))
const byTokenMenuDropdownOption = By.css('.menu__item--clickable')
await driver.clickElement(byTokenMenuDropdownOption)
await driver.clickElement(By.css('[data-testid="token-options__hide"]'))
const confirmHideModal = await driver.findElement(By.css('span .modal'))
@ -1245,6 +1232,7 @@ describe('MetaMask', function () {
describe('Add existing token using search', function () {
it('clicks on the Add Token button', async function () {
await driver.clickElement(By.css('[data-testid="asset__back"]'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Add Token')]`))
await driver.delay(regularDelayMs)
})

@ -52,15 +52,6 @@ describe('MetaMask Reducers', function () {
assert.equal(state.selectedAddress, 'test address')
})
it('sets select ', function () {
const state = reduceMetamask({}, {
type: actionConstants.SET_SELECTED_TOKEN,
value: 'test token',
})
assert.equal(state.selectedTokenAddress, 'test token')
})
it('sets account label', function () {
const state = reduceMetamask({}, {
type: actionConstants.SET_ACCOUNT_LABEL,

@ -4,12 +4,10 @@ import classnames from 'classnames'
import Identicon from '../../ui/identicon'
const AssetListItem = ({
active,
children,
className,
'data-testid': dataTestId,
iconClassName,
menu,
onClick,
tokenAddress,
tokenImage,
@ -17,9 +15,7 @@ const AssetListItem = ({
}) => {
return (
<div
className={classnames('asset-list-item__container', className, {
'asset-list-item__container--active': active,
})}
className={classnames('asset-list-item__container', className)}
data-testid={dataTestId}
onClick={onClick}
>
@ -35,18 +31,16 @@ const AssetListItem = ({
{ children }
</div>
{ warning }
{ menu }
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</div>
)
}
AssetListItem.propTypes = {
active: PropTypes.bool,
children: PropTypes.node.isRequired,
className: PropTypes.string,
'data-testid': PropTypes.string,
iconClassName: PropTypes.string,
menu: PropTypes.node,
onClick: PropTypes.func.isRequired,
tokenAddress: PropTypes.string,
tokenImage: PropTypes.string,
@ -54,10 +48,8 @@ AssetListItem.propTypes = {
}
AssetListItem.defaultProps = {
active: undefined,
className: undefined,
'data-testid': undefined,
menu: undefined,
iconClassName: undefined,
tokenAddress: undefined,
tokenImage: undefined,

@ -3,9 +3,12 @@
display: flex;
padding: 24px 16px;
align-items: center;
border-top: 1px solid $mercury;
border-bottom: 1px solid $mercury;
cursor: pointer;
&--active {
background: #D9D7DA;
&:hover {
background-color: $Grey-000;
}
}
@ -16,4 +19,8 @@
flex: 1;
min-width: 0;
}
&__chevron-right {
color: $Grey-500;
}
}

@ -1,5 +1,6 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import AddTokenButton from '../add-token-button'
import TokenList from '../token-list'
@ -9,14 +10,12 @@ import CurrencyDisplay from '../../ui/currency-display'
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'
import { getCurrentAccountWithSendEtherInfo, getShouldShowFiat } from '../../../selectors/selectors'
import { setSelectedToken } from '../../../store/actions'
import { getCurrentAccountWithSendEtherInfo, getNativeCurrency, getShouldShowFiat } from '../../../selectors'
const AssetList = () => {
const dispatch = useDispatch()
const AssetList = ({ onClickAsset }) => {
const history = useHistory()
const selectedAccountBalance = useSelector((state) => getCurrentAccountWithSendEtherInfo(state).balance)
const selectedTokenAddress = useSelector((state) => state.metamask.selectedTokenAddress)
const nativeCurrency = useSelector(getNativeCurrency)
const showFiat = useSelector(getShouldShowFiat)
const selectTokenEvent = useMetricEvent({
eventOpts: {
@ -45,8 +44,7 @@ const AssetList = () => {
return (
<>
<AssetListItem
active={!selectedTokenAddress}
onClick={() => dispatch(setSelectedToken())}
onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance"
>
<CurrencyDisplay
@ -68,7 +66,7 @@ const AssetList = () => {
</AssetListItem>
<TokenList
onTokenClick={(tokenAddress) => {
dispatch(setSelectedToken(tokenAddress))
onClickAsset(tokenAddress)
selectTokenEvent()
}}
/>
@ -82,4 +80,8 @@ const AssetList = () => {
)
}
AssetList.propTypes = {
onClickAsset: PropTypes.func.isRequired,
}
export default AssetList

@ -1,67 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actions from '../../../store/actions'
import { createAccountLink as genAccountLink } from '@metamask/etherscan-link'
import { Menu, Item, CloseArea } from './components/menu'
class TokenMenuDropdown extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
onClose: PropTypes.func.isRequired,
showHideTokenConfirmationModal: PropTypes.func.isRequired,
token: PropTypes.object.isRequired,
network: PropTypes.string.isRequired,
}
onClose = (e) => {
e.stopPropagation()
this.props.onClose()
}
render () {
const { showHideTokenConfirmationModal } = this.props
return (
<Menu className="token-menu-dropdown" isShowing>
<CloseArea onClick={this.onClose} />
<Item
onClick={(e) => {
e.stopPropagation()
showHideTokenConfirmationModal(this.props.token)
this.props.onClose()
}}
text={this.context.t('hideToken')}
/>
<Item
onClick={(e) => {
e.stopPropagation()
const url = genAccountLink(this.props.token.address, this.props.network)
global.platform.openTab({ url })
this.props.onClose()
}}
text={this.context.t('viewOnEtherscan')}
/>
</Menu>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown)
function mapStateToProps (state) {
return {
network: state.metamask.network,
}
}
function mapDispatchToProps (dispatch) {
return {
showHideTokenConfirmationModal: (token) => {
dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))
},
}
}

@ -2,8 +2,6 @@
display: grid;
grid-template-columns: 30% minmax(30%, 1fr) 30%;
column-gap: 5px;
margin-bottom: 24px;
padding: 0 8px;
border-bottom: 1px solid $Grey-100;

@ -10,7 +10,6 @@ import Identicon from '../../ui/identicon'
import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component'
import { conversionUtil } from '../../../helpers/utils/conversion-util'
import Button from '../../ui/button'
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
export default class SignatureRequestOriginal extends Component {
static contextTypes = {
@ -28,6 +27,7 @@ export default class SignatureRequestOriginal extends Component {
clearConfirmTransaction: PropTypes.func.isRequired,
conversionRate: PropTypes.number,
history: PropTypes.object.isRequired,
mostRecentOverviewPage: PropTypes.string.isRequired,
requesterAddress: PropTypes.string,
sign: PropTypes.func.isRequired,
txData: PropTypes.object.isRequired,
@ -268,7 +268,7 @@ export default class SignatureRequestOriginal extends Component {
}
renderFooter = () => {
const { cancel, sign } = this.props
const { cancel, clearConfirmTransaction, history, mostRecentOverviewPage, sign } = this.props
return (
<div className="request-signature__footer">
@ -286,8 +286,8 @@ export default class SignatureRequestOriginal extends Component {
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('cancel') }
@ -306,8 +306,8 @@ export default class SignatureRequestOriginal extends Component {
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('sign') }

@ -10,12 +10,14 @@ import {
import { getAccountByAddress } from '../../../helpers/utils/util'
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'
import SignatureRequestOriginal from './signature-request-original.component'
import { getMostRecentOverviewPage } from '../../../ducks/history/history'
function mapStateToProps (state) {
return {
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state),
// not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state),
}

@ -2,7 +2,6 @@ import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util'
import TokenMenuDropdown from '../dropdowns/token-menu-dropdown.js'
import Tooltip from '../../ui/tooltip-v2'
import { I18nContext } from '../../../contexts/i18n'
import AssetListItem from '../asset-list-item'
@ -15,7 +14,6 @@ export default class TokenCell extends Component {
outdatedBalance: PropTypes.bool,
symbol: PropTypes.string,
string: PropTypes.string,
selectedTokenAddress: PropTypes.string,
contractExchangeRates: PropTypes.object,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
@ -28,18 +26,12 @@ export default class TokenCell extends Component {
outdatedBalance: false,
}
state = {
tokenMenuOpen: false,
}
render () {
const t = this.context
const { tokenMenuOpen } = this.state
const {
address,
symbol,
string,
selectedTokenAddress,
contractExchangeRates,
conversionRate,
onClick,
@ -71,26 +63,6 @@ export default class TokenCell extends Component {
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
const menu = (
<>
<div>
<i
className="fa fa-ellipsis-h fa-lg token-cell__ellipsis cursor-pointer"
onClick={(e) => {
e.stopPropagation()
this.setState({ tokenMenuOpen: true })
}}
/>
</div>
{tokenMenuOpen && (
<TokenMenuDropdown
onClose={() => this.setState({ tokenMenuOpen: false })}
token={{ symbol, address }}
/>
)}
</>
)
const warning = outdatedBalance
? (
<Tooltip
@ -117,10 +89,8 @@ export default class TokenCell extends Component {
return (
<AssetListItem
active={selectedTokenAddress === address}
className={classnames('token-cell', { 'token-cell--outdated': outdatedBalance })}
iconClassName="token-cell__icon"
menu={menu}
onClick={onClick.bind(null, address)}
tokenAddress={address}
tokenImage={image}

@ -7,7 +7,6 @@ function mapStateToProps (state) {
contractExchangeRates: state.metamask.contractExchangeRates,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
selectedTokenAddress: state.metamask.selectedTokenAddress,
userAddress: getSelectedAddress(state),
}
}

@ -34,10 +34,6 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
opacity: 0.5
}
&__ellipsis {
line-height: 38px;
}
&__balance-wrapper {
flex: 1;
flex-flow: row wrap;

@ -15,7 +15,6 @@ describe('Token Cell', function () {
const state = {
metamask: {
currentCurrency: 'usd',
selectedTokenAddress: '0xToken',
selectedAddress: '0xAddress',
contractExchangeRates: {
'0xAnotherToken': 0.015,

@ -1,4 +1,5 @@
import React, { useContext } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import classnames from 'classnames'
import { useHistory } from 'react-router-dom'
@ -15,7 +16,7 @@ import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
import { showModal } from '../../../store/actions'
import { isBalanceCached, getSelectedAccount, getShouldShowFiat } from '../../../selectors/selectors'
const EthOverview = () => {
const EthOverview = ({ className }) => {
const dispatch = useDispatch()
const t = useContext(I18nContext)
const sendEvent = useMetricEvent({
@ -99,13 +100,18 @@ const EthOverview = () => {
</Button>
</>
)}
icon={<Identicon diameter={50} />}
className={className}
icon={<Identicon diameter={32} />}
/>
)
}
EthOverview.propTypes = {
className: PropTypes.string,
}
EthOverview.defaultProps = {
className: undefined,
}
export default EthOverview

@ -3,11 +3,12 @@
justify-content: space-between;
align-items: center;
flex: 1;
height: 54px;
height: 209px;
min-width: 0;
padding-top: 10px;
flex-direction: column;
height: initial;
width: 100%;
&__balance {
@ -21,7 +22,8 @@
&__buttons {
display: flex;
flex-direction: row;
margin-bottom: 16px;
height: 44px;
margin-bottom: 24px;
}
}

@ -11,9 +11,9 @@ import WalletOverview from './wallet-overview'
import { SEND_ROUTE } from '../../../helpers/constants/routes'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { getAssetImages } from '../../../selectors/selectors'
import { updateSend } from '../../../store/actions'
import { updateSendToken } from '../../../store/actions'
const TokenOverview = ({ token }) => {
const TokenOverview = ({ className, token }) => {
const dispatch = useDispatch()
const t = useContext(I18nContext)
const sendTokenEvent = useMetricEvent({
@ -43,16 +43,17 @@ const TokenOverview = ({ token }) => {
className="token-overview__button"
onClick={() => {
sendTokenEvent()
dispatch(updateSend({ token }))
dispatch(updateSendToken(token))
history.push(SEND_ROUTE)
}}
>
{ t('send') }
</Button>
)}
className={className}
icon={(
<Identicon
diameter={50}
diameter={32}
address={token.address}
image={assetImages[token.address]}
/>
@ -62,6 +63,7 @@ const TokenOverview = ({ token }) => {
}
TokenOverview.propTypes = {
className: PropTypes.string,
token: PropTypes.shape({
address: PropTypes.string.isRequired,
decimals: PropTypes.number,
@ -69,4 +71,8 @@ TokenOverview.propTypes = {
}).isRequired,
}
TokenOverview.defaultProps = {
className: undefined,
}
export default TokenOverview

@ -1,9 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const WalletOverview = ({ balance, buttons, icon }) => {
const WalletOverview = ({ balance, buttons, className, icon }) => {
return (
<div className="wallet-overview">
<div className={classnames('wallet-overview', className)}>
<div className="wallet-overview__balance">
{ icon }
{ balance }
@ -18,7 +19,12 @@ const WalletOverview = ({ balance, buttons, icon }) => {
WalletOverview.propTypes = {
balance: PropTypes.element.isRequired,
buttons: PropTypes.element.isRequired,
className: PropTypes.string,
icon: PropTypes.element.isRequired,
}
WalletOverview.defaultProps = {
className: undefined,
}
export default WalletOverview

@ -6,11 +6,11 @@ import { captureException } from '@sentry/browser'
import {
getCurrentNetworkId,
getSelectedAsset,
getAccountType,
getNumberOfAccounts,
getNumberOfTokens,
} from '../selectors/selectors'
import { getSendToken } from '../selectors/send'
import {
txDataSelector,
} from '../selectors/confirm-transaction'
@ -31,7 +31,7 @@ export function MetaMetricsProvider ({ children }) {
const txData = useSelector(txDataSelector) || {}
const network = useSelector(getCurrentNetworkId)
const environmentType = getEnvironmentType()
const activeCurrency = useSelector(getSelectedAsset)
const activeCurrency = useSelector(getSendToken)?.symbol
const accountType = useSelector(getAccountType)
const confirmTransactionOrigin = txData.origin
const metaMetricsId = useSelector((state) => state.metamask.metaMetricsId)

@ -0,0 +1,38 @@
import { createSlice } from '@reduxjs/toolkit'
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'
// Constants
const initialState = {
mostRecentOverviewPage: DEFAULT_ROUTE,
}
const name = 'history'
// Slice (reducer plus auto-generated actions and action creators)
const slice = createSlice({
name,
initialState,
reducers: {
pageChanged: (state, action) => {
const path = action.payload
if (path === DEFAULT_ROUTE || path.startsWith(ASSET_ROUTE)) {
state.mostRecentOverviewPage = path
}
},
},
})
const { actions, reducer } = slice
export default reducer
// Selectors
export const getMostRecentOverviewPage = (state) => state[name].mostRecentOverviewPage
// Actions / action-creators
export const { pageChanged } = actions

@ -6,6 +6,7 @@ import appStateReducer from './app/app'
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'
import gasReducer from './gas/gas.duck'
import { switchToConnected, unconnectedAccount } from './alerts'
import historyReducer from './history/history'
import { ALERT_TYPES } from '../../../app/scripts/controllers/alert'
export default combineReducers({
@ -14,6 +15,7 @@ export default combineReducers({
activeTab: (s) => (s === undefined ? null : s),
metamask: metamaskReducer,
appState: appStateReducer,
history: historyReducer,
send: sendReducer,
confirmTransaction: confirmTransactionReducer,
gas: gasReducer,

@ -11,7 +11,6 @@ export default function reduceMetamask (state = {}, action) {
unapprovedTxs: {},
frequentRpcList: [],
addressBook: [],
selectedTokenAddress: null,
contractExchangeRates: {},
tokens: [],
pendingTokens: {},
@ -87,33 +86,6 @@ export default function reduceMetamask (state = {}, action) {
selectedAddress: action.value,
}
case actionConstants.SET_SELECTED_TOKEN: {
const newState = {
...metamaskState,
selectedTokenAddress: action.value,
}
const newSend = { ...metamaskState.send }
if (metamaskState.send.editingTransactionId && !action.value) {
delete newSend.token
const unapprovedTx = newState.unapprovedTxs[newSend.editingTransactionId] || {}
const txParams = unapprovedTx.txParams || {}
newState.unapprovedTxs = {
...newState.unapprovedTxs,
[newSend.editingTransactionId]: {
...unapprovedTx,
txParams: { ...txParams, data: '' },
},
}
newSend.tokenBalance = null
newSend.balance = '0'
newSend.from = unapprovedTx.from || ''
}
newState.send = newSend
return newState
}
case actionConstants.SET_ACCOUNT_LABEL:
const account = action.value.account
const name = action.value.label
@ -227,6 +199,35 @@ export default function reduceMetamask (state = {}, action) {
},
})
case actionConstants.UPDATE_SEND_TOKEN:
const newSend = {
...metamaskState.send,
token: action.value,
}
// erase token-related state when switching back to native currency
if (newSend.editingTransactionId && !newSend.token) {
const unapprovedTx = newSend?.unapprovedTxs?.[newSend.editingTransactionId] || {}
const txParams = unapprovedTx.txParams || {}
Object.assign(newSend, {
tokenBalance: null,
balance: '0',
from: unapprovedTx.from || '',
unapprovedTxs: {
...newSend.unapprovedTxs,
[newSend.editingTransactionId]: {
...unapprovedTx,
txParams: {
...txParams,
data: '',
},
},
},
})
}
return Object.assign(metamaskState, {
send: newSend,
})
case actionConstants.UPDATE_SEND_ENS_RESOLUTION:
return {
...metamaskState,

@ -1,6 +1,7 @@
const DEFAULT_ROUTE = '/'
const UNLOCK_ROUTE = '/unlock'
const LOCK_ROUTE = '/lock'
const ASSET_ROUTE = '/asset'
const SETTINGS_ROUTE = '/settings'
const GENERAL_ROUTE = '/settings/general'
const CONNECTIONS_ROUTE = '/settings/connections'
@ -57,6 +58,7 @@ const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request'
export {
DEFAULT_ROUTE,
ALERTS_ROUTE,
ASSET_ROUTE,
UNLOCK_ROUTE,
LOCK_ROUTE,
SETTINGS_ROUTE,

@ -256,10 +256,6 @@ export function exportAsFile (filename, data, type = 'text/csv') {
}
}
export function getTokenAddressFromTokenObject (token) {
return Object.values(token)[0].address.toLowerCase()
}
/**
* Safely checksumms a potentially-null address
*

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import ethUtil from 'ethereumjs-util'
import { checkExistingAddresses } from './util'
import { tokenInfoGetter } from '../../helpers/utils/token-util'
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
import TextField from '../../components/ui/text-field'
import TokenList from './token-list'
import TokenSearch from './token-search'
@ -24,6 +24,7 @@ class AddToken extends Component {
clearPendingTokens: PropTypes.func,
tokens: PropTypes.array,
identities: PropTypes.object,
mostRecentOverviewPage: PropTypes.string.isRequired,
}
state = {
@ -307,7 +308,7 @@ class AddToken extends Component {
}
render () {
const { history, clearPendingTokens } = this.props
const { history, clearPendingTokens, mostRecentOverviewPage } = this.props
return (
<PageContainer
@ -317,7 +318,7 @@ class AddToken extends Component {
disabled={this.hasError() || !this.hasSelected()}
onCancel={() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
}}
/>
)

@ -2,11 +2,13 @@ import { connect } from 'react-redux'
import AddToken from './add-token.component'
import { setPendingTokens, clearPendingTokens } from '../../store/actions'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
const mapStateToProps = ({ metamask }) => {
const { identities, tokens, pendingTokens } = metamask
const mapStateToProps = (state) => {
const { metamask: { identities, tokens, pendingTokens } } = state
return {
identities,
mostRecentOverviewPage: getMostRecentOverviewPage(state),
tokens,
pendingTokens,
}

@ -25,6 +25,7 @@ describe('Add Token', function () {
clearPendingTokens: sinon.spy(),
tokens: [],
identities: {},
mostRecentOverviewPage: '/',
}
describe('Add Token', function () {

@ -0,0 +1,65 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Redirect, useHistory, useParams } from 'react-router-dom'
import { createAccountLink } from '@metamask/etherscan-link'
import TransactionList from '../../components/app/transaction-list'
import { EthOverview, TokenOverview } from '../../components/app/wallet-overview'
import { getCurrentNetworkId, getSelectedIdentity } from '../../selectors/selectors'
import { getTokens } from '../../ducks/metamask/metamask'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import { showModal } from '../../store/actions'
import AssetNavigation from './components/asset-navigation'
import TokenOptions from './components/token-options'
const Asset = () => {
const dispatch = useDispatch()
const network = useSelector(getCurrentNetworkId)
const selectedAccountName = useSelector((state) => getSelectedIdentity(state).name)
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency)
const tokens = useSelector(getTokens)
const history = useHistory()
const { asset } = useParams()
const token = tokens.find((token) => token.address === asset)
let assetName
let optionsButton
if (token) {
assetName = token.symbol
optionsButton = (
<TokenOptions
onRemove={() => dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))}
onViewEtherscan={() => {
const url = createAccountLink(token.address, network)
global.platform.openTab({ url })
}}
tokenSymbol={token.symbol}
/>
)
} else if (asset === nativeCurrency) {
assetName = nativeCurrency
} else {
return <Redirect to={{ pathname: DEFAULT_ROUTE }} />
}
const overview = token
? <TokenOverview className="asset__overview" token={token} />
: <EthOverview className="asset__overview" />
return (
<div className="main-container asset__container">
<AssetNavigation
accountName={selectedAccountName}
assetName={assetName}
onBack={() => history.push(DEFAULT_ROUTE)}
optionsButton={optionsButton}
/>
{ overview }
<TransactionList tokenAddress={token?.address} />
</div>
)
}
export default Asset

@ -0,0 +1,45 @@
.asset {
&__container {
background-color: white;
}
&__overview {
box-shadow: 0px 3px 4px rgba(135, 134, 134, 0.16);
}
}
.asset-navigation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
height: 54px;
}
.asset-breadcrumb {
font-size: 14px;
color: $Black-100;
&__chevron {
padding: 0 10px 0 2px;
font-size: 16px;
background-color: inherit;
}
&__asset {
font-weight: bold;
}
}
.token-options {
&__button {
font-size: 20px;
color: $Black-100;
background-color: inherit;
padding: 2px 8px;
}
&__icon {
font-size: 16px;
}
}

@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
return (
<div className="asset-breadcrumb">
<button className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" onClick={onBack} />
<span>
{accountName}
</span>
&nbsp;/&nbsp;
<span className="asset-breadcrumb__asset">
{ assetName }
</span>
</div>
)
}
AssetBreadcrumb.propTypes = {
accountName: PropTypes.string.isRequired,
assetName: PropTypes.string.isRequired,
onBack: PropTypes.func.isRequired,
}
export default AssetBreadcrumb

@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import AssetBreadcrumb from './asset-breadcrumb'
const AssetNavigation = ({ accountName, assetName, onBack, optionsButton }) => {
return (
<div className="asset-navigation">
<AssetBreadcrumb accountName={accountName} assetName={assetName} onBack={onBack} />
{ optionsButton }
</div>
)
}
AssetNavigation.propTypes = {
accountName: PropTypes.string.isRequired,
assetName: PropTypes.string.isRequired,
onBack: PropTypes.func.isRequired,
optionsButton: PropTypes.element,
}
AssetNavigation.defaultProps = {
optionsButton: undefined,
}
export default AssetNavigation

@ -0,0 +1,59 @@
import React, { useContext, useState } from 'react'
import PropTypes from 'prop-types'
import { I18nContext } from '../../../contexts/i18n'
import { Menu, MenuItem } from '../../../components/ui/menu'
const TokenOptions = ({ onRemove, onViewEtherscan, tokenSymbol }) => {
const t = useContext(I18nContext)
const [tokenOptionsButtonElement, setTokenOptionsButtonElement] = useState(null)
const [tokenOptionsOpen, setTokenOptionsOpen] = useState(false)
return (
<>
<button
className="fas fa-ellipsis-v token-options__button"
data-testid="token-options__button"
onClick={() => setTokenOptionsOpen(true)}
ref={setTokenOptionsButtonElement}
title={t('tokenOptions')}
/>
{
tokenOptionsOpen
? (
<Menu anchorElement={tokenOptionsButtonElement} onHide={() => setTokenOptionsOpen(false)} >
<MenuItem
iconClassName="fas fa-external-link-alt token-options__icon"
data-testid="token-options__etherscan"
onClick={() => {
setTokenOptionsOpen(false)
onViewEtherscan()
}}
>
{ t('viewOnEtherscan') }
</MenuItem>
<MenuItem
iconClassName="fas fa-trash-alt token-options__icon"
data-testid="token-options__hide"
onClick={() => {
setTokenOptionsOpen(false)
onRemove()
}}
>
{ t('hideTokenSymbol', [tokenSymbol]) }
</MenuItem>
</Menu>
)
: null
}
</>
)
}
TokenOptions.propTypes = {
onRemove: PropTypes.func.isRequired,
onViewEtherscan: PropTypes.func.isRequired,
tokenSymbol: PropTypes.string.isRequired,
}
export default TokenOptions

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

@ -1,6 +1,5 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import Button from '../../components/ui/button'
import Identicon from '../../components/ui/identicon'
import TokenBalance from '../../components/ui/token-balance'
@ -13,16 +12,17 @@ export default class ConfirmAddSuggestedToken extends Component {
static propTypes = {
history: PropTypes.object,
addToken: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
pendingTokens: PropTypes.object,
removeSuggestedTokens: PropTypes.func,
tokens: PropTypes.array,
}
componentDidMount () {
const { pendingTokens = {}, history } = this.props
const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props
if (Object.keys(pendingTokens).length === 0) {
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
}
}
@ -33,7 +33,7 @@ export default class ConfirmAddSuggestedToken extends Component {
}
render () {
const { addToken, pendingTokens, tokens, removeSuggestedTokens, history } = this.props
const { addToken, pendingTokens, tokens, removeSuggestedTokens, history, mostRecentOverviewPage } = this.props
const pendingTokenKey = Object.keys(pendingTokens)[0]
const pendingToken = pendingTokens[pendingTokenKey]
const hasTokenDuplicates = this.checkTokenDuplicates(pendingTokens, tokens)
@ -113,7 +113,7 @@ export default class ConfirmAddSuggestedToken extends Component {
className="page-container__footer-button"
onClick={() => {
removeSuggestedTokens()
.then(() => history.push(DEFAULT_ROUTE))
.then(() => history.push(mostRecentOverviewPage))
}}
>
{ this.context.t('cancel') }
@ -125,7 +125,7 @@ export default class ConfirmAddSuggestedToken extends Component {
onClick={() => {
addToken(pendingToken)
.then(() => removeSuggestedTokens())
.then(() => history.push(DEFAULT_ROUTE))
.then(() => history.push(mostRecentOverviewPage))
}}
>
{ this.context.t('addToken') }

@ -3,12 +3,14 @@ import { compose } from 'redux'
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
import { withRouter } from 'react-router-dom'
import { addToken, removeSuggestedTokens } from '../../store/actions'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
const mapStateToProps = ({ metamask }) => {
const { pendingTokens, suggestedTokens, tokens } = metamask
const mapStateToProps = (state) => {
const { metamask: { pendingTokens, suggestedTokens, tokens } } = state
const params = { ...pendingTokens, ...suggestedTokens }
return {
mostRecentOverviewPage: getMostRecentOverviewPage(state),
pendingTokens: params,
tokens,
}

@ -1,6 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
import { ASSET_ROUTE, ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
import Button from '../../components/ui/button'
import Identicon from '../../components/ui/identicon'
import TokenBalance from '../../components/ui/token-balance'
@ -14,14 +14,15 @@ export default class ConfirmAddToken extends Component {
history: PropTypes.object,
clearPendingTokens: PropTypes.func,
addTokens: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
pendingTokens: PropTypes.object,
}
componentDidMount () {
const { pendingTokens = {}, history } = this.props
const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props
if (Object.keys(pendingTokens).length === 0) {
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
}
}
@ -32,7 +33,7 @@ export default class ConfirmAddToken extends Component {
}
render () {
const { history, addTokens, clearPendingTokens, pendingTokens } = this.props
const { history, addTokens, clearPendingTokens, mostRecentOverviewPage, pendingTokens } = this.props
return (
<div className="page-container">
@ -103,7 +104,12 @@ export default class ConfirmAddToken extends Component {
addTokens(pendingTokens)
.then(() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
const firstTokenAddress = Object.values(pendingTokens)?.[0].address?.toLowerCase()
if (firstTokenAddress) {
history.push(`${ASSET_ROUTE}/${firstTokenAddress}`)
} else {
history.push(mostRecentOverviewPage)
}
})
}}
>

@ -2,10 +2,12 @@ import { connect } from 'react-redux'
import ConfirmAddToken from './confirm-add-token.component'
import { addTokens, clearPendingTokens } from '../../store/actions'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
const mapStateToProps = ({ metamask }) => {
const { pendingTokens } = metamask
const mapStateToProps = (state) => {
const { metamask: { pendingTokens } } = state
return {
mostRecentOverviewPage: getMostRecentOverviewPage(state),
pendingTokens,
}
}

@ -11,7 +11,6 @@ import Tooltip from '../../components/ui/tooltip-v2'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import { conversionUtil } from '../../helpers/utils/conversion-util'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
export default class ConfirmDecryptMessage extends Component {
static contextTypes = {
@ -31,6 +30,7 @@ export default class ConfirmDecryptMessage extends Component {
decryptMessageInline: PropTypes.func.isRequired,
conversionRate: PropTypes.number,
history: PropTypes.object.isRequired,
mostRecentOverviewPage: PropTypes.string.isRequired,
requesterAddress: PropTypes.string,
txData: PropTypes.object,
domainMetadata: PropTypes.object,
@ -278,7 +278,14 @@ export default class ConfirmDecryptMessage extends Component {
}
renderFooter = () => {
const { txData } = this.props
const {
cancelDecryptMessage,
clearConfirmTransaction,
decryptMessage,
history,
mostRecentOverviewPage,
txData,
} = this.props
return (
<div className="request-decrypt-message__footer">
@ -288,7 +295,7 @@ export default class ConfirmDecryptMessage extends Component {
className="request-decrypt-message__footer__cancel-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.cancelDecryptMessage(txData, event)
await cancelDecryptMessage(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
@ -296,8 +303,8 @@ export default class ConfirmDecryptMessage extends Component {
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('cancel') }
@ -308,7 +315,7 @@ export default class ConfirmDecryptMessage extends Component {
className="request-decrypt-message__footer__sign-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.decryptMessage(txData, event)
await decryptMessage(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
@ -316,8 +323,8 @@ export default class ConfirmDecryptMessage extends Component {
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('decrypt') }

@ -14,6 +14,7 @@ import {
} from '../../selectors'
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
import ConfirmDecryptMessage from './confirm-decrypt-message.component'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
function mapStateToProps (state) {
const { confirmTransaction,
@ -35,6 +36,7 @@ function mapStateToProps (state) {
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state),
}
}

@ -8,7 +8,6 @@ import Identicon from '../../components/ui/identicon'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import { conversionUtil } from '../../helpers/utils/conversion-util'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
export default class ConfirmEncryptionPublicKey extends Component {
static contextTypes = {
@ -30,6 +29,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
requesterAddress: PropTypes.string,
txData: PropTypes.object,
domainMetadata: PropTypes.object,
mostRecentOverviewPage: PropTypes.string.isRequired,
}
state = {
@ -183,7 +183,14 @@ export default class ConfirmEncryptionPublicKey extends Component {
}
renderFooter = () => {
const { txData } = this.props
const {
cancelEncryptionPublicKey,
clearConfirmTransaction,
encryptionPublicKey,
history,
mostRecentOverviewPage,
txData,
} = this.props
return (
<div className="request-encryption-public-key__footer">
@ -193,7 +200,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
className="request-encryption-public-key__footer__cancel-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.cancelEncryptionPublicKey(txData, event)
await cancelEncryptionPublicKey(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
@ -201,8 +208,8 @@ export default class ConfirmEncryptionPublicKey extends Component {
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('cancel') }
@ -213,7 +220,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
className="request-encryption-public-key__footer__sign-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.encryptionPublicKey(txData, event)
await encryptionPublicKey(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
@ -221,8 +228,8 @@ export default class ConfirmEncryptionPublicKey extends Component {
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('provide') }

@ -15,6 +15,7 @@ import {
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.component'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
function mapStateToProps (state) {
const { confirmTransaction,
@ -36,6 +37,7 @@ function mapStateToProps (state) {
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state),
}
}

@ -5,7 +5,7 @@ import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container'
import { isBalanceSufficient } from '../send/send.utils'
import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
import { CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
@ -96,6 +96,7 @@ export default class ConfirmTransactionBase extends Component {
tryReverseResolveAddress: PropTypes.func.isRequired,
hideSenderToRecipient: PropTypes.bool,
showAccountInHeader: PropTypes.bool,
mostRecentOverviewPage: PropTypes.string.isRequired,
}
state = {
@ -110,6 +111,7 @@ export default class ConfirmTransactionBase extends Component {
showTransactionConfirmedModal,
history,
clearConfirmTransaction,
mostRecentOverviewPage,
nextNonce,
customNonceValue,
toAddress,
@ -136,7 +138,7 @@ export default class ConfirmTransactionBase extends Component {
showTransactionConfirmedModal({
onSubmit: () => {
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
},
})
}
@ -383,6 +385,7 @@ export default class ConfirmTransactionBase extends Component {
cancelAllTransactions,
clearConfirmTransaction,
history,
mostRecentOverviewPage,
showRejectTransactionsConfirmationModal,
unapprovedTxCount,
} = this.props
@ -393,7 +396,7 @@ export default class ConfirmTransactionBase extends Component {
this._removeBeforeUnload()
await cancelAllTransactions()
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
},
})
}
@ -405,6 +408,7 @@ export default class ConfirmTransactionBase extends Component {
txData,
cancelTransaction,
history,
mostRecentOverviewPage,
clearConfirmTransaction,
actionKey,
txData: { origin },
@ -432,7 +436,7 @@ export default class ConfirmTransactionBase extends Component {
cancelTransaction(txData)
.then(() => {
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
})
}
}
@ -447,6 +451,7 @@ export default class ConfirmTransactionBase extends Component {
history,
onSubmit,
actionKey,
mostRecentOverviewPage,
metaMetricsSendCount = 0,
setMetaMetricsSendCount,
methodData = {},
@ -493,7 +498,7 @@ export default class ConfirmTransactionBase extends Component {
this.setState({
submitting: false,
}, () => {
history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
updateCustomNonce('')
})
})

@ -37,6 +37,7 @@ import {
getPreferences,
transactionFeeSelector,
} from '../../selectors'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
@ -178,6 +179,7 @@ const mapStateToProps = (state, ownProps) => {
metaMetricsSendCount,
transactionCategory,
nextNonce,
mostRecentOverviewPage: getMostRecentOverviewPage(state),
}
}

@ -10,7 +10,7 @@ import R from 'ramda'
import SignatureRequest from '../../components/app/signature-request'
import SignatureRequestOriginal from '../../components/app/signature-request-original'
import Loading from '../../components/ui/loading-screen'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
function mapStateToProps (state) {
const { metamask, appState } = state
@ -25,6 +25,7 @@ function mapStateToProps (state) {
return {
identities: state.metamask.identities,
mostRecentOverviewPage: getMostRecentOverviewPage(state),
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
@ -45,6 +46,7 @@ function mapStateToProps (state) {
class ConfirmTxScreen extends Component {
static propTypes = {
mostRecentOverviewPage: PropTypes.string.isRequired,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
@ -167,13 +169,15 @@ class ConfirmTxScreen extends Component {
componentDidMount () {
const {
unapprovedTxs = {},
history,
mostRecentOverviewPage,
network,
send,
} = this.props
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
this.props.history.push(DEFAULT_ROUTE)
history.push(mostRecentOverviewPage)
}
}
@ -185,6 +189,7 @@ class ConfirmTxScreen extends Component {
send,
history,
match: { params: { id: transactionId } = {} },
mostRecentOverviewPage,
} = this.props
let prevTx
@ -203,14 +208,14 @@ class ConfirmTxScreen extends Component {
if (prevTx && prevTx.status === 'dropped') {
this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
onSubmit: () => history.push(DEFAULT_ROUTE),
onSubmit: () => history.push(mostRecentOverviewPage),
}))
return
}
if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
this.props.history.push(DEFAULT_ROUTE)
this.props.history.push(mostRecentOverviewPage)
}
}

@ -14,7 +14,6 @@ import ConfirmDecryptMessage from '../confirm-decrypt-message'
import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key'
import {
DEFAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
CONFIRM_DEPLOY_CONTRACT_PATH,
CONFIRM_SEND_ETHER_PATH,
@ -39,6 +38,7 @@ export default class ConfirmTransaction extends Component {
setTransactionToConfirm: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
fetchBasicGasAndTimeEstimates: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
transaction: PropTypes.object,
getContractMethodData: PropTypes.func,
transactionId: PropTypes.string,
@ -52,6 +52,7 @@ export default class ConfirmTransaction extends Component {
totalUnapprovedCount = 0,
send = {},
history,
mostRecentOverviewPage,
transaction: { txParams: { data, to } = {} } = {},
fetchBasicGasAndTimeEstimates,
getContractMethodData,
@ -62,7 +63,7 @@ export default class ConfirmTransaction extends Component {
} = this.props
if (!totalUnapprovedCount && !send.to) {
history.replace(DEFAULT_ROUTE)
history.replace(mostRecentOverviewPage)
return
}
@ -86,6 +87,7 @@ export default class ConfirmTransaction extends Component {
paramsTransactionId,
transactionId,
history,
mostRecentOverviewPage,
totalUnapprovedCount,
} = this.props
@ -95,10 +97,10 @@ export default class ConfirmTransaction extends Component {
setTransactionToConfirm(paramsTransactionId)
return
} else if (prevProps.transactionId && !transactionId && !totalUnapprovedCount) {
history.replace(DEFAULT_ROUTE)
history.replace(mostRecentOverviewPage)
return
} else if (prevProps.transactionId && transactionId && prevProps.transactionId !== transactionId) {
history.replace(DEFAULT_ROUTE)
history.replace(mostRecentOverviewPage)
return
}
}

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

Loading…
Cancel
Save