aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app
diff options
context:
space:
mode:
authorKevin Serrano <kevgagser@gmail.com>2018-10-15 08:32:29 +0800
committerGitHub <noreply@github.com>2018-10-15 08:32:29 +0800
commit85884b21afe6e5e85b58123b24e72776d1437cc6 (patch)
tree6dbec947089691c0dffd5febbe6e92c1f712e40f /ui/app
parentdb981b827b07c946e17d3a8aeaefd2143c236ef7 (diff)
parent7f6b488c04e6ea12b2ef24ac936b6a236ad904c2 (diff)
downloadtangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.gz
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.bz2
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.lz
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.xz
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.zst
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.zip
Merge pull request #5512 from MetaMask/v4.14.0
Version 4.14.0
Diffstat (limited to 'ui/app')
-rw-r--r--ui/app/account-and-transaction-details.js33
-rw-r--r--ui/app/actions.js162
-rw-r--r--ui/app/app.js86
-rw-r--r--ui/app/components/app-header/app-header.component.js23
-rw-r--r--ui/app/components/app-header/index.js3
-rw-r--r--ui/app/components/app-header/index.scss (renamed from ui/app/css/itcss/components/header.scss)51
-rw-r--r--ui/app/components/balance-component.js48
-rw-r--r--ui/app/components/button/button.component.js5
-rw-r--r--ui/app/components/buy-button-subview.js267
-rw-r--r--ui/app/components/card/card.component.js25
-rw-r--r--ui/app/components/card/index.js1
-rw-r--r--ui/app/components/card/index.scss11
-rw-r--r--ui/app/components/card/tests/card.component.test.js25
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js12
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js4
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.scss2
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js18
-rw-r--r--ui/app/components/currency-display/currency-display.component.js31
-rw-r--r--ui/app/components/currency-display/currency-display.container.js21
-rw-r--r--ui/app/components/currency-display/index.js1
-rw-r--r--ui/app/components/currency-display/tests/currency-display.component.test.js27
-rw-r--r--ui/app/components/currency-display/tests/currency-display.container.test.js105
-rw-r--r--ui/app/components/custom-radio-list.js60
-rw-r--r--ui/app/components/customize-gas-modal/index.js15
-rw-r--r--ui/app/components/dropdowns/account-details-dropdown.js109
-rw-r--r--ui/app/components/dropdowns/components/account-dropdowns.js2
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js22
-rw-r--r--ui/app/components/dropdowns/tests/dropdown.test.js37
-rw-r--r--ui/app/components/dropdowns/tests/menu.test.js87
-rw-r--r--ui/app/components/dropdowns/tests/network-dropdown-icon.test.js25
-rw-r--r--ui/app/components/dropdowns/tests/network-dropdown.test.js97
-rw-r--r--ui/app/components/dropdowns/token-menu-dropdown.js9
-rw-r--r--ui/app/components/error-message/error-message.component.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js)16
-rw-r--r--ui/app/components/error-message/index.js1
-rw-r--r--ui/app/components/error-message/index.scss (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss)10
-rw-r--r--ui/app/components/error-message/tests/error-message.component.test.js36
-rw-r--r--ui/app/components/hex-as-decimal-input.js161
-rw-r--r--ui/app/components/hex-to-decimal/hex-to-decimal.component.js21
-rw-r--r--ui/app/components/hex-to-decimal/index.js1
-rw-r--r--ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js26
-rw-r--r--ui/app/components/identicon.js65
-rw-r--r--ui/app/components/index.scss40
-rw-r--r--ui/app/components/input-number.js13
-rw-r--r--ui/app/components/menu-bar/index.js1
-rw-r--r--ui/app/components/menu-bar/index.scss23
-rw-r--r--ui/app/components/menu-bar/menu-bar.component.js63
-rw-r--r--ui/app/components/menu-bar/menu-bar.container.js26
-rw-r--r--ui/app/components/modal/index.js2
-rw-r--r--ui/app/components/modal/index.scss62
-rw-r--r--ui/app/components/modal/modal-content/index.js1
-rw-r--r--ui/app/components/modal/modal-content/index.scss19
-rw-r--r--ui/app/components/modal/modal-content/modal-content.component.js32
-rw-r--r--ui/app/components/modal/modal-content/tests/modal-content.component.test.js44
-rw-r--r--ui/app/components/modal/modal.component.js80
-rw-r--r--ui/app/components/modal/tests/modal.component.test.js103
-rw-r--r--ui/app/components/modals/account-details-modal.js13
-rw-r--r--ui/app/components/modals/account-modal-container.js4
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js29
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js1
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss17
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js27
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction.component.js68
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction.container.js62
-rw-r--r--ui/app/components/modals/cancel-transaction/index.js1
-rw-r--r--ui/app/components/modals/cancel-transaction/index.scss18
-rw-r--r--ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js56
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js78
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js12
-rw-r--r--ui/app/components/modals/confirm-remove-account/index.js3
-rw-r--r--ui/app/components/modals/confirm-remove-account/index.scss58
-rw-r--r--ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js48
-rw-r--r--ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js11
-rw-r--r--ui/app/components/modals/confirm-reset-account/index.js3
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js15
-rw-r--r--ui/app/components/modals/deposit-ether-modal.js7
-rw-r--r--ui/app/components/modals/export-private-key-modal.js86
-rw-r--r--ui/app/components/modals/hide-token-confirmation-modal.js5
-rw-r--r--ui/app/components/modals/index.scss109
-rw-r--r--ui/app/components/modals/modal.js68
-rw-r--r--ui/app/components/modals/notification/index.js2
-rw-r--r--ui/app/components/modals/notification/notification.component.js30
-rw-r--r--ui/app/components/modals/notification/notification.container.js38
-rw-r--r--ui/app/components/modals/reject-transactions/index.js1
-rw-r--r--ui/app/components/modals/reject-transactions/index.scss6
-rw-r--r--ui/app/components/modals/reject-transactions/reject-transactions.component.js45
-rw-r--r--ui/app/components/modals/reject-transactions/reject-transactions.container.js17
-rw-r--r--ui/app/components/modals/transaction-confirmed/index.js3
-rw-r--r--ui/app/components/modals/transaction-confirmed/index.scss22
-rw-r--r--ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js59
-rw-r--r--ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js4
-rw-r--r--ui/app/components/modals/transaction-details/index.js1
-rw-r--r--ui/app/components/modals/transaction-details/transaction-details.component.js54
-rw-r--r--ui/app/components/modals/transaction-details/transaction-details.container.js4
-rw-r--r--ui/app/components/modals/welcome-beta/index.js3
-rw-r--r--ui/app/components/modals/welcome-beta/welcome-beta.component.js23
-rw-r--r--ui/app/components/modals/welcome-beta/welcome-beta.container.js4
-rw-r--r--ui/app/components/page-container/index.scss31
-rw-r--r--ui/app/components/page-container/page-container-footer/page-container-footer.component.js46
-rw-r--r--ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js79
-rw-r--r--ui/app/components/page-container/page-container-header/page-container-header.component.js28
-rw-r--r--ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js82
-rw-r--r--ui/app/components/page-container/page-container.component.js84
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js84
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js2
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js122
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js29
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.component.js50
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js16
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js44
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js30
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js17
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js3
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js6
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js31
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js15
-rw-r--r--ui/app/components/pages/create-account/import-account/index.js2
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js21
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js19
-rw-r--r--ui/app/components/pages/create-account/new-account.js19
-rw-r--r--ui/app/components/pages/home.js239
-rw-r--r--ui/app/components/pages/home/home.component.js77
-rw-r--r--ui/app/components/pages/home/home.container.js30
-rw-r--r--ui/app/components/pages/home/index.js1
-rw-r--r--ui/app/components/pages/index.scss2
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js29
-rw-r--r--ui/app/components/pages/settings/index.js65
-rw-r--r--ui/app/components/pages/settings/index.scss80
-rw-r--r--ui/app/components/pages/settings/info-tab/index.js1
-rw-r--r--ui/app/components/pages/settings/info-tab/index.scss56
-rw-r--r--ui/app/components/pages/settings/info-tab/info-tab.component.js136
-rw-r--r--ui/app/components/pages/settings/info.js120
-rw-r--r--ui/app/components/pages/settings/settings-tab/index.js1
-rw-r--r--ui/app/components/pages/settings/settings-tab/index.scss51
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.component.js360
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.container.js59
-rw-r--r--ui/app/components/pages/settings/settings.component.js54
-rw-r--r--ui/app/components/pages/settings/settings.js365
-rw-r--r--ui/app/components/pages/unlock-page/index.scss1
-rw-r--r--ui/app/components/pending-msg-details.js56
-rw-r--r--ui/app/components/pending-msg.js73
-rw-r--r--ui/app/components/qr-code.js2
-rw-r--r--ui/app/components/selected-account/selected-account.component.js9
-rw-r--r--ui/app/components/selected-account/tests/selected-account-component.test.js16
-rw-r--r--ui/app/components/send/currency-display/tests/currency-display.test.js91
-rw-r--r--ui/app/components/send/send-content/send-content.component.js13
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js8
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js7
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.container.js2
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.utils.js6
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js2
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js2
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js6
-rw-r--r--ui/app/components/send/send-content/tests/send-content-component.test.js14
-rw-r--r--ui/app/components/send/send-footer/send-footer.component.js4
-rw-r--r--ui/app/components/send/send.component.js6
-rw-r--r--ui/app/components/send/send.container.js5
-rw-r--r--ui/app/components/send/send.selectors.js5
-rw-r--r--ui/app/components/send/send.utils.js32
-rw-r--r--ui/app/components/send/tests/send-component.test.js6
-rw-r--r--ui/app/components/send/tests/send-container.test.js7
-rw-r--r--ui/app/components/send/tests/send-selectors-test-data.js2
-rw-r--r--ui/app/components/send/tests/send-selectors.test.js10
-rw-r--r--ui/app/components/send/tests/send-utils.test.js25
-rw-r--r--ui/app/components/send/to-autocomplete/to-autocomplete.js3
-rw-r--r--ui/app/components/sender-to-recipient/index.scss146
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js152
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.constants.js3
-rw-r--r--ui/app/components/shapeshift-form.js8
-rw-r--r--ui/app/components/shift-list-item.js3
-rw-r--r--ui/app/components/sidebars/index.js1
-rw-r--r--ui/app/components/sidebars/index.scss74
-rw-r--r--ui/app/components/sidebars/sidebar.component.js49
-rw-r--r--ui/app/components/sidebars/sidebar.constants.js1
-rw-r--r--ui/app/components/sidebars/tests/sidebars-component.test.js88
-rw-r--r--ui/app/components/signature-request.js42
-rw-r--r--ui/app/components/tabs/tab/tab.component.js13
-rw-r--r--ui/app/components/token-balance.js120
-rw-r--r--ui/app/components/token-balance/index.js1
-rw-r--r--ui/app/components/token-balance/token-balance.component.js23
-rw-r--r--ui/app/components/token-balance/token-balance.container.js (renamed from ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js)4
-rw-r--r--ui/app/components/token-cell.js5
-rw-r--r--ui/app/components/token-currency-display/index.js1
-rw-r--r--ui/app/components/token-currency-display/token-currency-display.component.js54
-rw-r--r--ui/app/components/token-list.js9
-rw-r--r--ui/app/components/tooltip-v2.js87
-rw-r--r--ui/app/components/transaction-action/index.js1
-rw-r--r--ui/app/components/transaction-action/tests/transaction-action.component.test.js162
-rw-r--r--ui/app/components/transaction-action/transaction-action.component.js58
-rw-r--r--ui/app/components/transaction-activity-log/index.js1
-rw-r--r--ui/app/components/transaction-activity-log/index.scss70
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js35
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js27
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js208
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.component.js91
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.container.js11
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.util.js82
-rw-r--r--ui/app/components/transaction-breakdown/index.js1
-rw-r--r--ui/app/components/transaction-breakdown/index.scss23
-rw-r--r--ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js37
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js1
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss19
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js39
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js26
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown.component.js82
-rw-r--r--ui/app/components/transaction-list-item-details/index.js1
-rw-r--r--ui/app/components/transaction-list-item-details/index.scss49
-rw-r--r--ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js66
-rw-r--r--ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js108
-rw-r--r--ui/app/components/transaction-list-item/index.js1
-rw-r--r--ui/app/components/transaction-list-item/index.scss131
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js200
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js48
-rw-r--r--ui/app/components/transaction-list/index.js1
-rw-r--r--ui/app/components/transaction-list/index.scss49
-rw-r--r--ui/app/components/transaction-list/transaction-list.component.js119
-rw-r--r--ui/app/components/transaction-list/transaction-list.container.js51
-rw-r--r--ui/app/components/transaction-status/index.js1
-rw-r--r--ui/app/components/transaction-status/index.scss28
-rw-r--r--ui/app/components/transaction-status/transaction-status.component.js59
-rw-r--r--ui/app/components/transaction-view-balance/index.js1
-rw-r--r--ui/app/components/transaction-view-balance/index.scss76
-rw-r--r--ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js71
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.component.js96
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.container.js31
-rw-r--r--ui/app/components/transaction-view/index.js1
-rw-r--r--ui/app/components/transaction-view/index.scss27
-rw-r--r--ui/app/components/transaction-view/transaction-view.component.js27
-rw-r--r--ui/app/components/tx-list-item.js356
-rw-r--r--ui/app/components/tx-list.js171
-rw-r--r--ui/app/components/tx-view.js156
-rw-r--r--ui/app/components/wallet-view.js17
-rw-r--r--ui/app/conf-tx.js2
-rw-r--r--ui/app/constants/common.js3
-rw-r--r--ui/app/constants/transactions.js23
-rw-r--r--ui/app/conversion-util.js3
-rw-r--r--ui/app/css/itcss/components/account-details-dropdown.scss7
-rw-r--r--ui/app/css/itcss/components/buttons.scss15
-rw-r--r--ui/app/css/itcss/components/hero-balance.scss130
-rw-r--r--ui/app/css/itcss/components/index.scss8
-rw-r--r--ui/app/css/itcss/components/loading-overlay.scss20
-rw-r--r--ui/app/css/itcss/components/network.scss9
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss53
-rw-r--r--ui/app/css/itcss/components/request-signature.scss19
-rw-r--r--ui/app/css/itcss/components/send.scss25
-rw-r--r--ui/app/css/itcss/components/settings.scss214
-rw-r--r--ui/app/css/itcss/components/transaction-list.scss2
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js50
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js70
-rw-r--r--ui/app/helpers/common.util.js5
-rw-r--r--ui/app/helpers/confirm-transaction/util.js27
-rw-r--r--ui/app/helpers/confirm-transaction/util.test.js6
-rw-r--r--ui/app/helpers/conversions.util.js63
-rw-r--r--ui/app/helpers/tests/common.util.test.js27
-rw-r--r--ui/app/helpers/tests/transactions.util.test.js22
-rw-r--r--ui/app/helpers/transactions.util.js128
-rw-r--r--ui/app/higher-order-components/with-method-data/index.js1
-rw-r--r--ui/app/higher-order-components/with-method-data/with-method-data.component.js52
-rw-r--r--ui/app/higher-order-components/with-modal-props/index.js1
-rw-r--r--ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js43
-rw-r--r--ui/app/higher-order-components/with-modal-props/with-modal-props.js21
-rw-r--r--ui/app/higher-order-components/with-token-tracker/index.js1
-rw-r--r--ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js (renamed from ui/app/helpers/with-token-tracker.js)4
-rw-r--r--ui/app/i18n-provider.js11
-rw-r--r--ui/app/main-container.js49
-rw-r--r--ui/app/new-keychain.js29
-rw-r--r--ui/app/reducers/app.js25
-rw-r--r--ui/app/routes.js2
-rw-r--r--ui/app/selectors.js35
-rw-r--r--ui/app/selectors/confirm-transaction.js7
-rw-r--r--ui/app/selectors/tokens.js11
-rw-r--r--ui/app/selectors/transactions.js58
-rw-r--r--ui/app/token-util.js117
-rw-r--r--ui/app/util.js2
277 files changed, 7725 insertions, 3865 deletions
diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js
deleted file mode 100644
index 03101d37a..000000000
--- a/ui/app/account-and-transaction-details.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-// Main Views
-const TxView = require('./components/tx-view')
-const WalletView = require('./components/wallet-view')
-
-module.exports = AccountAndTransactionDetails
-
-inherits(AccountAndTransactionDetails, Component)
-function AccountAndTransactionDetails () {
- Component.call(this)
-}
-
-AccountAndTransactionDetails.prototype.render = function () {
- return h('div.account-and-transaction-details', [
- // wallet
- h(WalletView, {
- style: {
- },
- responsiveDisplayClassname: '.lap-visible',
- }, [
- ]),
-
- // transaction
- h(TxView, {
- style: {
- },
- }, [
- ]),
- ])
-}
-
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 6bcc64e17..eea581d33 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -167,6 +167,7 @@ var actions = {
updateTransaction,
updateAndApproveTx,
cancelTx: cancelTx,
+ cancelTxs,
completedTx: completedTx,
txError: txError,
nextTx: nextTx,
@@ -227,13 +228,17 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
+ SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
showAddTokenPage,
+ showAddSuggestedTokenPage,
addToken,
addTokens,
removeToken,
updateTokens,
+ removeSuggestedTokens,
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
+ delRpcTarget: delRpcTarget,
setProviderType: setProviderType,
SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
setHardwareWalletDefaultHdPath,
@@ -312,6 +317,8 @@ var actions = {
CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
setPendingTokens,
clearPendingTokens,
+
+ createCancelTransaction,
}
module.exports = actions
@@ -410,12 +417,18 @@ function createNewVaultAndRestore (password, seed) {
log.debug(`background.createNewVaultAndRestore`)
return new Promise((resolve, reject) => {
- background.createNewVaultAndRestore(password, seed, err => {
+ background.clearSeedWordCache((err) => {
if (err) {
return reject(err)
}
- resolve()
+ background.createNewVaultAndRestore(password, seed, (err) => {
+ if (err) {
+ return reject(err)
+ }
+
+ resolve()
+ })
})
})
.then(() => dispatch(actions.unMarkPasswordForgotten()))
@@ -905,6 +918,7 @@ function updateGasData ({
selectedToken,
to,
value,
+ data,
}) {
return (dispatch) => {
dispatch(actions.gasLoadingStarted())
@@ -925,6 +939,7 @@ function updateGasData ({
to,
value,
estimateGasPrice,
+ data,
}),
])
})
@@ -1147,6 +1162,10 @@ function updateAndApproveTx (txData) {
return txData
})
+ .catch((err) => {
+ dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
+ })
}
}
@@ -1284,6 +1303,47 @@ function cancelTx (txData) {
}
}
+/**
+ * Cancels all of the given transactions
+ * @param {Array<object>} txDataList a list of tx data objects
+ * @return {function(*): Promise<void>}
+ */
+function cancelTxs (txDataList) {
+ return async (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ const txIds = txDataList.map(({id}) => id)
+ const cancellations = txIds.map((id) => new Promise((resolve, reject) => {
+ background.cancelTransaction(id, (err) => {
+ if (err) {
+ return reject(err)
+ }
+
+ resolve()
+ })
+ }))
+
+ await Promise.all(cancellations)
+ const newState = await updateMetamaskStateFromBackground()
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.clearSend())
+
+ txIds.forEach((id) => {
+ dispatch(actions.completedTx(id))
+ })
+
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
+ return global.platform.closeCurrentWindow()
+ }
+ }
+}
+
+/**
+ * @deprecated
+ * @param {Array<object>} txsData
+ * @return {Function}
+ */
function cancelAllTx (txsData) {
return (dispatch) => {
txsData.forEach((txData, i) => {
@@ -1589,11 +1649,18 @@ function showAddTokenPage (transitionForward = true) {
}
}
-function addToken (address, symbol, decimals) {
+function showAddSuggestedTokenPage (transitionForward = true) {
+ return {
+ type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
+ value: transitionForward,
+ }
+}
+
+function addToken (address, symbol, decimals, image) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.addToken(address, symbol, decimals, (err, tokens) => {
+ background.addToken(address, symbol, decimals, image, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
@@ -1643,6 +1710,27 @@ function addTokens (tokens) {
}
}
+function removeSuggestedTokens () {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.removeSuggestedTokens((err, suggestedTokens) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ }
+ dispatch(actions.clearPendingTokens())
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
+ return global.platform.closeCurrentWindow()
+ }
+ resolve(suggestedTokens)
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens})))
+ }
+}
+
function updateTokens (newTokens) {
return {
type: actions.UPDATE_TOKENS,
@@ -1650,6 +1738,12 @@ function updateTokens (newTokens) {
}
}
+function clearPendingTokens () {
+ return {
+ type: actions.CLEAR_PENDING_TOKENS,
+ }
+}
+
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
@@ -1668,7 +1762,7 @@ function markNoticeRead (notice) {
background.markNoticeRead(notice, (err, notice) => {
dispatch(actions.hideLoadingIndication())
if (err) {
- dispatch(actions.displayWarning(err))
+ dispatch(actions.displayWarning(err.message))
return reject(err)
}
@@ -1725,6 +1819,29 @@ function retryTransaction (txId) {
}
}
+function createCancelTransaction (txId, customGasPrice) {
+ log.debug('background.cancelTransaction')
+ let newTxId
+
+ return dispatch => {
+ return new Promise((resolve, reject) => {
+ background.createCancelTransaction(txId, customGasPrice, (err, newState) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+
+ const { selectedAddressTxList } = newState
+ const { id } = selectedAddressTxList[selectedAddressTxList.length - 1]
+ newTxId = id
+ resolve(newState)
+ })
+ })
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => newTxId)
+ }
+}
+
//
// config
//
@@ -1735,7 +1852,7 @@ function setProviderType (type) {
background.setProviderType(type, (err, result) => {
if (err) {
log.error(err)
- return dispatch(self.displayWarning('Had a problem changing networks!'))
+ return dispatch(actions.displayWarning('Had a problem changing networks!'))
}
dispatch(actions.updateProviderType(type))
dispatch(actions.setSelectedToken())
@@ -1757,7 +1874,20 @@ function setRpcTarget (newRpc) {
background.setCustomRpc(newRpc, (err, result) => {
if (err) {
log.error(err)
- return dispatch(self.displayWarning('Had a problem changing networks!'))
+ return dispatch(actions.displayWarning('Had a problem changing networks!'))
+ }
+ dispatch(actions.setSelectedToken())
+ })
+ }
+}
+
+function delRpcTarget (oldRpc) {
+ return (dispatch) => {
+ log.debug(`background.delRpcTarget: ${oldRpc}`)
+ background.delCustomRpc(oldRpc, (err, result) => {
+ if (err) {
+ log.error(err)
+ return dispatch(self.displayWarning('Had a problem removing network!'))
}
dispatch(actions.setSelectedToken())
})
@@ -1812,9 +1942,13 @@ function hideModal (payload) {
}
}
-function showSidebar () {
+function showSidebar ({ transitionName, type }) {
return {
type: actions.SIDEBAR_OPEN,
+ value: {
+ transitionName,
+ type,
+ },
}
}
@@ -2175,6 +2309,10 @@ function updateNetworkNonce (address) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.getTransactionCount(address, (err, data) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
dispatch(setNetworkNonce(data))
resolve(data)
})
@@ -2262,7 +2400,7 @@ function setUseBlockie (val) {
function updateCurrentLocale (key) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
- fetchLocale(key)
+ return fetchLocale(key)
.then((localeMessages) => {
log.debug(`background.setCurrentLocale`)
background.setCurrentLocale(key, (err) => {
@@ -2310,9 +2448,3 @@ function setPendingTokens (pendingTokens) {
payload: tokens,
}
}
-
-function clearPendingTokens () {
- return {
- type: actions.CLEAR_PENDING_TOKENS,
- }
-}
diff --git a/ui/app/app.js b/ui/app/app.js
index dbb6146d1..aeb3d05ee 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -15,22 +15,22 @@ const SendTransactionScreen = require('./components/send/send.container')
const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
-const WalletView = require('./components/wallet-view')
+const Sidebar = require('./components/sidebars').default
// other views
-const Home = require('./components/pages/home')
+import Home from './components/pages/home'
+import Settings from './components/pages/settings'
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
-const Settings = require('./components/pages/settings')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
+const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading-screen')
-const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
@@ -39,8 +39,7 @@ const Modal = require('./components/modals/index').Modal
// Global Alert
const Alert = require('./components/alert')
-const AppHeader = require('./components/app-header')
-
+import AppHeader from './components/app-header'
import UnlockPage from './components/pages/unlock-page'
// Routes
@@ -52,6 +51,7 @@ const {
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@@ -86,6 +86,7 @@ class App extends Component {
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
+ h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
@@ -103,6 +104,7 @@ class App extends Component {
frequentRpcList,
currentView,
setMouseUserState,
+ sidebar,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
@@ -135,7 +137,12 @@ class App extends Component {
h(AppHeader),
// sidebar
- this.renderSidebar(),
+ h(Sidebar, {
+ sidebarOpen: sidebar.isOpen,
+ hideSidebar: this.props.hideSidebar,
+ transitionName: sidebar.transitionName,
+ type: sidebar.type,
+ }),
// network dropdown
h(NetworkDropdown, {
@@ -145,61 +152,18 @@ class App extends Component {
h(AccountMenu),
- (isLoading || isLoadingNetwork) && h(Loading, {
- loadingMessage: loadMessage,
- }),
+ h('div.main-container-wrapper', [
+ (isLoading || isLoadingNetwork) && h(Loading, {
+ loadingMessage: loadMessage,
+ }),
- // content
- this.renderRoutes(),
+ // content
+ this.renderRoutes(),
+ ]),
])
)
}
- renderSidebar () {
- return h('div', [
- h('style', `
- .sidebar-enter {
- transition: transform 300ms ease-in-out;
- transform: translateX(-100%);
- }
- .sidebar-enter.sidebar-enter-active {
- transition: transform 300ms ease-in-out;
- transform: translateX(0%);
- }
- .sidebar-leave {
- transition: transform 200ms ease-out;
- transform: translateX(0%);
- }
- .sidebar-leave.sidebar-leave-active {
- transition: transform 200ms ease-out;
- transform: translateX(-100%);
- }
- `),
-
- h(ReactCSSTransitionGroup, {
- transitionName: 'sidebar',
- transitionEnterTimeout: 300,
- transitionLeaveTimeout: 200,
- }, [
- // A second instance of Walletview is used for non-mobile viewports
- this.props.sidebarOpen ? h(WalletView, {
- responsiveDisplayClassname: '.sidebar',
- style: {},
- }) : undefined,
-
- ]),
-
- // overlay
- // TODO: add onClick for overlay to close sidebar
- this.props.sidebarOpen ? h('div.sidebar-overlay', {
- style: {},
- onClick: () => {
- this.props.hideSidebar()
- },
- }, []) : undefined,
- ])
- }
-
toggleMetamaskActive () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
@@ -226,7 +190,7 @@ class App extends Component {
} else if (providerName === 'ropsten') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'kovan') {
- name = this.context.t('connectingToRopsten')
+ name = this.context.t('connectingToKovan')
} else if (providerName === 'rinkeby') {
name = this.context.t('connectingToRinkeby')
} else {
@@ -268,7 +232,7 @@ App.propTypes = {
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
- sidebarOpen: PropTypes.bool,
+ sidebar: PropTypes.object,
alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
@@ -304,7 +268,7 @@ function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
- sidebarOpen,
+ sidebar,
alertOpen,
alertMessage,
isLoading,
@@ -331,7 +295,7 @@ function mapStateToProps (state) {
return {
// state from plugin
networkDropdownOpen,
- sidebarOpen,
+ sidebar,
alertOpen,
alertMessage,
isLoading,
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
index 07ca6cf84..b8b002dcc 100644
--- a/ui/app/components/app-header/app-header.component.js
+++ b/ui/app/components/app-header/app-header.component.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { matchPath } from 'react-router-dom'
@@ -11,7 +11,7 @@ const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('
const Identicon = require('../identicon')
const NetworkIndicator = require('../network')
-class AppHeader extends Component {
+export default class AppHeader extends PureComponent {
static propTypes = {
history: PropTypes.object,
location: PropTypes.object,
@@ -107,20 +107,19 @@ class AppHeader extends Component {
onClick={() => history.push(DEFAULT_ROUTE)}
>
<img
- className="app-header__metafox"
- src="/images/metamask-fox.svg"
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal-beta.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
- <div className="flex-row">
- <h1>{ this.context.t('appName') }</h1>
- <div className="app-header__beta-label">
- { this.context.t('beta') }
- </div>
- </div>
</div>
<div className="app-header__account-menu-container">
- <div className="network-component-wrapper">
+ <div className="app-header__network-component-wrapper">
<NetworkIndicator
network={network}
provider={provider}
@@ -135,5 +134,3 @@ class AppHeader extends Component {
)
}
}
-
-export default AppHeader
diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js
index daa31f621..6de2f9c78 100644
--- a/ui/app/components/app-header/index.js
+++ b/ui/app/components/app-header/index.js
@@ -1,2 +1 @@
-import AppHeader from './app-header.container'
-module.exports = AppHeader
+export { default } from './app-header.container'
diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/components/app-header/index.scss
index 3ccfd5c15..325844af5 100644
--- a/ui/app/css/itcss/components/header.scss
+++ b/ui/app/components/app-header/index.scss
@@ -30,21 +30,19 @@
}
}
- &__metafox {
+ &__metafox-logo {
cursor: pointer;
- }
- &__beta-label {
- font-family: Roboto;
- text-transform: uppercase;
- font-weight: 500;
- font-size: .8rem;
- color: $buttercup;
- margin-left: 5px;
- line-height: initial;
+ &--icon {
+ @media screen and (min-width: $break-large) {
+ display: none;
+ }
+ }
- @media screen and (max-width: 575px) {
- display: none;
+ &--horizontal {
+ @media screen and (max-width: $break-small) {
+ display: none;
+ }
}
}
@@ -83,31 +81,10 @@
flex-flow: row nowrap;
align-items: center;
}
-}
-
-.app-header h1 {
- font-family: Roboto;
- text-transform: uppercase;
- font-weight: 400;
- font-size: 1.1rem;
- position: relative;
- padding-left: 15px;
- color: #5b5d67;
- @media screen and (max-width: 575px) {
- display: none;
+ &__network-component-wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
}
}
-
-h2.page-subtitle {
- text-transform: uppercase;
- color: #aeaeae;
- font-size: 1em;
- margin: 12px;
-}
-
-.network-component-wrapper {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js
index e31552f2d..d63d78c9f 100644
--- a/ui/app/components/balance-component.js
+++ b/ui/app/components/balance-component.js
@@ -4,8 +4,8 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
+import CurrencyDisplay from './currency-display'
+const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance, generateBalanceObject } = require('../util')
@@ -20,8 +20,9 @@ function mapStateToProps (state) {
return {
account,
network,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
+ conversionRate: conversionRateSelector(state),
+ currentCurrency: getCurrentCurrency(state),
+ assetImages: getAssetImages(state),
}
}
@@ -32,7 +33,9 @@ function BalanceComponent () {
BalanceComponent.prototype.render = function () {
const props = this.props
- const { token, network } = props
+ const { token, network, assetImages } = props
+ const address = token && token.address
+ const image = assetImages && address ? assetImages[token.address] : undefined
return h('div.balance-container', {}, [
@@ -43,8 +46,9 @@ BalanceComponent.prototype.render = function () {
// }),
h(Identicon, {
diameter: 50,
- address: token && token.address,
+ address,
network,
+ image,
}),
token ? this.renderTokenBalance() : this.renderBalance(),
@@ -80,38 +84,12 @@ BalanceComponent.prototype.renderBalance = function () {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
- showFiat ? this.renderFiatValue(formattedBalance) : null,
+ showFiat && h(CurrencyDisplay, {
+ value: balanceValue,
+ }),
])
}
-BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
-
- const { conversionRate, currentCurrency } = this.props
-
- const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
-
- const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
-
- return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
-}
-
-BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
- const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
- if (shouldNotRenderFiat) return null
-
- const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
-
- const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
- ? currencyFormatter.format(Number(fiatDisplayNumber), {
- code: upperCaseFiatSuffix,
- })
- : `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
-
- return h('div.fiat-amount', {
- style: {},
- }, display)
-}
-
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index 1e0ef1b64..4a06333e7 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -6,6 +6,7 @@ const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_CONFIRM = 'btn-confirm'
+const CLASSNAME_RAISED = 'btn-raised'
const CLASSNAME_LARGE = 'btn--large'
const typeHash = {
@@ -13,6 +14,7 @@ const typeHash = {
primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY,
confirm: CLASSNAME_CONFIRM,
+ raised: CLASSNAME_RAISED,
}
export default class Button extends Component {
@@ -20,7 +22,7 @@ export default class Button extends Component {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
- children: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}
render () {
@@ -29,6 +31,7 @@ export default class Button extends Component {
return (
<button
className={classnames(
+ 'button',
typeHash[type],
large && CLASSNAME_LARGE,
className
diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js
deleted file mode 100644
index c6957d2aa..000000000
--- a/ui/app/components/buy-button-subview.js
+++ /dev/null
@@ -1,267 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const actions = require('../actions')
-const CoinbaseForm = require('./coinbase-form')
-const ShapeshiftForm = require('./shapeshift-form')
-const Loading = require('./loading-screen')
-const AccountPanel = require('./account-panel')
-const RadioList = require('./custom-radio-list')
-const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
-
-BuyButtonSubview.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(BuyButtonSubview)
-
-
-function mapStateToProps (state) {
- return {
- identity: state.appState.identity,
- account: state.metamask.accounts[state.appState.buyView.buyAddress],
- warning: state.appState.warning,
- buyView: state.appState.buyView,
- network: state.metamask.network,
- provider: state.metamask.provider,
- context: state.appState.currentView.context,
- isSubLoading: state.appState.isSubLoading,
- }
-}
-
-inherits(BuyButtonSubview, Component)
-function BuyButtonSubview () {
- Component.call(this)
-}
-
-BuyButtonSubview.prototype.render = function () {
- return (
- h('div', {
- style: {
- width: '100%',
- },
- }, [
- this.headerSubview(),
- this.primarySubview(),
- ])
- )
-}
-
-BuyButtonSubview.prototype.headerSubview = function () {
- const props = this.props
- const isLoading = props.isSubLoading
- return (
-
- h('.flex-column', {
- style: {
- alignItems: 'center',
- },
- }, [
-
- // header bar (back button, label)
- h('.flex-row', {
- style: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- }, [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: this.backButtonContext.bind(this),
- style: {
- position: 'absolute',
- left: '10px',
- },
- }),
- h('h2.text-transform-uppercase.flex-center', {
- style: {
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, this.context.t('depositEth')),
- ]),
-
- // loading indication
- h('div', {
- style: {
- position: 'absolute',
- top: '57vh',
- left: '49vw',
- },
- }, [
- isLoading && h(Loading),
- ]),
-
- // account panel
- h('div', {
- style: {
- width: '80%',
- },
- }, [
- h(AccountPanel, {
- showFullAddress: true,
- identity: props.identity,
- account: props.account,
- }),
- ]),
-
- h('.flex-row', {
- style: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- }, [
- h('h3.text-transform-uppercase.flex-center', {
- style: {
- paddingLeft: '15px',
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, this.context.t('selectService')),
- ]),
-
- ])
-
- )
-}
-
-
-BuyButtonSubview.prototype.primarySubview = function () {
- const props = this.props
- const network = props.network
-
- switch (network) {
- case 'loading':
- return
-
- case '1':
- return this.mainnetSubview()
-
- // Ropsten, Rinkeby, Kovan
- case '3':
- case '4':
- case '42':
- const networkName = getNetworkDisplayName(network)
- const label = `${networkName} ${this.context.t('testFaucet')}`
- return (
- h('div.flex-column', {
- style: {
- alignItems: 'center',
- margin: '20px 50px',
- },
- }, [
- h('button.text-transform-uppercase', {
- onClick: () => this.props.dispatch(actions.buyEth({ network })),
- style: {
- marginTop: '15px',
- },
- }, label),
- // Kovan only: Dharma loans beta
- network === '42' ? (
- h('button.text-transform-uppercase', {
- onClick: () => this.navigateTo('https://borrow.dharma.io/'),
- style: {
- marginTop: '15px',
- },
- }, this.context.t('borrowDharma'))
- ) : null,
- ])
- )
-
- default:
- return (
- h('h2.error', this.context.t('unknownNetworkId'))
- )
-
- }
-}
-
-BuyButtonSubview.prototype.mainnetSubview = function () {
- const props = this.props
-
- return (
-
- h('.flex-column', {
- style: {
- alignItems: 'center',
- },
- }, [
-
- h('.flex-row.selected-exchange', {
- style: {
- position: 'relative',
- right: '35px',
- marginTop: '20px',
- marginBottom: '20px',
- },
- }, [
- h(RadioList, {
- defaultFocus: props.buyView.subview,
- labels: [
- 'Coinbase',
- 'ShapeShift',
- ],
- subtext: {
- 'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
- 'ShapeShift': this.context.t('crypto'),
- },
- onClick: this.radioHandler.bind(this),
- }),
- ]),
-
- h('h3.text-transform-uppercase', {
- style: {
- paddingLeft: '15px',
- fontFamily: 'Montserrat Light',
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, props.buyView.subview),
-
- this.formVersionSubview(),
- ])
-
- )
-}
-
-BuyButtonSubview.prototype.formVersionSubview = function () {
- const network = this.props.network
- if (network === '1') {
- if (this.props.buyView.formView.coinbase) {
- return h(CoinbaseForm, this.props)
- } else if (this.props.buyView.formView.shapeshift) {
- return h(ShapeshiftForm, this.props)
- }
- }
-}
-
-BuyButtonSubview.prototype.navigateTo = function (url) {
- global.platform.openWindow({ url })
-}
-
-BuyButtonSubview.prototype.backButtonContext = function () {
- if (this.props.context === 'confTx') {
- this.props.dispatch(actions.showConfTxPage({transForward: false}))
- } else {
- this.props.dispatch(actions.goHome())
- }
-}
-
-BuyButtonSubview.prototype.radioHandler = function (event) {
- switch (event.target.title) {
- case 'Coinbase':
- return this.props.dispatch(actions.coinBaseSubview())
- case 'ShapeShift':
- return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
- }
-}
diff --git a/ui/app/components/card/card.component.js b/ui/app/components/card/card.component.js
new file mode 100644
index 000000000..bb7241da1
--- /dev/null
+++ b/ui/app/components/card/card.component.js
@@ -0,0 +1,25 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class Card extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ overrideClassName: PropTypes.bool,
+ title: PropTypes.string,
+ children: PropTypes.node,
+ }
+
+ render () {
+ const { className, overrideClassName, title } = this.props
+
+ return (
+ <div className={classnames({ 'card': !overrideClassName }, className)}>
+ <div className="card__title">
+ { title }
+ </div>
+ { this.props.children }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/card/index.js b/ui/app/components/card/index.js
new file mode 100644
index 000000000..c3ca6e3f4
--- /dev/null
+++ b/ui/app/components/card/index.js
@@ -0,0 +1 @@
+export { default } from './card.component'
diff --git a/ui/app/components/card/index.scss b/ui/app/components/card/index.scss
new file mode 100644
index 000000000..bde54a15e
--- /dev/null
+++ b/ui/app/components/card/index.scss
@@ -0,0 +1,11 @@
+.card {
+ border-radius: 4px;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
+ padding: 8px;
+
+ &__title {
+ border-bottom: 1px solid #d8d8d8;
+ padding-bottom: 4px;
+ text-transform: capitalize;
+ }
+}
diff --git a/ui/app/components/card/tests/card.component.test.js b/ui/app/components/card/tests/card.component.test.js
new file mode 100644
index 000000000..cea05033f
--- /dev/null
+++ b/ui/app/components/card/tests/card.component.test.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import Card from '../card.component'
+
+describe('Card Component', () => {
+ it('should render a card with a title and child element', () => {
+ const wrapper = shallow(
+ <Card
+ title="Test"
+ className="card-test-class"
+ >
+ <div className="child-test-class">Child</div>
+ </Card>
+ )
+
+ assert.ok(wrapper.hasClass('card-test-class'))
+ const title = wrapper.find('.card__title')
+ assert.ok(title)
+ assert.equal(title.text(), 'Test')
+ const child = wrapper.find('.child-test-class')
+ assert.ok(child)
+ assert.equal(child.text(), 'Child')
+ })
+})
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
index 08923af88..74e95ece6 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -2,11 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Tabs, Tab } from '../../tabs'
-import {
- ConfirmPageContainerSummary,
- ConfirmPageContainerError,
- ConfirmPageContainerWarning,
-} from './'
+import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './'
+import ErrorMessage from '../../error-message'
export default class ConfirmPageContainerContent extends Component {
static propTypes = {
@@ -18,6 +15,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
summaryComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -60,6 +58,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle,
identiconAddress,
nonce,
+ assetImage,
summaryComponent,
detailsComponent,
dataComponent,
@@ -85,6 +84,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress}
nonce={nonce}
+ assetImage={assetImage}
/>
)
}
@@ -92,7 +92,7 @@ export default class ConfirmPageContainerContent extends Component {
{
(errorKey || errorMessage) && (
<div className="confirm-page-container-content__error-container">
- <ConfirmPageContainerError
+ <ErrorMessage
errorMessage={errorMessage}
errorKey={errorKey}
/>
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
deleted file mode 100644
index 4ac95d0e3..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './confirm-page-container-error.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
index 3b1ee62c5..38b158fd3 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -4,7 +4,7 @@ import classnames from 'classnames'
import Identicon from '../../../identicon'
const ConfirmPageContainerSummary = props => {
- const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
+ const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props
return (
<div className={classnames('confirm-page-container-summary', className)}>
@@ -27,6 +27,7 @@ const ConfirmPageContainerSummary = props => {
className="confirm-page-container-summary__identicon"
diameter={36}
address={identiconAddress}
+ image={assetImage}
/>
)
}
@@ -51,6 +52,7 @@ ConfirmPageContainerSummary.propTypes = {
className: PropTypes.string,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
}
export default ConfirmPageContainerSummary
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
index 1469dd438..4dfd89d92 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
@@ -1,4 +1,3 @@
export { default } from './confirm-page-container-content.component'
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
-export { default as ConfirmPageContainerError } from './confirm-page-container-error'
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
index 39797a43f..698e624f4 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
@@ -1,5 +1,3 @@
-@import './confirm-page-container-error/index';
-
@import './confirm-page-container-warning/index';
@import './confirm-page-container-summary/index';
diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js
index 24ff05353..36d5a1f58 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js
@@ -38,9 +38,12 @@ export default class ConfirmPageContainer extends Component {
detailsComponent: PropTypes.node,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
summaryComponent: PropTypes.node,
warning: PropTypes.string,
+ unapprovedTxCount: PropTypes.number,
// Footer
+ onCancelAll: PropTypes.func,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
disabled: PropTypes.bool,
@@ -66,12 +69,16 @@ export default class ConfirmPageContainer extends Component {
summaryComponent,
detailsComponent,
dataComponent,
+ onCancelAll,
onCancel,
onSubmit,
identiconAddress,
nonce,
+ unapprovedTxCount,
+ assetImage,
warning,
} = this.props
+ const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
return (
<div className="page-container">
@@ -84,6 +91,7 @@ export default class ConfirmPageContainer extends Component {
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
+ assetImage={renderAssetImage ? assetImage : undefined}
/>
</ConfirmPageContainerHeader>
{
@@ -101,17 +109,25 @@ export default class ConfirmPageContainer extends Component {
errorKey={errorKey}
identiconAddress={identiconAddress}
nonce={nonce}
+ assetImage={assetImage}
warning={warning}
/>
)
}
<PageContainerFooter
onCancel={() => onCancel()}
+ cancelText={this.context.t('reject')}
onSubmit={() => onSubmit()}
submitText={this.context.t('confirm')}
submitButtonType="confirm"
disabled={disabled}
- />
+ >
+ {unapprovedTxCount > 1 && (
+ <a onClick={() => onCancelAll()}>
+ {this.context.t('rejectTxsN', [unapprovedTxCount])}
+ </a>
+ )}
+ </PageContainerFooter>
</div>
)
}
diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js
new file mode 100644
index 000000000..e4eb58a2a
--- /dev/null
+++ b/ui/app/components/currency-display/currency-display.component.js
@@ -0,0 +1,31 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { ETH, GWEI } from '../../constants/common'
+
+export default class CurrencyDisplay extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ displayValue: PropTypes.string,
+ prefix: PropTypes.string,
+ // Used in container
+ currency: PropTypes.oneOf([ETH]),
+ denomination: PropTypes.oneOf([GWEI]),
+ value: PropTypes.string,
+ numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ hideLabel: PropTypes.bool,
+ }
+
+ render () {
+ const { className, displayValue, prefix } = this.props
+ const text = `${prefix || ''}${displayValue}`
+
+ return (
+ <div
+ className={className}
+ title={text}
+ >
+ { text }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js
new file mode 100644
index 000000000..6644a1099
--- /dev/null
+++ b/ui/app/components/currency-display/currency-display.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import CurrencyDisplay from './currency-display.component'
+import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps
+ const { metamask: { currentCurrency, conversionRate } } = state
+
+ const toCurrency = currency || currentCurrency
+ const convertedValue = getValueFromWeiHex({
+ value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
+ })
+ const formattedValue = formatCurrency(convertedValue, toCurrency)
+ const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
+
+ return {
+ displayValue,
+ }
+}
+
+export default connect(mapStateToProps)(CurrencyDisplay)
diff --git a/ui/app/components/currency-display/index.js b/ui/app/components/currency-display/index.js
new file mode 100644
index 000000000..38f08765f
--- /dev/null
+++ b/ui/app/components/currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './currency-display.container'
diff --git a/ui/app/components/currency-display/tests/currency-display.component.test.js b/ui/app/components/currency-display/tests/currency-display.component.test.js
new file mode 100644
index 000000000..d9ef052f1
--- /dev/null
+++ b/ui/app/components/currency-display/tests/currency-display.component.test.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import CurrencyDisplay from '../currency-display.component'
+
+describe('CurrencyDisplay Component', () => {
+ it('should render text with a className', () => {
+ const wrapper = shallow(<CurrencyDisplay
+ displayValue="$123.45"
+ className="currency-display"
+ />)
+
+ assert.ok(wrapper.hasClass('currency-display'))
+ assert.equal(wrapper.text(), '$123.45')
+ })
+
+ it('should render text with a prefix', () => {
+ const wrapper = shallow(<CurrencyDisplay
+ displayValue="$123.45"
+ className="currency-display"
+ prefix="-"
+ />)
+
+ assert.ok(wrapper.hasClass('currency-display'))
+ assert.equal(wrapper.text(), '-$123.45')
+ })
+})
diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js
new file mode 100644
index 000000000..5265bbb04
--- /dev/null
+++ b/ui/app/components/currency-display/tests/currency-display.container.test.js
@@ -0,0 +1,105 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../currency-display.container.js', {
+ 'react-redux': {
+ connect: ms => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+})
+
+describe('CurrencyDisplay container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ },
+ }
+
+ const tests = [
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ numberOfDecimals: 2,
+ currency: 'usd',
+ },
+ result: {
+ displayValue: '$2.80 USD',
+ },
+ },
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ },
+ result: {
+ displayValue: '$2.80 USD',
+ },
+ },
+ {
+ props: {
+ value: '0x1193461d01595930',
+ currency: 'ETH',
+ numberOfDecimals: 3,
+ },
+ result: {
+ displayValue: '1.266 ETH',
+ },
+ },
+ {
+ props: {
+ value: '0x1193461d01595930',
+ currency: 'ETH',
+ numberOfDecimals: 3,
+ hideLabel: true,
+ },
+ result: {
+ displayValue: '1.266',
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ denomination: 'GWEI',
+ hideLabel: true,
+ },
+ result: {
+ displayValue: '1',
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ denomination: 'WEI',
+ hideLabel: true,
+ },
+ result: {
+ displayValue: '1000000000',
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ numberOfDecimals: 100,
+ hideLabel: true,
+ },
+ result: {
+ displayValue: '1e-9',
+ },
+ },
+ ]
+
+ tests.forEach(({ props, result }) => {
+ assert.deepEqual(mapStateToProps(mockState, props), result)
+ })
+ })
+ })
+})
diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js
deleted file mode 100644
index a4c525396..000000000
--- a/ui/app/components/custom-radio-list.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-
-module.exports = RadioList
-
-inherits(RadioList, Component)
-function RadioList () {
- Component.call(this)
-}
-
-RadioList.prototype.render = function () {
- const props = this.props
- const activeClass = '.custom-radio-selected'
- const inactiveClass = '.custom-radio-inactive'
- const {
- labels,
- defaultFocus,
- } = props
-
-
- return (
- h('.flex-row', {
- style: {
- fontSize: '12px',
- },
- }, [
- h('.flex-column.custom-radios', {
- style: {
- marginRight: '5px',
- },
- },
- labels.map((lable, i) => {
- let isSelcted = (this.state !== null)
- isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
- return h(isSelcted ? activeClass : inactiveClass, {
- title: lable,
- onClick: (event) => {
- this.setState({selected: event.target.title})
- props.onClick(event)
- },
- })
- })
- ),
- h('.text', {},
- labels.map((lable) => {
- if (props.subtext) {
- return h('.flex-row', {}, [
- h('.radio-titles', lable),
- h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
- ])
- } else {
- return h('.radio-titles', lable)
- }
- })
- ),
- ])
- )
-}
-
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
index c255fd64d..e67fbe45b 100644
--- a/ui/app/components/customize-gas-modal/index.js
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -5,6 +5,7 @@ const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const GasModalCard = require('./gas-modal-card')
+import Button from '../button'
const ethUtil = require('ethereumjs-util')
@@ -353,16 +354,16 @@ CustomizeGasModal.prototype.render = function () {
}, [this.context.t('revert')]),
h('div.send-v2__customize-gas__buttons', [
- h('button.btn-default.send-v2__customize-gas__cancel', {
+ h(Button, {
+ type: 'default',
+ className: 'send-v2__customize-gas__cancel',
onClick: this.props.hideModal,
- style: {
- marginRight: '10px',
- },
}, [this.context.t('cancel')]),
-
- h('button.btn-primary.send-v2__customize-gas__save', {
+ h(Button, {
+ type: 'primary',
+ className: 'send-v2__customize-gas__save',
onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
- className: error && 'btn-primary--disabled',
+ disabled: error,
}, [this.context.t('save')]),
]),
diff --git a/ui/app/components/dropdowns/account-details-dropdown.js b/ui/app/components/dropdowns/account-details-dropdown.js
new file mode 100644
index 000000000..7476cfdd9
--- /dev/null
+++ b/ui/app/components/dropdowns/account-details-dropdown.js
@@ -0,0 +1,109 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const { getSelectedIdentity } = require('../../selectors')
+const genAccountLink = require('../../../lib/account-link.js')
+const { Menu, Item, CloseArea } = require('./components/menu')
+
+AccountDetailsDropdown.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsDropdown)
+
+function mapStateToProps (state) {
+ return {
+ selectedIdentity: getSelectedIdentity(state),
+ network: state.metamask.network,
+ keyrings: state.metamask.keyrings,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ viewOnEtherscan: (address, network) => {
+ global.platform.openWindow({ url: genAccountLink(address, network) })
+ },
+ showRemoveAccountConfirmationModal: (identity) => {
+ return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
+ },
+ }
+}
+
+inherits(AccountDetailsDropdown, Component)
+function AccountDetailsDropdown () {
+ Component.call(this)
+
+ this.onClose = this.onClose.bind(this)
+}
+
+AccountDetailsDropdown.prototype.onClose = function (e) {
+ e.stopPropagation()
+ this.props.onClose()
+}
+
+AccountDetailsDropdown.prototype.render = function () {
+ const {
+ selectedIdentity,
+ network,
+ keyrings,
+ showAccountDetailModal,
+ viewOnEtherscan,
+ showRemoveAccountConfirmationModal } = this.props
+
+ const address = selectedIdentity.address
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(address)
+ })
+
+ const isRemovable = keyring.type !== 'HD Key Tree'
+
+ return h(Menu, { className: 'account-details-dropdown', isShowing: true }, [
+ h(CloseArea, {
+ onClick: this.onClose,
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ global.platform.openExtensionInBrowser()
+ this.props.onClose()
+ },
+ text: this.context.t('expandView'),
+ icon: h(`img`, { src: 'images/expand.svg', style: { height: '15px' } }),
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ showAccountDetailModal()
+ this.props.onClose()
+ },
+ text: this.context.t('accountDetails'),
+ icon: h(`img`, { src: 'images/info.svg', style: { height: '15px' } }),
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ viewOnEtherscan(address, network)
+ this.props.onClose()
+ },
+ text: this.context.t('viewOnEtherscan'),
+ icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }),
+ }),
+ isRemovable ? h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ showRemoveAccountConfirmationModal(selectedIdentity)
+ this.props.onClose()
+ },
+ text: this.context.t('removeAccount'),
+ icon: h(`img`, { src: 'images/hide.svg', style: { height: '15px' } }),
+ }) : null,
+ ])
+}
diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js
index 179b6617f..b497f5c09 100644
--- a/ui/app/components/dropdowns/components/account-dropdowns.js
+++ b/ui/app/components/dropdowns/components/account-dropdowns.js
@@ -459,7 +459,7 @@ const mapDispatchToProps = (dispatch) => {
function mapStateToProps (state) {
return {
keyrings: state.metamask.keyrings,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
}
}
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
index e5363ff56..b252b25d9 100644
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ b/ui/app/components/dropdowns/network-dropdown.js
@@ -43,6 +43,9 @@ function mapDispatchToProps (dispatch) {
setRpcTarget: (target) => {
dispatch(actions.setRpcTarget(target))
},
+ delRpcTarget: (target) => {
+ dispatch(actions.delRpcTarget(target))
+ },
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
}
@@ -272,10 +275,12 @@ NetworkDropdown.prototype.getNetworkName = function () {
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
- const rpcTarget = provider.rpcTarget
+ const reversedRpcList = rpcList.slice().reverse()
+
+ return reversedRpcList.map((rpc) => {
+ const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
- return rpcList.map((rpc) => {
- if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
+ if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
return null
} else {
return h(
@@ -291,13 +296,20 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
},
},
[
- rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
h('span.network-name-item', {
style: {
- color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b',
+ color: currentRpcTarget ? '#ffffff' : '#9b9b9b',
},
}, rpc),
+ h('i.fa.fa-times.delete',
+ {
+ onClick: (e) => {
+ e.stopPropagation()
+ props.delRpcTarget(rpc)
+ },
+ }),
]
)
}
diff --git a/ui/app/components/dropdowns/tests/dropdown.test.js b/ui/app/components/dropdowns/tests/dropdown.test.js
new file mode 100644
index 000000000..2b026589a
--- /dev/null
+++ b/ui/app/components/dropdowns/tests/dropdown.test.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import assert from 'assert'
+import sinon from 'sinon'
+import { shallow } from 'enzyme'
+import { DropdownMenuItem } from '../components/dropdown.js'
+
+describe('', () => {
+ let wrapper
+ const onClickSpy = sinon.spy()
+ const closeMenuSpy = sinon.spy()
+
+ beforeEach(() => {
+ wrapper = shallow(
+ <DropdownMenuItem
+ onClick = {onClickSpy}
+ style = {{test: 'style'}}
+ closeMenu = {closeMenuSpy}
+ >
+ </DropdownMenuItem>
+ )
+ })
+
+ it('renders li with dropdown-menu-item class', () => {
+ assert.equal(wrapper.find('li.dropdown-menu-item').length, 1)
+ })
+
+ it('adds style based on props passed', () => {
+ assert.equal(wrapper.prop('style').test, 'style')
+ })
+
+ it('simulates click event and calls onClick and closeMenu', () => {
+ wrapper.prop('onClick')()
+ assert.equal(onClickSpy.callCount, 1)
+ assert.equal(closeMenuSpy.callCount, 1)
+ })
+
+})
diff --git a/ui/app/components/dropdowns/tests/menu.test.js b/ui/app/components/dropdowns/tests/menu.test.js
new file mode 100644
index 000000000..9f5f13f00
--- /dev/null
+++ b/ui/app/components/dropdowns/tests/menu.test.js
@@ -0,0 +1,87 @@
+import React from 'react'
+import assert from 'assert'
+import sinon from 'sinon'
+import { shallow } from 'enzyme'
+import { Menu, Item, Divider, CloseArea } from '../components/menu'
+
+describe('Dropdown Menu Components', () => {
+
+ describe('Menu', () => {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(
+ <Menu className = {'Test Class'} isShowing = {true}/>
+ )
+ })
+
+ it('adds prop className to menu', () => {
+ assert.equal(wrapper.find('.menu').prop('className'), 'menu Test Class')
+ })
+
+ })
+
+ describe('Item', () => {
+ let wrapper
+
+ const onClickSpy = sinon.spy()
+
+ beforeEach(() => {
+ wrapper = shallow(
+ <Item
+ icon = {'test icon'}
+ text = {'test text'}
+ className = {'test className'}
+ onClick = {onClickSpy}
+ />
+ )
+ })
+
+ it('add className based on props', () => {
+ assert.equal(wrapper.find('.menu__item').prop('className'), 'menu__item menu__item test className menu__item--clickable')
+ })
+
+ it('simulates onClick called', () => {
+ wrapper.find('.menu__item').prop('onClick')()
+ assert.equal(onClickSpy.callCount, 1)
+ })
+
+ it('adds icon based on icon props', () => {
+ assert.equal(wrapper.find('.menu__item__icon').text(), 'test icon')
+ })
+
+ it('adds html text based on text props', () => {
+ assert.equal(wrapper.find('.menu__item__text').text(), 'test text')
+ })
+ })
+
+ describe('Divider', () => {
+ let wrapper
+
+ before(() => {
+ wrapper = shallow(<Divider />)
+ })
+
+ it('renders menu divider', () => {
+ assert.equal(wrapper.find('.menu__divider').length, 1)
+ })
+ })
+
+ describe('CloseArea', () => {
+ let wrapper
+
+ const onClickSpy = sinon.spy()
+
+ beforeEach(() => {
+ wrapper = shallow(<CloseArea
+ onClick = {onClickSpy}
+ />)
+ })
+
+ it('simulates click', () => {
+ wrapper.prop('onClick')()
+ assert.equal(onClickSpy.callCount, 1)
+ })
+ })
+
+})
diff --git a/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js b/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js
new file mode 100644
index 000000000..67b192c11
--- /dev/null
+++ b/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import NetworkDropdownIcon from '../components/network-dropdown-icon'
+
+describe('Network Dropdown Icon', () => {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<NetworkDropdownIcon
+ backgroundColor = {'red'}
+ isSelected = {false}
+ innerBorder = {'none'}
+ diameter = {'12'}
+ />)
+ })
+
+ it('adds style props based on props', () => {
+ const styleProp = wrapper.find('.menu-icon-circle').children().prop('style')
+ assert.equal(styleProp.background, 'red')
+ assert.equal(styleProp.border, 'none')
+ assert.equal(styleProp.height, '12px')
+ assert.equal(styleProp.width, '12px')
+ })
+})
diff --git a/ui/app/components/dropdowns/tests/network-dropdown.test.js b/ui/app/components/dropdowns/tests/network-dropdown.test.js
new file mode 100644
index 000000000..699b54605
--- /dev/null
+++ b/ui/app/components/dropdowns/tests/network-dropdown.test.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import assert from 'assert'
+import { createMockStore } from 'redux-test-utils'
+import { mountWithRouter } from '../../../../../test/lib/render-helpers'
+import NetworkDropdown from '../network-dropdown'
+import { DropdownMenuItem } from '../components/dropdown'
+import NetworkDropdownIcon from '../components/network-dropdown-icon'
+
+describe('Network Dropdown', () => {
+ let wrapper
+
+ describe('NetworkDropdown in appState in false', () => {
+ const mockState = {
+ metamask: {
+ provider: {
+ type: 'test',
+ },
+ },
+ appState: {
+ networkDropdown: false,
+ },
+ }
+
+ const store = createMockStore(mockState)
+
+ beforeEach(() => {
+ wrapper = mountWithRouter(
+ <NetworkDropdown store={store} />
+ )
+ })
+
+ it('checks for network droppo class', () => {
+ assert.equal(wrapper.find('.network-droppo').length, 1)
+ })
+
+ it('renders only one child when networkDropdown is false in state', () => {
+ assert.equal(wrapper.children().length, 1)
+ })
+
+ })
+
+ describe('NetworkDropdown in appState is true', () => {
+ const mockState = {
+ metamask: {
+ provider: {
+ 'type': 'test',
+ },
+ frequentRpcList: [
+ 'http://localhost:7545',
+ ],
+ },
+ appState: {
+ 'networkDropdownOpen': true,
+ },
+ }
+ const store = createMockStore(mockState)
+
+ beforeEach(() => {
+ wrapper = mountWithRouter(
+ <NetworkDropdown store={store}/>,
+ )
+ })
+
+ it('renders 7 DropDownMenuItems ', () => {
+ assert.equal(wrapper.find(DropdownMenuItem).length, 7)
+ })
+
+ it('checks background color for first NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(0).prop('backgroundColor'), '#29B6AF') // Main Ethereum Network Teal
+ })
+
+ it('checks background color for second NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(1).prop('backgroundColor'), '#ff4a8d') // Ropsten Red
+ })
+
+ it('checks background color for third NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(2).prop('backgroundColor'), '#7057ff') // Kovan Purple
+ })
+
+ it('checks background color for fourth NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(3).prop('backgroundColor'), '#f6c343') // Rinkeby Yellow
+ })
+
+ it('checks background color for fifth NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(4).prop('innerBorder'), '1px solid #9b9b9b')
+ })
+
+ it('checks dropdown for frequestRPCList from state ', () => {
+ assert.equal(wrapper.find(DropdownMenuItem).at(5).text(), '✓http://localhost:7545')
+ })
+
+ it('checks background color for sixth NetworkDropdownIcon', () => {
+ assert.equal(wrapper.find(NetworkDropdownIcon).at(5).prop('innerBorder'), '1px solid #9b9b9b')
+ })
+
+ })
+})
diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js
index 5a794c7c1..8a072b1bc 100644
--- a/ui/app/components/dropdowns/token-menu-dropdown.js
+++ b/ui/app/components/dropdowns/token-menu-dropdown.js
@@ -5,7 +5,6 @@ const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const genAccountLink = require('etherscan-link').createAccountLink
-const copyToClipboard = require('copy-to-clipboard')
const { Menu, Item, CloseArea } = require('./components/menu')
TokenMenuDropdown.contextTypes = {
@@ -59,14 +58,6 @@ TokenMenuDropdown.prototype.render = function () {
h(Item, {
onClick: (e) => {
e.stopPropagation()
- copyToClipboard(this.props.token.address)
- this.props.onClose()
- },
- text: this.context.t('copyContractAddress'),
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
const url = genAccountLink(this.props.token.address, this.props.network)
global.platform.openWindow({ url })
this.props.onClose()
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/error-message/error-message.component.js
index 70ebdeb20..b4464c33b 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
+++ b/ui/app/components/error-message/error-message.component.js
@@ -1,28 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
-const ConfirmPageContainerError = (props, context) => {
+const ErrorMessage = (props, context) => {
const { errorMessage, errorKey } = props
const error = errorKey ? context.t(errorKey) : errorMessage
return (
- <div className="confirm-page-container-error">
+ <div className="error-message">
<img
src="/images/alert-red.svg"
- className="confirm-page-container-error__icon"
+ className="error-message__icon"
/>
- { `ALERT: ${error}` }
+ <div className="error-message__text">
+ { `ALERT: ${error}` }
+ </div>
</div>
)
}
-ConfirmPageContainerError.propTypes = {
+ErrorMessage.propTypes = {
errorMessage: PropTypes.string,
errorKey: PropTypes.string,
}
-ConfirmPageContainerError.contextTypes = {
+ErrorMessage.contextTypes = {
t: PropTypes.func,
}
-export default ConfirmPageContainerError
+export default ErrorMessage
diff --git a/ui/app/components/error-message/index.js b/ui/app/components/error-message/index.js
new file mode 100644
index 000000000..1c97a9955
--- /dev/null
+++ b/ui/app/components/error-message/index.js
@@ -0,0 +1 @@
+export { default } from './error-message.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/error-message/index.scss
index e99b0f631..5915e21cf 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
+++ b/ui/app/components/error-message/index.scss
@@ -1,5 +1,5 @@
-.confirm-page-container-error {
- height: 32px;
+.error-message {
+ min-height: 32px;
border: 1px solid $monzo;
color: $monzo;
background: lighten($monzo, 56%);
@@ -8,10 +8,14 @@
display: flex;
justify-content: flex-start;
align-items: center;
- padding-left: 16px;
+ padding: 8px 16px;
&__icon {
margin-right: 8px;
flex: 0 0 auto;
}
+
+ &__text {
+ overflow: auto;
+ }
}
diff --git a/ui/app/components/error-message/tests/error-message.component.test.js b/ui/app/components/error-message/tests/error-message.component.test.js
new file mode 100644
index 000000000..8c5347173
--- /dev/null
+++ b/ui/app/components/error-message/tests/error-message.component.test.js
@@ -0,0 +1,36 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import ErrorMessage from '../error-message.component'
+
+describe('ErrorMessage Component', () => {
+ const t = key => `translate ${key}`
+
+ it('should render a message from props.errorMessage', () => {
+ const wrapper = shallow(
+ <ErrorMessage
+ errorMessage="This is an error."
+ />,
+ { context: { t }}
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.error-message').length, 1)
+ assert.equal(wrapper.find('.error-message__icon').length, 1)
+ assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: This is an error.')
+ })
+
+ it('should render a message translated from props.errorKey', () => {
+ const wrapper = shallow(
+ <ErrorMessage
+ errorKey="testKey"
+ />,
+ { context: { t }}
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.error-message').length, 1)
+ assert.equal(wrapper.find('.error-message__icon').length, 1)
+ assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: translate testKey')
+ })
+})
diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js
deleted file mode 100644
index 75303a34a..000000000
--- a/ui/app/components/hex-as-decimal-input.js
+++ /dev/null
@@ -1,161 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const extend = require('xtend')
-const connect = require('react-redux').connect
-
-HexAsDecimalInput.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(HexAsDecimalInput)
-
-
-inherits(HexAsDecimalInput, Component)
-function HexAsDecimalInput () {
- this.state = { invalid: null }
- Component.call(this)
-}
-
-/* Hex as Decimal Input
- *
- * A component for allowing easy, decimal editing
- * of a passed in hex string value.
- *
- * On change, calls back its `onChange` function parameter
- * and passes it an updated hex string.
- */
-
-HexAsDecimalInput.prototype.render = function () {
- const props = this.props
- const state = this.state
-
- const { value, onChange, min, max } = props
-
- const toEth = props.toEth
- const suffix = props.suffix
- const decimalValue = decimalize(value, toEth)
- const style = props.style
-
- return (
- h('.flex-column', [
- h('.flex-row', {
- style: {
- alignItems: 'flex-end',
- lineHeight: '13px',
- fontFamily: 'Montserrat Light',
- textRendering: 'geometricPrecision',
- },
- }, [
- h('input.hex-input', {
- type: 'number',
- required: true,
- min: min,
- max: max,
- style: extend({
- display: 'block',
- textAlign: 'right',
- backgroundColor: 'transparent',
- border: '1px solid #bdbdbd',
-
- }, style),
- value: parseInt(decimalValue),
- onBlur: (event) => {
- this.updateValidity(event)
- },
- onChange: (event) => {
- this.updateValidity(event)
- const hexString = (event.target.value === '') ? '' : hexify(event.target.value)
- onChange(hexString)
- },
- onInvalid: (event) => {
- const msg = this.constructWarning()
- if (msg === state.invalid) {
- return
- }
- this.setState({ invalid: msg })
- event.preventDefault()
- return false
- },
- }),
- h('div', {
- style: {
- color: ' #AEAEAE',
- fontSize: '12px',
- marginLeft: '5px',
- marginRight: '6px',
- width: '20px',
- },
- }, suffix),
- ]),
-
- state.invalid ? h('span.error', {
- style: {
- position: 'absolute',
- right: '0px',
- textAlign: 'right',
- transform: 'translateY(26px)',
- padding: '3px',
- background: 'rgba(255,255,255,0.85)',
- zIndex: '1',
- textTransform: 'capitalize',
- border: '2px solid #E20202',
- },
- }, state.invalid) : null,
- ])
- )
-}
-
-HexAsDecimalInput.prototype.setValid = function (message) {
- this.setState({ invalid: null })
-}
-
-HexAsDecimalInput.prototype.updateValidity = function (event) {
- const target = event.target
- const value = this.props.value
- const newValue = target.value
-
- if (value === newValue) {
- return
- }
-
- const valid = target.checkValidity()
- if (valid) {
- this.setState({ invalid: null })
- }
-}
-
-HexAsDecimalInput.prototype.constructWarning = function () {
- const { name, min, max } = this.props
- let message = name ? name + ' ' : ''
-
- if (min && max) {
- message += this.context.t('betweenMinAndMax', [min, max])
- } else if (min) {
- message += this.context.t('greaterThanMin', [min])
- } else if (max) {
- message += this.context.t('lessThanMax', [max])
- } else {
- message += this.context.t('invalidInput')
- }
-
- return message
-}
-
-function hexify (decimalString) {
- const hexBN = new BN(parseInt(decimalString), 10)
- return '0x' + hexBN.toString('hex')
-}
-
-function decimalize (input, toEth) {
- if (input === '') {
- return ''
- } else {
- const strippedInput = ethUtil.stripHexPrefix(input)
- const inputBN = new BN(strippedInput, 'hex')
- return inputBN.toString(10)
- }
-}
diff --git a/ui/app/components/hex-to-decimal/hex-to-decimal.component.js b/ui/app/components/hex-to-decimal/hex-to-decimal.component.js
new file mode 100644
index 000000000..6847a6919
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/hex-to-decimal.component.js
@@ -0,0 +1,21 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { hexToDecimal } from '../../helpers/conversions.util'
+
+export default class HexToDecimal extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ value: PropTypes.string,
+ }
+
+ render () {
+ const { className, value } = this.props
+ const decimalValue = hexToDecimal(value)
+
+ return (
+ <div className={className}>
+ { decimalValue }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/hex-to-decimal/index.js b/ui/app/components/hex-to-decimal/index.js
new file mode 100644
index 000000000..6e8567ca9
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/index.js
@@ -0,0 +1 @@
+export { default } from './hex-to-decimal.component'
diff --git a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js b/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
new file mode 100644
index 000000000..c98da9ad4
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import HexToDecimal from '../hex-to-decimal.component'
+
+describe('HexToDecimal Component', () => {
+ it('should render a prefixed hex as a decimal with a className', () => {
+ const wrapper = shallow(<HexToDecimal
+ value="0x3039"
+ className="hex-to-decimal"
+ />)
+
+ assert.ok(wrapper.hasClass('hex-to-decimal'))
+ assert.equal(wrapper.text(), '12345')
+ })
+
+ it('should render an unprefixed hex as a decimal with a className', () => {
+ const wrapper = shallow(<HexToDecimal
+ value="1A85"
+ className="hex-to-decimal"
+ />)
+
+ assert.ok(wrapper.hasClass('hex-to-decimal'))
+ assert.equal(wrapper.text(), '6789')
+ })
+})
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index 424048745..7bd921892 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -26,36 +26,43 @@ function mapStateToProps (state) {
IdenticonComponent.prototype.render = function () {
var props = this.props
- const { className = '', address } = props
+ const { className = '', address, image } = props
var diameter = props.diameter || this.defaultDiameter
-
- return address
- ? (
- h('div', {
- className: `${className} identicon`,
- key: 'identicon-' + address,
- style: {
- display: 'flex',
- flexShrink: 0,
- alignItems: 'center',
- justifyContent: 'center',
- height: diameter,
- width: diameter,
- borderRadius: diameter / 2,
- overflow: 'hidden',
- },
- })
- )
- : (
- h('img.balance-icon', {
- src: './images/eth_logo.svg',
- style: {
- height: diameter,
- width: diameter,
- borderRadius: diameter / 2,
- },
- })
- )
+ const style = {
+ height: diameter,
+ width: diameter,
+ borderRadius: diameter / 2,
+ }
+ if (image) {
+ return h('img', {
+ className: `${className} identicon`,
+ src: image,
+ style: {
+ ...style,
+ },
+ })
+ } else if (address) {
+ return h('div', {
+ className: `${className} identicon`,
+ key: 'identicon-' + address,
+ style: {
+ display: 'flex',
+ flexShrink: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ ...style,
+ overflow: 'hidden',
+ },
+ })
+ } else {
+ return h('img.balance-icon', {
+ className,
+ src: './images/eth_logo.svg',
+ style: {
+ ...style,
+ },
+ })
+ }
}
IdenticonComponent.prototype.componentDidMount = function () {
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index b3e14ce23..21b65bf55 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -1,21 +1,51 @@
+@import './app-header/index';
+
@import './button-group/index';
-@import './export-text-container/index';
+@import './card/index';
-@import './selected-account/index';
+@import './confirm-page-container/index';
+
+@import './error-message/index';
+
+@import './export-text-container/index';
@import './info-box/index';
-@import './network-display/index';
+@import './menu-bar/index';
-@import './confirm-page-container/index';
+@import './modal/index';
+
+@import './modals/index';
+
+@import './network-display/index';
@import './page-container/index';
@import './pages/index';
-@import './modals/index';
+@import './selected-account/index';
@import './sender-to-recipient/index';
@import './tabs/index';
+
+@import './transaction-activity-log/index';
+
+@import './transaction-breakdown/index';
+
+@import './transaction-view/index';
+
+@import './transaction-view-balance/index';
+
+@import './transaction-list/index';
+
+@import './transaction-list-item/index';
+
+@import './transaction-list-item-details/index';
+
+@import './transaction-status/index';
+
+@import './app-header/index';
+
+@import './sidebars/index';
diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js
index 59c6842ef..eec5e3740 100644
--- a/ui/app/components/input-number.js
+++ b/ui/app/components/input-number.js
@@ -66,13 +66,16 @@ InputNumber.prototype.render = function () {
}),
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
- h('i.fa.fa-angle-up', {
+ h('div.gas-tooltip-input-arrow-wrapper', {
onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
- }),
- h('i.fa.fa-angle-down', {
- style: { cursor: 'pointer' },
+ }, [
+ h('i.fa.fa-angle-up'),
+ ]),
+ h('div.gas-tooltip-input-arrow-wrapper', {
onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })),
- }),
+ }, [
+ h('i.fa.fa-angle-down'),
+ ]),
]),
])
}
diff --git a/ui/app/components/menu-bar/index.js b/ui/app/components/menu-bar/index.js
new file mode 100644
index 000000000..c5760847f
--- /dev/null
+++ b/ui/app/components/menu-bar/index.js
@@ -0,0 +1 @@
+export { default } from './menu-bar.container'
diff --git a/ui/app/components/menu-bar/index.scss b/ui/app/components/menu-bar/index.scss
new file mode 100644
index 000000000..f699f4090
--- /dev/null
+++ b/ui/app/components/menu-bar/index.scss
@@ -0,0 +1,23 @@
+.menu-bar {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ flex: 0 0 auto;
+ margin-bottom: 16px;
+ padding: 5px;
+ border-bottom: 1px solid #e5e5e5;
+
+ &__sidebar-button {
+ font-size: 1.25rem;
+ cursor: pointer;
+ padding: 10px;
+ }
+
+ &__open-in-browser {
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ padding: 10px;
+ }
+}
diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js
new file mode 100644
index 000000000..7460e8dd5
--- /dev/null
+++ b/ui/app/components/menu-bar/menu-bar.component.js
@@ -0,0 +1,63 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Tooltip from '../tooltip'
+import SelectedAccount from '../selected-account'
+import AccountDetailsDropdown from '../dropdowns/account-details-dropdown.js'
+
+export default class MenuBar extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideSidebar: PropTypes.func,
+ isMascara: PropTypes.bool,
+ sidebarOpen: PropTypes.bool,
+ showSidebar: PropTypes.func,
+ }
+
+ state = { accountDetailsMenuOpen: false }
+
+ render () {
+ const { t } = this.context
+ const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
+ const { accountDetailsMenuOpen } = this.state
+
+ return (
+ <div className="menu-bar">
+ <Tooltip
+ title={t('menu')}
+ position="bottom"
+ >
+ <div
+ className="fa fa-bars menu-bar__sidebar-button"
+ onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
+ />
+ </Tooltip>
+ <SelectedAccount />
+ {
+ !isMascara && (
+ <Tooltip
+ title={t('accountOptions')}
+ position="bottom"
+ >
+ <div
+ className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser"
+ onClick={() => this.setState({ accountDetailsMenuOpen: true })}
+ >
+ </div>
+ </Tooltip>
+ )
+ }
+ {
+ accountDetailsMenuOpen && (
+ <AccountDetailsDropdown
+ className="menu-bar__account-details-dropdown"
+ onClose={() => this.setState({ accountDetailsMenuOpen: false })}
+ />
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/menu-bar/menu-bar.container.js
new file mode 100644
index 000000000..ae32882ae
--- /dev/null
+++ b/ui/app/components/menu-bar/menu-bar.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import MenuBar from './menu-bar.component'
+import { showSidebar, hideSidebar } from '../../actions'
+
+const mapStateToProps = state => {
+ const { appState: { sidebar: { isOpen }, isMascara } } = state
+
+ return {
+ sidebarOpen: isOpen,
+ isMascara,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showSidebar: () => {
+ dispatch(showSidebar({
+ transitionName: 'sidebar-right',
+ type: 'wallet-view',
+ }))
+ },
+ hideSidebar: () => dispatch(hideSidebar()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
diff --git a/ui/app/components/modal/index.js b/ui/app/components/modal/index.js
new file mode 100644
index 000000000..58309abbe
--- /dev/null
+++ b/ui/app/components/modal/index.js
@@ -0,0 +1,2 @@
+export { default } from './modal.component'
+export { default as ModalContent } from './modal-content'
diff --git a/ui/app/components/modal/index.scss b/ui/app/components/modal/index.scss
new file mode 100644
index 000000000..2beb14633
--- /dev/null
+++ b/ui/app/components/modal/index.scss
@@ -0,0 +1,62 @@
+@import './modal-content/index';
+
+.modal-container {
+ width: 100%;
+ height: 100%;
+ background-color: #fff;
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+
+ @media screen and (max-width: 575px) {
+ max-height: 450px;
+ }
+
+ &__content {
+ overflow-y: auto;
+ flex: 1;
+ padding: 16px 32px;
+
+ @media screen and (max-width: 575px) {
+ justify-content: center;
+ padding: 28px 20px;
+ }
+ }
+
+ &__header {
+ position: relative;
+ display: flex;
+ padding: 12px;
+ justify-content: center;
+ border-bottom: 1px solid #d2d8dd;
+ flex: 0 0 auto;
+ }
+
+ &__header-close::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ position: absolute;
+ top: -5px;
+ right: 10px;
+ cursor: pointer;
+ }
+
+ &__footer {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ border-top: 1px solid #d2d8dd;
+ padding: 16px;
+ flex: 0 0 auto;
+
+ &-button {
+ min-width: 0;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+ }
+ }
+}
diff --git a/ui/app/components/modal/modal-content/index.js b/ui/app/components/modal/modal-content/index.js
new file mode 100644
index 000000000..733cfb3b8
--- /dev/null
+++ b/ui/app/components/modal/modal-content/index.js
@@ -0,0 +1 @@
+export { default } from './modal-content.component'
diff --git a/ui/app/components/modal/modal-content/index.scss b/ui/app/components/modal/modal-content/index.scss
new file mode 100644
index 000000000..560505b84
--- /dev/null
+++ b/ui/app/components/modal/modal-content/index.scss
@@ -0,0 +1,19 @@
+.modal-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 16px 0;
+
+ &__title {
+ font-size: 1.5rem;
+ font-weight: 500;
+ padding: 16px 0;
+ text-align: center;
+ }
+
+ &__description {
+ text-align: center;
+ font-size: .875rem;
+ }
+}
diff --git a/ui/app/components/modal/modal-content/modal-content.component.js b/ui/app/components/modal/modal-content/modal-content.component.js
new file mode 100644
index 000000000..ecec0ee5b
--- /dev/null
+++ b/ui/app/components/modal/modal-content/modal-content.component.js
@@ -0,0 +1,32 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+
+export default class ModalContent extends PureComponent {
+ static propTypes = {
+ title: PropTypes.string,
+ description: PropTypes.string,
+ }
+
+ render () {
+ const { title, description } = this.props
+
+ return (
+ <div className="modal-content">
+ {
+ title && (
+ <div className="modal-content__title">
+ { title }
+ </div>
+ )
+ }
+ {
+ description && (
+ <div className="modal-content__description">
+ { description }
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/modal/modal-content/tests/modal-content.component.test.js
new file mode 100644
index 000000000..17af09f45
--- /dev/null
+++ b/ui/app/components/modal/modal-content/tests/modal-content.component.test.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import ModalContent from '../modal-content.component'
+
+describe('ModalContent Component', () => {
+ it('should render a title', () => {
+ const wrapper = shallow(
+ <ModalContent
+ title="Modal Title"
+ />
+ )
+
+ assert.equal(wrapper.find('.modal-content__title').length, 1)
+ assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title')
+ assert.equal(wrapper.find('.modal-content__description').length, 0)
+ })
+
+ it('should render a description', () => {
+ const wrapper = shallow(
+ <ModalContent
+ description="Modal Description"
+ />
+ )
+
+ assert.equal(wrapper.find('.modal-content__title').length, 0)
+ assert.equal(wrapper.find('.modal-content__description').length, 1)
+ assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description')
+ })
+
+ it('should render both a title and a description', () => {
+ const wrapper = shallow(
+ <ModalContent
+ title="Modal Title"
+ description="Modal Description"
+ />
+ )
+
+ assert.equal(wrapper.find('.modal-content__title').length, 1)
+ assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title')
+ assert.equal(wrapper.find('.modal-content__description').length, 1)
+ assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description')
+ })
+})
diff --git a/ui/app/components/modal/modal.component.js b/ui/app/components/modal/modal.component.js
new file mode 100644
index 000000000..2a75b559b
--- /dev/null
+++ b/ui/app/components/modal/modal.component.js
@@ -0,0 +1,80 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../button'
+
+export default class Modal extends PureComponent {
+ static propTypes = {
+ children: PropTypes.node,
+ // Header text
+ headerText: PropTypes.string,
+ onClose: PropTypes.func,
+ // Submit button (right button)
+ onSubmit: PropTypes.func,
+ submitType: PropTypes.string,
+ submitText: PropTypes.string,
+ // Cancel button (left button)
+ onCancel: PropTypes.func,
+ cancelType: PropTypes.string,
+ cancelText: PropTypes.string,
+ }
+
+ static defaultProps = {
+ submitType: 'primary',
+ cancelType: 'default',
+ }
+
+ render () {
+ const {
+ children,
+ headerText,
+ onClose,
+ onSubmit,
+ submitType,
+ submitText,
+ onCancel,
+ cancelType,
+ cancelText,
+ } = this.props
+
+ return (
+ <div className="modal-container">
+ {
+ headerText && (
+ <div className="modal-container__header">
+ <div className="modal-container__header-text">
+ { headerText }
+ </div>
+ <div
+ className="modal-container__header-close"
+ onClick={onClose}
+ />
+ </div>
+ )
+ }
+ <div className="modal-container__content">
+ { children }
+ </div>
+ <div className="modal-container__footer">
+ {
+ onCancel && (
+ <Button
+ type={cancelType}
+ onClick={onCancel}
+ className="modal-container__footer-button"
+ >
+ { cancelText }
+ </Button>
+ )
+ }
+ <Button
+ type={submitType}
+ onClick={onSubmit}
+ className="modal-container__footer-button"
+ >
+ { submitText }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modal/tests/modal.component.test.js b/ui/app/components/modal/tests/modal.component.test.js
new file mode 100644
index 000000000..8cce1a808
--- /dev/null
+++ b/ui/app/components/modal/tests/modal.component.test.js
@@ -0,0 +1,103 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import Modal from '../modal.component'
+import Button from '../../button'
+
+describe('Modal Component', () => {
+ it('should render a modal with a submit button', () => {
+ const wrapper = shallow(<Modal />)
+
+ assert.equal(wrapper.find('.modal-container').length, 1)
+ const buttons = wrapper.find(Button)
+ assert.equal(buttons.length, 1)
+ assert.equal(buttons.at(0).props().type, 'primary')
+ })
+
+ it('should render a modal with a cancel and a submit button', () => {
+ const handleCancel = sinon.spy()
+ const handleSubmit = sinon.spy()
+ const wrapper = shallow(
+ <Modal
+ onCancel={handleCancel}
+ cancelText="Cancel"
+ onSubmit={handleSubmit}
+ submitText="Submit"
+ />
+ )
+
+ const buttons = wrapper.find(Button)
+ assert.equal(buttons.length, 2)
+ const cancelButton = buttons.at(0)
+ const submitButton = buttons.at(1)
+
+ assert.equal(cancelButton.props().type, 'default')
+ assert.equal(cancelButton.props().children, 'Cancel')
+ assert.equal(handleCancel.callCount, 0)
+ cancelButton.simulate('click')
+ assert.equal(handleCancel.callCount, 1)
+
+ assert.equal(submitButton.props().type, 'primary')
+ assert.equal(submitButton.props().children, 'Submit')
+ assert.equal(handleSubmit.callCount, 0)
+ submitButton.simulate('click')
+ assert.equal(handleSubmit.callCount, 1)
+ })
+
+ it('should render a modal with different button types', () => {
+ const wrapper = shallow(
+ <Modal
+ onCancel={() => {}}
+ cancelText="Cancel"
+ cancelType="secondary"
+ onSubmit={() => {}}
+ submitText="Submit"
+ submitType="confirm"
+ />
+ )
+
+ const buttons = wrapper.find(Button)
+ assert.equal(buttons.length, 2)
+ assert.equal(buttons.at(0).props().type, 'secondary')
+ assert.equal(buttons.at(1).props().type, 'confirm')
+ })
+
+ it('should render a modal with children', () => {
+ const wrapper = shallow(
+ <Modal
+ onCancel={() => {}}
+ cancelText="Cancel"
+ onSubmit={() => {}}
+ submitText="Submit"
+ >
+ <div className="test-child" />
+ </Modal>
+ )
+
+ assert.ok(wrapper.find('.test-class'))
+ })
+
+ it('should render a modal with a header', () => {
+ const handleCancel = sinon.spy()
+ const handleSubmit = sinon.spy()
+ const wrapper = shallow(
+ <Modal
+ onCancel={handleCancel}
+ cancelText="Cancel"
+ onSubmit={handleSubmit}
+ submitText="Submit"
+ headerText="My Header"
+ onClose={handleCancel}
+ />
+ )
+
+ assert.ok(wrapper.find('.modal-container__header'))
+ assert.equal(wrapper.find('.modal-container__header-text').text(), 'My Header')
+ assert.equal(handleCancel.callCount, 0)
+ assert.equal(handleSubmit.callCount, 0)
+ wrapper.find('.modal-container__header-close').simulate('click')
+ assert.equal(handleCancel.callCount, 1)
+ assert.equal(handleSubmit.callCount, 0)
+ })
+})
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index cc90cf578..248ffe008 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -10,6 +10,8 @@ const genAccountLink = require('../../../lib/account-link.js')
const QrView = require('../qr-code')
const EditableLabel = require('../editable-label')
+import Button from '../button'
+
function mapStateToProps (state) {
return {
network: state.metamask.network,
@@ -61,7 +63,7 @@ AccountDetailsModal.prototype.render = function () {
let exportPrivateKeyFeatureEnabled = true
// This feature is disabled for hardware wallets
- if (keyring.type.search('Hardware') !== -1) {
+ if (keyring && keyring.type.search('Hardware') !== -1) {
exportPrivateKeyFeatureEnabled = false
}
@@ -80,12 +82,17 @@ AccountDetailsModal.prototype.render = function () {
h('div.account-modal-divider'),
- h('button.btn-primary.account-modal__button', {
+ h(Button, {
+ type: 'primary',
+ className: 'account-modal__button',
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }),
}, this.context.t('etherscanView')),
// Holding on redesign for Export Private Key functionality
- exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
+
+ exportPrivateKeyFeatureEnabled ? h(Button, {
+ type: 'primary',
+ className: 'account-modal__button',
onClick: () => showExportPrivateKeyModal(),
}, this.context.t('exportPrivateKey')) : null,
diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js
index a9856b20f..aa0593df8 100644
--- a/ui/app/components/modals/account-modal-container.js
+++ b/ui/app/components/modals/account-modal-container.js
@@ -7,9 +7,9 @@ const actions = require('../../actions')
const { getSelectedIdentity } = require('../../selectors')
const Identicon = require('../identicon')
-function mapStateToProps (state) {
+function mapStateToProps (state, ownProps) {
return {
- selectedIdentity: getSelectedIdentity(state),
+ selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
}
}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js
new file mode 100644
index 000000000..b082db1d0
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js
@@ -0,0 +1,29 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../../../currency-display'
+import { ETH } from '../../../../constants/common'
+
+export default class CancelTransaction extends PureComponent {
+ static propTypes = {
+ value: PropTypes.string,
+ }
+
+ render () {
+ const { value } = this.props
+
+ return (
+ <div className="cancel-transaction-gas-fee">
+ <CurrencyDisplay
+ className="cancel-transaction-gas-fee__eth"
+ currency={ETH}
+ value={value}
+ numberOfDecimals={6}
+ />
+ <CurrencyDisplay
+ className="cancel-transaction-gas-fee__fiat"
+ value={value}
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js
new file mode 100644
index 000000000..1a9ae2e07
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js
@@ -0,0 +1 @@
+export { default } from './cancel-transaction-gas-fee.component'
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss
new file mode 100644
index 000000000..ce81dd448
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss
@@ -0,0 +1,17 @@
+.cancel-transaction-gas-fee {
+ background: #F1F4F9;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px;
+
+ &__eth {
+ font-size: 1.5rem;
+ font-weight: 500;
+ }
+
+ &__fiat {
+ font-size: .75rem;
+ }
+}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js
new file mode 100644
index 000000000..994c2a577
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component'
+import CurrencyDisplay from '../../../../currency-display'
+
+describe('CancelTransactionGasFee Component', () => {
+ it('should render', () => {
+ const wrapper = shallow(
+ <CancelTransactionGasFee
+ value="0x3b9aca00"
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(CurrencyDisplay).length, 2)
+ const ethDisplay = wrapper.find(CurrencyDisplay).at(0)
+ const fiatDisplay = wrapper.find(CurrencyDisplay).at(1)
+
+ assert.equal(ethDisplay.props().value, '0x3b9aca00')
+ assert.equal(ethDisplay.props().currency, 'ETH')
+ assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth')
+
+ assert.equal(fiatDisplay.props().value, '0x3b9aca00')
+ assert.equal(fiatDisplay.props().className, 'cancel-transaction-gas-fee__fiat')
+ })
+})
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js
new file mode 100644
index 000000000..8b00cb9b9
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js
@@ -0,0 +1,68 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Modal from '../../modal'
+import CancelTransactionGasFee from './cancel-transaction-gas-fee'
+import { SUBMITTED_STATUS } from '../../../constants/transactions'
+
+export default class CancelTransaction extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ createCancelTransaction: PropTypes.func,
+ hideModal: PropTypes.func,
+ showTransactionConfirmedModal: PropTypes.func,
+ transactionStatus: PropTypes.string,
+ newGasFee: PropTypes.string,
+ }
+
+ componentDidUpdate () {
+ const { transactionStatus, showTransactionConfirmedModal } = this.props
+
+ if (transactionStatus !== SUBMITTED_STATUS) {
+ showTransactionConfirmedModal()
+ return
+ }
+ }
+
+ handleSubmit = async () => {
+ const { createCancelTransaction, hideModal } = this.props
+
+ await createCancelTransaction()
+ hideModal()
+ }
+
+ handleCancel = () => {
+ this.props.hideModal()
+ }
+
+ render () {
+ const { t } = this.context
+ const { newGasFee } = this.props
+
+ return (
+ <Modal
+ headerText={t('attemptToCancel')}
+ onClose={this.handleCancel}
+ onSubmit={this.handleSubmit}
+ onCancel={this.handleCancel}
+ submitText={t('yesLetsTry')}
+ cancelText={t('nevermind')}
+ submitType="secondary"
+ >
+ <div>
+ <div className="cancel-transaction__title">
+ { t('cancellationGasFee') }
+ </div>
+ <div className="cancel-transaction__cancel-transaction-gas-fee-container">
+ <CancelTransactionGasFee value={newGasFee} />
+ </div>
+ <div className="cancel-transaction__description">
+ { t('attemptToCancelDescription') }
+ </div>
+ </div>
+ </Modal>
+ )
+ }
+}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js
new file mode 100644
index 000000000..eede8b1ee
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js
@@ -0,0 +1,62 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ethUtil from 'ethereumjs-util'
+import { multiplyCurrencies } from '../../../conversion-util'
+import withModalProps from '../../../higher-order-components/with-modal-props'
+import CancelTransaction from './cancel-transaction.component'
+import { showModal, createCancelTransaction } from '../../../actions'
+import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { metamask } = state
+ const { transactionId, originalGasPrice } = ownProps
+ const { selectedAddressTxList } = metamask
+ const transaction = selectedAddressTxList.find(({ id }) => id === transactionId)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ const defaultNewGasPrice = ethUtil.addHexPrefix(
+ multiplyCurrencies(originalGasPrice, 1.1, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ })
+ )
+
+ const newGasFee = getHexGasTotal({ gasPrice: defaultNewGasPrice, gasLimit: '0x5208' })
+
+ return {
+ transactionId,
+ transactionStatus,
+ originalGasPrice,
+ newGasFee,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ createCancelTransaction: txId => dispatch(createCancelTransaction(txId)),
+ showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { transactionId, ...restStateProps } = stateProps
+ const {
+ createCancelTransaction: dispatchCreateCancelTransaction,
+ ...restDispatchProps
+ } = dispatchProps
+
+ return {
+ ...restStateProps,
+ ...restDispatchProps,
+ ...ownProps,
+ createCancelTransaction: newGasPrice => {
+ return dispatchCreateCancelTransaction(transactionId, newGasPrice)
+ },
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps),
+)(CancelTransaction)
diff --git a/ui/app/components/modals/cancel-transaction/index.js b/ui/app/components/modals/cancel-transaction/index.js
new file mode 100644
index 000000000..7abc871ee
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/index.js
@@ -0,0 +1 @@
+export { default } from './cancel-transaction.container'
diff --git a/ui/app/components/modals/cancel-transaction/index.scss b/ui/app/components/modals/cancel-transaction/index.scss
new file mode 100644
index 000000000..62e8e36fd
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/index.scss
@@ -0,0 +1,18 @@
+@import './cancel-transaction-gas-fee/index';
+
+.cancel-transaction {
+ &__title {
+ font-weight: 500;
+ padding-bottom: 16px;
+ text-align: center;
+ }
+
+ &__description {
+ text-align: center;
+ font-size: .875rem;
+ }
+
+ &__cancel-transaction-gas-fee-container {
+ margin-bottom: 16px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js
new file mode 100644
index 000000000..858fb01a8
--- /dev/null
+++ b/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import CancelTransaction from '../cancel-transaction.component'
+import CancelTransactionGasFee from '../cancel-transaction-gas-fee'
+import Modal from '../../../modal'
+
+describe('CancelTransaction Component', () => {
+ const t = key => key
+
+ it('should render a CancelTransaction modal', () => {
+ const wrapper = shallow(
+ <CancelTransaction
+ newGasFee="0x1319718a5000"
+ />,
+ { context: { t }}
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(Modal).length, 1)
+ assert.equal(wrapper.find(CancelTransactionGasFee).length, 1)
+ assert.equal(wrapper.find(CancelTransactionGasFee).props().value, '0x1319718a5000')
+ assert.equal(wrapper.find('.cancel-transaction__title').text(), 'cancellationGasFee')
+ assert.equal(wrapper.find('.cancel-transaction__description').text(), 'attemptToCancelDescription')
+ })
+
+ it('should pass the correct props to the Modal component', async () => {
+ const createCancelTransactionSpy = sinon.stub().callsFake(() => Promise.resolve())
+ const hideModalSpy = sinon.spy()
+
+ const wrapper = shallow(
+ <CancelTransaction
+ defaultNewGasPrice="0x3b9aca00"
+ createCancelTransaction={createCancelTransactionSpy}
+ hideModal={hideModalSpy}
+ />,
+ { context: { t }}
+ )
+
+ assert.equal(wrapper.find(Modal).length, 1)
+ const modalProps = wrapper.find(Modal).props()
+
+ assert.equal(modalProps.headerText, 'attemptToCancel')
+ assert.equal(modalProps.submitText, 'yesLetsTry')
+ assert.equal(modalProps.cancelText, 'nevermind')
+
+ assert.equal(createCancelTransactionSpy.callCount, 0)
+ assert.equal(hideModalSpy.callCount, 0)
+ await modalProps.onSubmit()
+ assert.equal(createCancelTransactionSpy.callCount, 1)
+ assert.equal(hideModalSpy.callCount, 1)
+ modalProps.onCancel()
+ assert.equal(hideModalSpy.callCount, 2)
+ })
+})
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
index 5a9f0f289..195c55421 100644
--- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
@@ -1,11 +1,11 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import Button from '../../button'
+import Modal from '../../modal'
import { addressSummary } from '../../../util'
import Identicon from '../../identicon'
import genAccountLink from '../../../../lib/account-link'
-class ConfirmRemoveAccount extends Component {
+export default class ConfirmRemoveAccount extends Component {
static propTypes = {
hideModal: PropTypes.func.isRequired,
removeAccount: PropTypes.func.isRequired,
@@ -17,30 +17,34 @@ class ConfirmRemoveAccount extends Component {
t: PropTypes.func,
}
- handleRemove () {
+ handleRemove = () => {
this.props.removeAccount(this.props.identity.address)
.then(() => this.props.hideModal())
}
+ handleCancel = () => {
+ this.props.hideModal()
+ }
+
renderSelectedAccount () {
const { identity } = this.props
return (
- <div className="modal-container__account">
- <div className="modal-container__account__identicon">
+ <div className="confirm-remove-account__account">
+ <div className="confirm-remove-account__account__identicon">
<Identicon
- address={identity.address}
- diameter={32}
+ address={identity.address}
+ diameter={32}
/>
</div>
- <div className="modal-container__account__name">
- <span className="modal-container__account__label">Name</span>
- <span className="account_value">{identity.name}</span>
+ <div className="confirm-remove-account__account__name">
+ <span className="confirm-remove-account__account__label">Name</span>
+ <span className="account_value">{identity.name}</span>
</div>
- <div className="modal-container__account__address">
- <span className="modal-container__account__label">Public Address</span>
- <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
+ <div className="confirm-remove-account__account__address">
+ <span className="confirm-remove-account__account__label">Public Address</span>
+ <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
</div>
- <div className="modal-container__account__link">
+ <div className="confirm-remove-account__account__link">
<a
className=""
href={genAccountLink(identity.address, this.props.network)}
@@ -58,36 +62,28 @@ class ConfirmRemoveAccount extends Component {
const { t } = this.context
return (
- <div className="modal-container">
- <div className="modal-container__content">
- <div className="modal-container__title">
- { `${t('removeAccount')}` }?
- </div>
- { this.renderSelectedAccount() }
- <div className="modal-container__description">
+ <Modal
+ headerText={`${t('removeAccount')}?`}
+ onClose={this.handleCancel}
+ onSubmit={this.handleRemove}
+ onCancel={this.handleCancel}
+ submitText={t('remove')}
+ cancelText={t('nevermind')}
+ submitType="secondary"
+ >
+ <div>
+ { this.renderSelectedAccount() }
+ <div className="confirm-remove-account__description">
{ t('removeAccountDescription') }
- <a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a>
+ <a
+ className="confirm-remove-account__link"
+ rel="noopener noreferrer"
+ target="_blank" href="https://metamask.zendesk.com/hc/en-us/articles/360015289932">
+ { t('learnMore') }
+ </a>
</div>
</div>
- <div className="modal-container__footer">
- <Button
- type="default"
- className="modal-container__footer-button"
- onClick={() => this.props.hideModal()}
- >
- { t('nevermind') }
- </Button>
- <Button
- type="secondary"
- className="modal-container__footer-button"
- onClick={() => this.handleRemove()}
- >
- { t('remove') }
- </Button>
- </div>
- </div>
+ </Modal>
)
}
}
-
-export default ConfirmRemoveAccount
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
index 4b194c995..45c6654ab 100644
--- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
@@ -1,20 +1,22 @@
import { connect } from 'react-redux'
+import { compose } from 'recompose'
import ConfirmRemoveAccount from './confirm-remove-account.component'
-
-const { hideModal, removeAccount } = require('../../../actions')
+import withModalProps from '../../../higher-order-components/with-modal-props'
+import { removeAccount } from '../../../actions'
const mapStateToProps = state => {
return {
- identity: state.appState.modal.modalState.props.identity,
network: state.metamask.network,
}
}
const mapDispatchToProps = dispatch => {
return {
- hideModal: () => dispatch(hideModal()),
removeAccount: (address) => dispatch(removeAccount(address)),
}
}
-export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount)
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmRemoveAccount)
diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js
index 9763fbe05..ecb5f7790 100644
--- a/ui/app/components/modals/confirm-remove-account/index.js
+++ b/ui/app/components/modals/confirm-remove-account/index.js
@@ -1,2 +1 @@
-import ConfirmRemoveAccount from './confirm-remove-account.container'
-module.exports = ConfirmRemoveAccount
+export { default } from './confirm-remove-account.container'
diff --git a/ui/app/components/modals/confirm-remove-account/index.scss b/ui/app/components/modals/confirm-remove-account/index.scss
new file mode 100644
index 000000000..3be3a1967
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/index.scss
@@ -0,0 +1,58 @@
+.confirm-remove-account {
+ &__description {
+ text-align: center;
+ font-size: .875rem;
+ }
+
+ &__account {
+ border: 1px solid #b7b7b7;
+ border-radius: 4px;
+ padding: 10px;
+ display: flex;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ width: 100%;
+
+ &__identicon {
+ margin-right: 10px;
+ }
+
+ &__name,
+ &__address {
+ margin-right: 10px;
+ font-size: 14px;
+ }
+
+ &__name {
+ width: 100px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__label {
+ font-size: 11px;
+ display: block;
+ color: #9b9b9b;
+ }
+
+ &__link {
+ margin-top: 14px;
+
+ img {
+ width: 15px;
+ height: 15px;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ &__name {
+ width: 90px;
+ }
+ }
+ }
+
+ &__link {
+ color: #2f9ae0;
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js
index 14a4da62a..f1a4542ac 100644
--- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js
+++ b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js
@@ -1,8 +1,8 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
-import Button from '../../button'
+import Modal, { ModalContent } from '../../modal'
-class ConfirmResetAccount extends Component {
+export default class ConfirmResetAccount extends PureComponent {
static propTypes = {
hideModal: PropTypes.func.isRequired,
resetAccount: PropTypes.func.isRequired,
@@ -12,7 +12,7 @@ class ConfirmResetAccount extends Component {
t: PropTypes.func,
}
- handleReset () {
+ handleReset = () => {
this.props.resetAccount()
.then(() => this.props.hideModal())
}
@@ -21,34 +21,18 @@ class ConfirmResetAccount extends Component {
const { t } = this.context
return (
- <div className="modal-container">
- <div className="modal-container__content">
- <div className="modal-container__title">
- { `${t('resetAccount')}?` }
- </div>
- <div className="modal-container__description">
- { t('resetAccountDescription') }
- </div>
- </div>
- <div className="modal-container__footer">
- <Button
- type="default"
- className="modal-container__footer-button"
- onClick={() => this.props.hideModal()}
- >
- { t('nevermind') }
- </Button>
- <Button
- type="secondary"
- className="modal-container__footer-button"
- onClick={() => this.handleReset()}
- >
- { t('reset') }
- </Button>
- </div>
- </div>
+ <Modal
+ onSubmit={this.handleReset}
+ onCancel={() => this.props.hideModal()}
+ submitText={t('reset')}
+ cancelText={t('nevermind')}
+ submitType="secondary"
+ >
+ <ModalContent
+ title={`${t('resetAccount')}?`}
+ description={t('resetAccountDescription')}
+ />
+ </Modal>
)
}
}
-
-export default ConfirmResetAccount
diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js
index 9630a5593..c8a7b8478 100644
--- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js
+++ b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js
@@ -1,13 +1,16 @@
import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withModalProps from '../../../higher-order-components/with-modal-props'
import ConfirmResetAccount from './confirm-reset-account.component'
-
-const { hideModal, resetAccount } = require('../../../actions')
+import { resetAccount } from '../../../actions'
const mapDispatchToProps = dispatch => {
return {
- hideModal: () => dispatch(hideModal()),
resetAccount: () => dispatch(resetAccount()),
}
}
-export default connect(null, mapDispatchToProps)(ConfirmResetAccount)
+export default compose(
+ withModalProps,
+ connect(null, mapDispatchToProps)
+)(ConfirmResetAccount)
diff --git a/ui/app/components/modals/confirm-reset-account/index.js b/ui/app/components/modals/confirm-reset-account/index.js
index c812ffc55..ca4d9c5bf 100644
--- a/ui/app/components/modals/confirm-reset-account/index.js
+++ b/ui/app/components/modals/confirm-reset-account/index.js
@@ -1,2 +1 @@
-import ConfirmResetAccount from './confirm-reset-account.container'
-module.exports = ConfirmResetAccount
+export { default } from './confirm-reset-account.container'
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
index 0337c5413..3f526bd43 100644
--- a/ui/app/components/modals/customize-gas/customize-gas.component.js
+++ b/ui/app/components/modals/customize-gas/customize-gas.component.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import GasModalCard from '../../customize-gas-modal/gas-modal-card'
import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
+import Button from '../../button'
import {
getDecimalGasLimit,
@@ -116,21 +117,23 @@ export default class CustomizeGas extends Component {
{ t('revert') }
</div>
<div className="customize-gas__buttons">
- <button
- className="btn-default customize-gas__cancel"
+ <Button
+ type="default"
+ className="customize-gas__cancel"
onClick={() => hideModal()}
style={{ marginRight: '10px' }}
>
{ t('cancel') }
- </button>
- <button
- className="btn-primary customize-gas__save"
+ </Button>
+ <Button
+ type="primary"
+ className="customize-gas__save"
onClick={() => this.handleSave()}
style={{ marginRight: '10px' }}
disabled={!valid}
>
{ t('save') }
- </button>
+ </Button>
</div>
</div>
</div>
diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js
index 2daa7fa1d..09137d39a 100644
--- a/ui/app/components/modals/deposit-ether-modal.js
+++ b/ui/app/components/modals/deposit-ether-modal.js
@@ -7,6 +7,8 @@ const actions = require('../../actions')
const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
const ShapeshiftForm = require('../shapeshift-form')
+import Button from '../button'
+
let DIRECT_DEPOSIT_ROW_TITLE
let DIRECT_DEPOSIT_ROW_TEXT
let COINBASE_ROW_TITLE
@@ -109,7 +111,10 @@ DepositEtherModal.prototype.renderRow = function ({
]),
!hideButton && h('div.deposit-ether-modal__buy-row__button', [
- h('button.btn-primary.btn--large.deposit-ether-modal__deposit-button', {
+ h(Button, {
+ type: 'primary',
+ className: 'deposit-ether-modal__deposit-button',
+ large: true,
onClick: onButtonClick,
}, [buttonLabel]),
]),
diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js
index 80ece425f..d3e3c9a56 100644
--- a/ui/app/components/modals/export-private-key-modal.js
+++ b/ui/app/components/modals/export-private-key-modal.js
@@ -1,3 +1,4 @@
+const log = require('loglevel')
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
@@ -10,20 +11,35 @@ const { getSelectedIdentity } = require('../../selectors')
const ReadOnlyInput = require('../readonly-input')
const copyToClipboard = require('copy-to-clipboard')
const { checksumAddress } = require('../../util')
-
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- privateKey: state.appState.accountDetail.privateKey,
- network: state.metamask.network,
- selectedIdentity: getSelectedIdentity(state),
- previousModalState: state.appState.modal.previousModalState.name,
+import Button from '../button'
+
+function mapStateToPropsFactory () {
+ let selectedIdentity = null
+ return function mapStateToProps (state) {
+ // We should **not** change the identity displayed here even if it changes from underneath us.
+ // If we do, we will be showing the user one private key and a **different** address and name.
+ // Note that the selected identity **will** change from underneath us when we unlock the keyring
+ // which is the expected behavior that we are side-stepping.
+ selectedIdentity = selectedIdentity || getSelectedIdentity(state)
+ return {
+ warning: state.appState.warning,
+ privateKey: state.appState.accountDetail.privateKey,
+ network: state.metamask.network,
+ selectedIdentity,
+ previousModalState: state.appState.modal.previousModalState.name,
+ }
}
}
function mapDispatchToProps (dispatch) {
return {
- exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)),
+ exportAccount: (password, address) => {
+ return dispatch(actions.exportAccount(password, address))
+ .then((res) => {
+ dispatch(actions.hideWarning())
+ return res
+ })
+ },
showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
hideModal: () => dispatch(actions.hideModal()),
}
@@ -36,6 +52,7 @@ function ExportPrivateKeyModal () {
this.state = {
password: '',
privateKey: null,
+ showWarning: true,
}
}
@@ -43,14 +60,18 @@ ExportPrivateKeyModal.contextTypes = {
t: PropTypes.func,
}
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal)
+module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
const { exportAccount } = this.props
exportAccount(password, address)
- .then(privateKey => this.setState({ privateKey }))
+ .then(privateKey => this.setState({
+ privateKey,
+ showWarning: false,
+ }))
+ .catch((e) => log.error(e))
}
ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
@@ -77,24 +98,31 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
})
}
-ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, label) {
- return h('button', {
- className,
- onClick,
- }, label)
-}
-
ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
return h('div.export-private-key-buttons', {}, [
- !privateKey && this.renderButton(
- 'btn-default btn--large export-private-key__button export-private-key__button--cancel',
- () => hideModal(),
- 'Cancel'
- ),
+ !privateKey && h(Button, {
+ type: 'default',
+ large: true,
+ className: 'export-private-key__button export-private-key__button--cancel',
+ onClick: () => hideModal(),
+ }, this.context.t('cancel')),
(privateKey
- ? this.renderButton('btn-primary btn--large export-private-key__button', () => hideModal(), this.context.t('done'))
- : this.renderButton('btn-primary btn--large export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm'))
+ ? (
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'export-private-key__button',
+ onClick: () => hideModal(),
+ }, this.context.t('done'))
+ ) : (
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'export-private-key__button',
+ onClick: () => this.exportAccountAndGetPrivateKey(this.state.password, address),
+ }, this.context.t('confirm'))
+ )
),
])
@@ -110,9 +138,13 @@ ExportPrivateKeyModal.prototype.render = function () {
} = this.props
const { name, address } = selectedIdentity
- const { privateKey } = this.state
+ const {
+ privateKey,
+ showWarning,
+ } = this.state
return h(AccountModalContainer, {
+ selectedIdentity,
showBackButton: previousModalState === 'ACCOUNT_DETAILS',
backButtonAction: () => showAccountDetailModal(),
}, [
@@ -134,7 +166,7 @@ ExportPrivateKeyModal.prototype.render = function () {
this.renderPasswordInput(privateKey),
- !warning ? null : h('span.private-key-password-error', warning),
+ showWarning && warning ? h('span.private-key-password-error', warning) : null,
]),
h('div.private-key-password-warning', this.context.t('privateKeyWarning')),
diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js
index 1518fa9a0..fb38516d3 100644
--- a/ui/app/components/modals/hide-token-confirmation-modal.js
+++ b/ui/app/components/modals/hide-token-confirmation-modal.js
@@ -10,6 +10,7 @@ function mapStateToProps (state) {
return {
network: state.metamask.network,
token: state.appState.modal.modalState.props.token,
+ assetImages: state.metamask.assetImages,
}
}
@@ -40,8 +41,9 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmat
HideTokenConfirmationModal.prototype.render = function () {
- const { token, network, hideToken, hideModal } = this.props
+ const { token, network, hideToken, hideModal, assetImages } = this.props
const { symbol, address } = token
+ const image = assetImages[address]
return h('div.hide-token-confirmation', {}, [
h('div.hide-token-confirmation__container', {
@@ -55,6 +57,7 @@ HideTokenConfirmationModal.prototype.render = function () {
diameter: 45,
address,
network,
+ image,
}),
h('div.hide-token-confirmation__symbol', {}, symbol),
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
index 0acccf172..45453a582 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -1,108 +1,9 @@
-@import './customize-gas/index';
-
-@import './qr-scanner/index';
-
-.modal-container {
- width: 100%;
- height: 100%;
- background-color: #fff;
- display: flex;
- flex-flow: column;
- border-radius: 8px;
-
- &__title {
- font-size: 1.5rem;
- font-weight: 500;
- padding: 16px 0;
- text-align: center;
- }
-
- &__description {
- text-align: center;
- font-size: .875rem;
- }
-
- &__account {
- border: 1px solid #b7b7b7;
- border-radius: 4px;
- padding: 10px;
- display: flex;
- margin-top: 10px;
- margin-bottom: 20px;
- width: 100%;
-
- &__identicon {
- margin-right: 10px;
- }
-
- &__name,
- &__address {
- margin-right: 10px;
- font-size: 14px;
- }
+@import './cancel-transaction/index';
- &__name {
- width: 100px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+@import './confirm-remove-account/index';
- &__label {
- font-size: 11px;
- display: block;
- color: #9b9b9b;
- }
-
- &__link {
- margin-top: 14px;
-
- img {
- width: 15px;
- height: 15px;
- }
- }
-
- @media screen and (max-width: 575px) {
- &__name {
- width: 90px;
- }
- }
- }
-
- &__link {
- color: #2f9ae0;
- }
-
- &__content {
- overflow-y: auto;
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 32px;
-
- @media screen and (max-width: 575px) {
- justify-content: center;
- padding: 28px 20px;
- }
- }
-
- &__footer {
- display: flex;
- flex-flow: row;
- justify-content: center;
- border-top: 1px solid #d2d8dd;
- padding: 16px;
- flex: 0 0 auto;
+@import './customize-gas/index';
- &-button {
- min-width: 0;
- margin-right: 16px;
+@import './qr-scanner/index';
- &:last-of-type {
- margin-right: 0;
- }
- }
- }
-}
+@import './transaction-confirmed/index';
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 5dda50e52..15ca9deaa 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -19,14 +19,16 @@ const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
-const ConfirmResetAccount = require('./confirm-reset-account')
-const ConfirmRemoveAccount = require('./confirm-remove-account')
const QRScanner = require('./qr-scanner')
-const TransactionConfirmed = require('./transaction-confirmed')
-const WelcomeBeta = require('./welcome-beta')
-const Notification = require('./notification')
+import ConfirmRemoveAccount from './confirm-remove-account'
+import ConfirmResetAccount from './confirm-reset-account'
+import TransactionConfirmed from './transaction-confirmed'
import ConfirmCustomizeGasModal from './customize-gas'
+import CancelTransaction from './cancel-transaction'
+import WelcomeBeta from './welcome-beta'
+import TransactionDetails from './transaction-details'
+import RejectTransactions from './reject-transactions'
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
@@ -199,11 +201,7 @@ const MODALS = {
},
BETA_UI_NOTIFICATION_MODAL: {
- contents: [
- h(Notification, [
- h(WelcomeBeta),
- ]),
- ],
+ contents: h(WelcomeBeta),
mobileModalStyle: {
...modalContainerMobileStyle,
},
@@ -307,9 +305,7 @@ const MODALS = {
},
CONFIRM_CUSTOMIZE_GAS: {
- contents: [
- h(ConfirmCustomizeGasModal),
- ],
+ contents: h(ConfirmCustomizeGasModal),
mobileModalStyle: {
width: '100vw',
height: '100vh',
@@ -332,11 +328,7 @@ const MODALS = {
TRANSACTION_CONFIRMED: {
disableBackdropClick: true,
- contents: [
- h(Notification, [
- h(TransactionConfirmed),
- ]),
- ],
+ contents: h(TransactionConfirmed),
mobileModalStyle: {
...modalContainerMobileStyle,
},
@@ -347,6 +339,7 @@ const MODALS = {
borderRadius: '8px',
},
},
+
QR_SCANNER: {
contents: h(QRScanner),
mobileModalStyle: {
@@ -360,6 +353,45 @@ const MODALS = {
},
},
+ CANCEL_TRANSACTION: {
+ contents: h(CancelTransaction),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ TRANSACTION_DETAILS: {
+ contents: h(TransactionDetails),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ REJECT_TRANSACTIONS: {
+ contents: h(RejectTransactions),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
DEFAULT: {
contents: [],
mobileModalStyle: {},
diff --git a/ui/app/components/modals/notification/index.js b/ui/app/components/modals/notification/index.js
deleted file mode 100644
index d60a3129b..000000000
--- a/ui/app/components/modals/notification/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import Notification from './notification.container'
-module.exports = Notification
diff --git a/ui/app/components/modals/notification/notification.component.js b/ui/app/components/modals/notification/notification.component.js
deleted file mode 100644
index 1af2f3ca8..000000000
--- a/ui/app/components/modals/notification/notification.component.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../button'
-
-const Notification = (props, context) => {
- return (
- <div className="modal-container">
- { props.children }
- <div className="modal-container__footer">
- <Button
- type="primary"
- onClick={() => props.onHide()}
- >
- { context.t('ok') }
- </Button>
- </div>
- </div>
- )
-}
-
-Notification.propTypes = {
- onHide: PropTypes.func.isRequired,
- children: PropTypes.element,
-}
-
-Notification.contextTypes = {
- t: PropTypes.func,
-}
-
-export default Notification
diff --git a/ui/app/components/modals/notification/notification.container.js b/ui/app/components/modals/notification/notification.container.js
deleted file mode 100644
index 5b98714da..000000000
--- a/ui/app/components/modals/notification/notification.container.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { connect } from 'react-redux'
-import Notification from './notification.component'
-
-const { hideModal } = require('../../../actions')
-
-const mapStateToProps = state => {
- const { appState: { modal: { modalState: { props } } } } = state
- const { onHide } = props
- return {
- onHide,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- hideModal: () => dispatch(hideModal()),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { onHide, ...otherStateProps } = stateProps
- const { hideModal, ...otherDispatchProps } = dispatchProps
-
- return {
- ...otherStateProps,
- ...otherDispatchProps,
- ...ownProps,
- onHide: () => {
- hideModal()
-
- if (onHide && typeof onHide === 'function') {
- onHide()
- }
- },
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notification)
diff --git a/ui/app/components/modals/reject-transactions/index.js b/ui/app/components/modals/reject-transactions/index.js
new file mode 100644
index 000000000..fcdc372b6
--- /dev/null
+++ b/ui/app/components/modals/reject-transactions/index.js
@@ -0,0 +1 @@
+export { default } from './reject-transactions.container'
diff --git a/ui/app/components/modals/reject-transactions/index.scss b/ui/app/components/modals/reject-transactions/index.scss
new file mode 100644
index 000000000..753466883
--- /dev/null
+++ b/ui/app/components/modals/reject-transactions/index.scss
@@ -0,0 +1,6 @@
+.reject-transactions {
+ &__description {
+ text-align: center;
+ font-size: .875rem;
+ }
+}
diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.component.js b/ui/app/components/modals/reject-transactions/reject-transactions.component.js
new file mode 100644
index 000000000..60b259bdc
--- /dev/null
+++ b/ui/app/components/modals/reject-transactions/reject-transactions.component.js
@@ -0,0 +1,45 @@
+import PropTypes from 'prop-types'
+import React, { PureComponent } from 'react'
+import Modal from '../../modal'
+
+export default class RejectTransactionsModal extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func.isRequired,
+ }
+
+ static propTypes = {
+ onSubmit: PropTypes.func.isRequired,
+ hideModal: PropTypes.func.isRequired,
+ unapprovedTxCount: PropTypes.number.isRequired,
+ }
+
+ onSubmit = async () => {
+ const { onSubmit, hideModal } = this.props
+
+ await onSubmit()
+ hideModal()
+ }
+
+ render () {
+ const { t } = this.context
+ const { hideModal, unapprovedTxCount } = this.props
+
+ return (
+ <Modal
+ headerText={t('rejectTxsN', [unapprovedTxCount])}
+ onClose={hideModal}
+ onSubmit={this.onSubmit}
+ onCancel={hideModal}
+ submitText={t('rejectAll')}
+ cancelText={t('cancel')}
+ submitType="secondary"
+ >
+ <div>
+ <div className="reject-transactions__description">
+ { t('rejectTxsDescription', [unapprovedTxCount]) }
+ </div>
+ </div>
+ </Modal>
+ )
+ }
+}
diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/modals/reject-transactions/reject-transactions.container.js
new file mode 100644
index 000000000..81e98d3ff
--- /dev/null
+++ b/ui/app/components/modals/reject-transactions/reject-transactions.container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import RejectTransactionsModal from './reject-transactions.component'
+import withModalProps from '../../../higher-order-components/with-modal-props'
+
+const mapStateToProps = (state, ownProps) => {
+ const { unapprovedTxCount } = ownProps
+
+ return {
+ unapprovedTxCount,
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps),
+)(RejectTransactionsModal)
diff --git a/ui/app/components/modals/transaction-confirmed/index.js b/ui/app/components/modals/transaction-confirmed/index.js
index cee8da7f8..7776b969e 100644
--- a/ui/app/components/modals/transaction-confirmed/index.js
+++ b/ui/app/components/modals/transaction-confirmed/index.js
@@ -1,2 +1 @@
-import TransactionConfirmed from './transaction-confirmed.component'
-module.exports = TransactionConfirmed
+export { default } from './transaction-confirmed.container'
diff --git a/ui/app/components/modals/transaction-confirmed/index.scss b/ui/app/components/modals/transaction-confirmed/index.scss
new file mode 100644
index 000000000..c97371fb6
--- /dev/null
+++ b/ui/app/components/modals/transaction-confirmed/index.scss
@@ -0,0 +1,22 @@
+.transaction-confirmed {
+ &__title {
+ font-size: 1.5rem;
+ font-weight: 500;
+ padding: 16px 0;
+ text-align: center;
+ }
+
+ &__description {
+ text-align: center;
+ font-size: .875rem;
+ }
+
+ &__content {
+ overflow-y: auto;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 16px;
+ }
+}
diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js
index c1c8a2976..0a98eb1a1 100644
--- a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js
+++ b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js
@@ -1,24 +1,45 @@
-import React from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
+import Modal from '../../modal'
-const TransactionConfirmed = (props, context) => {
- const { t } = context
+export default class TransactionConfirmed extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
- return (
- <div className="modal-container__content">
- <img src="images/check-icon.svg" />
- <div className="modal-container__title">
- { `${t('confirmed')}!` }
- </div>
- <div className="modal-container__description">
- { t('initialTransactionConfirmed') }
- </div>
- </div>
- )
-}
+ static propTypes = {
+ onSubmit: PropTypes.func,
+ hideModal: PropTypes.func,
+ }
-TransactionConfirmed.contextTypes = {
- t: PropTypes.func,
-}
+ handleSubmit = () => {
+ const { hideModal, onSubmit } = this.props
+
+ hideModal()
-export default TransactionConfirmed
+ if (onSubmit && typeof onSubmit === 'function') {
+ onSubmit()
+ }
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <Modal
+ onSubmit={this.handleSubmit}
+ submitText={t('ok')}
+ >
+ <div className="transaction-confirmed__content">
+ <img src="images/check-icon.svg" />
+ <div className="transaction-confirmed__title">
+ { `${t('confirmed')}!` }
+ </div>
+ <div className="transaction-confirmed__description">
+ { t('initialTransactionConfirmed') }
+ </div>
+ </div>
+ </Modal>
+ )
+ }
+}
diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js
new file mode 100644
index 000000000..d4e39681a
--- /dev/null
+++ b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js
@@ -0,0 +1,4 @@
+import TransactionConfirmed from './transaction-confirmed.component'
+import withModalProps from '../../../higher-order-components/with-modal-props'
+
+export default withModalProps(TransactionConfirmed)
diff --git a/ui/app/components/modals/transaction-details/index.js b/ui/app/components/modals/transaction-details/index.js
new file mode 100644
index 000000000..1fc42c662
--- /dev/null
+++ b/ui/app/components/modals/transaction-details/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-details.container'
diff --git a/ui/app/components/modals/transaction-details/transaction-details.component.js b/ui/app/components/modals/transaction-details/transaction-details.component.js
new file mode 100644
index 000000000..f2fec3409
--- /dev/null
+++ b/ui/app/components/modals/transaction-details/transaction-details.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Modal from '../../modal'
+import TransactionListItemDetails from '../../transaction-list-item-details'
+import { hexToDecimal } from '../../../helpers/conversions.util'
+
+export default class TransactionConfirmed extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideModal: PropTypes.func,
+ transaction: PropTypes.object,
+ onRetry: PropTypes.func,
+ showRetry: PropTypes.bool,
+ onCancel: PropTypes.func,
+ showCancel: PropTypes.bool,
+ }
+
+ handleSubmit = () => {
+ this.props.hideModal()
+ }
+
+ handleRetry = () => {
+ const { onRetry, hideModal } = this.props
+
+ Promise.resolve(onRetry()).then(() => hideModal())
+ }
+
+ render () {
+ const { t } = this.context
+ const { transaction, showRetry, onCancel, showCancel } = this.props
+ const { txParams: { nonce } = {} } = transaction
+ const decimalNonce = nonce && hexToDecimal(nonce)
+
+ return (
+ <Modal
+ onSubmit={this.handleSubmit}
+ onClose={this.handleSubmit}
+ submitText={t('ok')}
+ headerText={t('transactionWithNonce', [`#${decimalNonce}`])}
+ >
+ <TransactionListItemDetails
+ transaction={transaction}
+ onRetry={this.handleRetry}
+ showRetry={showRetry}
+ onCancel={() => onCancel()}
+ showCancel={showCancel}
+ />
+ </Modal>
+ )
+ }
+}
diff --git a/ui/app/components/modals/transaction-details/transaction-details.container.js b/ui/app/components/modals/transaction-details/transaction-details.container.js
new file mode 100644
index 000000000..f212920bb
--- /dev/null
+++ b/ui/app/components/modals/transaction-details/transaction-details.container.js
@@ -0,0 +1,4 @@
+import TransactionDetails from './transaction-details.component'
+import withModalProps from '../../../higher-order-components/with-modal-props'
+
+export default withModalProps(TransactionDetails)
diff --git a/ui/app/components/modals/welcome-beta/index.js b/ui/app/components/modals/welcome-beta/index.js
index 515c9cdaf..49e45b9d7 100644
--- a/ui/app/components/modals/welcome-beta/index.js
+++ b/ui/app/components/modals/welcome-beta/index.js
@@ -1,2 +1 @@
-import WelcomeBeta from './welcome-beta.component'
-module.exports = WelcomeBeta
+export { default } from './welcome-beta.container'
diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.component.js b/ui/app/components/modals/welcome-beta/welcome-beta.component.js
index 61571723a..ef1799164 100644
--- a/ui/app/components/modals/welcome-beta/welcome-beta.component.js
+++ b/ui/app/components/modals/welcome-beta/welcome-beta.component.js
@@ -1,18 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
+import Modal, { ModalContent } from '../../modal'
const TransactionConfirmed = (props, context) => {
const { t } = context
+ const { hideModal } = props
return (
- <div className="modal-container__content">
- <div className="modal-container__title">
- { `${t('uiWelcome')}` }
- </div>
- <div className="modal-container__description">
- { t('uiWelcomeMessage') }
- </div>
- </div>
+ <Modal
+ onSubmit={() => hideModal()}
+ submitText={t('ok')}
+ >
+ <ModalContent
+ title={t('uiWelcome')}
+ description={t('uiWelcomeMessage')}
+ />
+ </Modal>
)
}
@@ -20,4 +23,8 @@ TransactionConfirmed.contextTypes = {
t: PropTypes.func,
}
+TransactionConfirmed.propTypes = {
+ hideModal: PropTypes.func,
+}
+
export default TransactionConfirmed
diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.container.js b/ui/app/components/modals/welcome-beta/welcome-beta.container.js
new file mode 100644
index 000000000..c5123ad47
--- /dev/null
+++ b/ui/app/components/modals/welcome-beta/welcome-beta.container.js
@@ -0,0 +1,4 @@
+import WelcomeBeta from './welcome-beta.component'
+import withModalProps from '../../../higher-order-components/with-modal-props'
+
+export default withModalProps(WelcomeBeta)
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss
index 06c3ef709..6742e3082 100644
--- a/ui/app/components/page-container/index.scss
+++ b/ui/app/components/page-container/index.scss
@@ -43,16 +43,39 @@
&__footer {
display: flex;
- flex-flow: row;
+ flex-flow: column;
justify-content: center;
border-top: 1px solid $geyser;
- padding: 16px;
flex: 0 0 auto;
.btn-default,
.btn-confirm {
font-size: 1rem;
}
+
+ header {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ padding: 16px;
+ flex: 0 0 auto;
+ }
+
+ footer {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-around;
+ padding: 0 16px 16px;
+ flex: 0 0 auto;
+
+ a, a:hover {
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ color: #2f9ae0;
+ }
+ }
}
&__footer-button {
@@ -109,7 +132,7 @@
&--selected {
color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
+ border-bottom: 2px solid $curious-blue;
}
}
@@ -182,5 +205,7 @@
max-height: 82vh;
min-height: 570px;
flex: 0 0 auto;
+ margin-right: auto;
+ margin-left: auto;
}
}
diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
index 3d15df294..773fe1f56 100644
--- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
+++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
@@ -5,6 +5,7 @@ import Button from '../../button'
export default class PageContainerFooter extends Component {
static propTypes = {
+ children: PropTypes.node,
onCancel: PropTypes.func,
cancelText: PropTypes.string,
onSubmit: PropTypes.func,
@@ -19,6 +20,7 @@ export default class PageContainerFooter extends Component {
render () {
const {
+ children,
onCancel,
cancelText,
onSubmit,
@@ -30,24 +32,32 @@ export default class PageContainerFooter extends Component {
return (
<div className="page-container__footer">
- <Button
- type="default"
- large
- className="page-container__footer-button"
- onClick={e => onCancel(e)}
- >
- { cancelText || this.context.t('cancel') }
- </Button>
-
- <Button
- type={submitButtonType || 'primary'}
- large
- className="page-container__footer-button"
- disabled={disabled}
- onClick={e => onSubmit(e)}
- >
- { submitText || this.context.t('next') }
- </Button>
+ <header>
+ <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={e => onCancel(e)}
+ >
+ { cancelText || this.context.t('cancel') }
+ </Button>
+
+ <Button
+ type={submitButtonType || 'primary'}
+ large
+ className="page-container__footer-button"
+ disabled={disabled}
+ onClick={e => onSubmit(e)}
+ >
+ { submitText || this.context.t('next') }
+ </Button>
+ </header>
+
+ {children && (
+ <footer>
+ {children}
+ </footer>
+ )}
</div>
)
diff --git a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
index e69de29bb..64efabab0 100644
--- a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
+++ b/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
@@ -0,0 +1,79 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import Button from '../../../button'
+import PageFooter from '../page-container-footer.component'
+
+describe('Page Footer', () => {
+ let wrapper
+ const onCancel = sinon.spy()
+ const onSubmit = sinon.spy()
+
+ beforeEach(() => {
+ wrapper = shallow(<PageFooter
+ onCancel = {onCancel}
+ onSubmit = {onSubmit}
+ cancelText = {'Cancel'}
+ submitText = {'Submit'}
+ disabled = {false}
+ submitButtonType = {'Test Type'}
+ />)
+ })
+
+ it('renders page container footer', () => {
+ assert.equal(wrapper.find('.page-container__footer').length, 1)
+ })
+
+ it('should render a footer inside page-container__footer when given children', () => {
+ const wrapper = shallow(
+ <PageFooter>
+ <div>Works</div>
+ </PageFooter>,
+ { context: { t: sinon.spy((k) => `[${k}]`) } }
+ )
+
+ assert.equal(wrapper.find('.page-container__footer footer').length, 1)
+ })
+
+ it('renders two button components', () => {
+ assert.equal(wrapper.find(Button).length, 2)
+ })
+
+ describe('Cancel Button', () => {
+
+ it('has button type of default', () => {
+ assert.equal(wrapper.find('.page-container__footer-button').first().prop('type'), 'default')
+ })
+
+ it('has children text of Cancel', () => {
+ assert.equal(wrapper.find('.page-container__footer-button').first().prop('children'), 'Cancel')
+ })
+
+ it('should call cancel when click is simulated', () => {
+ wrapper.find('.page-container__footer-button').first().prop('onClick')()
+ assert.equal(onCancel.callCount, 1)
+ })
+
+ })
+
+ describe('Submit Button', () => {
+
+ it('assigns button type based on props', () => {
+ assert.equal(wrapper.find('.page-container__footer-button').last().prop('type'), 'Test Type')
+ })
+
+ it('has disabled prop', () => {
+ assert.equal(wrapper.find('.page-container__footer-button').last().prop('disabled'), false)
+ })
+
+ it('has children text when submitText prop exists', () => {
+ assert.equal(wrapper.find('.page-container__footer-button').last().prop('children'), 'Submit')
+ })
+
+ it('should call submit when click is simulated', () => {
+ wrapper.find('.page-container__footer-button').last().prop('onClick')()
+ assert.equal(onSubmit.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js
index 5a5de1e5a..a8458604e 100644
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ b/ui/app/components/page-container/page-container-header/page-container-header.component.js
@@ -1,8 +1,8 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
+import classnames from 'classnames'
export default class PageContainerHeader extends Component {
-
static propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
@@ -11,8 +11,18 @@ export default class PageContainerHeader extends Component {
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
- children: PropTypes.node,
- };
+ tabs: PropTypes.node,
+ }
+
+ renderTabs () {
+ const { tabs } = this.props
+
+ return tabs && (
+ <ul className="page-container__tabs">
+ { tabs }
+ </ul>
+ )
+ }
renderHeaderRow () {
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
@@ -31,15 +41,18 @@ export default class PageContainerHeader extends Component {
}
render () {
- const { title, subtitle, onClose, children } = this.props
+ const { title, subtitle, onClose, tabs } = this.props
return (
- <div className="page-container__header">
+ <div className={
+ classnames(
+ 'page-container__header',
+ { 'page-container__header--no-padding-bottom': Boolean(tabs) }
+ )
+ }>
{ this.renderHeaderRow() }
- { children }
-
{
title && <div className="page-container__title">
{ title }
@@ -59,6 +72,7 @@ export default class PageContainerHeader extends Component {
/>
}
+ { this.renderTabs() }
</div>
)
}
diff --git a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
index e69de29bb..59304b2bd 100644
--- a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
+++ b/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
@@ -0,0 +1,82 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import PageContainerHeader from '../page-container-header.component'
+
+describe('Page Container Header', () => {
+ let wrapper, style, onBackButtonClick, onClose
+
+ beforeEach(() => {
+ style = {test: 'style'}
+ onBackButtonClick = sinon.spy()
+ onClose = sinon.spy()
+
+ wrapper = shallow(<PageContainerHeader
+ showBackButton = {true}
+ onBackButtonClick = {onBackButtonClick}
+ backButtonStyles = {style}
+ title = {'Test Title'}
+ subtitle = {'Test Subtitle'}
+ tabs = {'Test Tab'}
+ onClose = {onClose}
+ />)
+ })
+
+ describe('Render Header Row', () => {
+
+ it('renders back button', () => {
+ assert.equal(wrapper.find('.page-container__back-button').length, 1)
+ assert.equal(wrapper.find('.page-container__back-button').text(), 'Back')
+ })
+
+ it('ensures style prop', () => {
+ assert.equal(wrapper.find('.page-container__back-button').props().style, style)
+ })
+
+ it('should call back button when click is simulated', () => {
+ wrapper.find('.page-container__back-button').prop('onClick')()
+ assert.equal(onBackButtonClick.callCount, 1)
+ })
+ })
+
+ describe('Render', () => {
+ let header, headerRow, pageTitle, pageSubtitle, pageClose, pageTab
+
+ beforeEach(() => {
+ header = wrapper.find('.page-container__header--no-padding-bottom')
+ headerRow = wrapper.find('.page-container__header-row')
+ pageTitle = wrapper.find('.page-container__title')
+ pageSubtitle = wrapper.find('.page-container__subtitle')
+ pageClose = wrapper.find('.page-container__header-close')
+ pageTab = wrapper.find('.page-container__tabs')
+ })
+
+ it('renders page container', () => {
+ assert.equal(header.length, 1)
+ assert.equal(headerRow.length, 1)
+ assert.equal(pageTitle.length, 1)
+ assert.equal(pageSubtitle.length, 1)
+ assert.equal(pageClose.length, 1)
+ assert.equal(pageTab.length, 1)
+ })
+
+ it('renders title', () => {
+ assert.equal(pageTitle.text(), 'Test Title')
+ })
+
+ it('renders subtitle', () => {
+ assert.equal(pageSubtitle.text(), 'Test Subtitle')
+ })
+
+ it('renders tabs', () => {
+ assert.equal(pageTab.text(), 'Test Tab')
+ })
+
+ it('should call close when click is simulated', () => {
+ pageClose.prop('onClick')()
+ assert.equal(onClose.callCount, 1)
+ })
+ })
+
+})
diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js
index 9bfb99ade..3a2274a29 100644
--- a/ui/app/components/page-container/page-container.component.js
+++ b/ui/app/components/page-container/page-container.component.js
@@ -1,30 +1,82 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer'
-export default class PageContainer extends Component {
-
+export default class PageContainer extends PureComponent {
static propTypes = {
// PageContainerHeader props
- title: PropTypes.string.isRequired,
- subtitle: PropTypes.string,
+ backButtonString: PropTypes.string,
+ backButtonStyles: PropTypes.object,
+ onBackButtonClick: PropTypes.func,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
- onBackButtonClick: PropTypes.func,
- backButtonStyles: PropTypes.object,
- backButtonString: PropTypes.string,
+ subtitle: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ // Tabs-related props
+ defaultActiveTabIndex: PropTypes.number,
+ tabsComponent: PropTypes.node,
// Content props
- ContentComponent: PropTypes.func,
- contentComponentProps: PropTypes.object,
+ contentComponent: PropTypes.node,
// PageContainerFooter props
- onCancel: PropTypes.func,
cancelText: PropTypes.string,
+ disabled: PropTypes.bool,
+ onCancel: PropTypes.func,
onSubmit: PropTypes.func,
submitText: PropTypes.string,
- disabled: PropTypes.bool,
- };
+ }
+
+ state = {
+ activeTabIndex: this.props.defaultActiveTabIndex || 0,
+ }
+
+ handleTabClick (activeTabIndex) {
+ this.setState({ activeTabIndex })
+ }
+
+ renderTabs () {
+ const { tabsComponent } = this.props
+
+ if (!tabsComponent) {
+ return
+ }
+
+ const numberOfTabs = React.Children.count(tabsComponent.props.children)
+
+ return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
+ return child && React.cloneElement(child, {
+ onClick: index => this.handleTabClick(index),
+ tabIndex,
+ isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
+ key: tabIndex,
+ className: 'page-container__tab',
+ activeClassName: 'page-container__tab--selected',
+ })
+ })
+ }
+
+ renderActiveTabContent () {
+ const { tabsComponent } = this.props
+ const { children } = tabsComponent.props
+ const { activeTabIndex } = this.state
+
+ return children[activeTabIndex]
+ ? children[activeTabIndex].props.children
+ : children.props.children
+ }
+
+ renderContent () {
+ const { contentComponent, tabsComponent } = this.props
+
+ if (contentComponent) {
+ return contentComponent
+ } else if (tabsComponent) {
+ return this.renderActiveTabContent()
+ } else {
+ return null
+ }
+ }
render () {
const {
@@ -35,8 +87,6 @@ export default class PageContainer extends Component {
onBackButtonClick,
backButtonStyles,
backButtonString,
- ContentComponent,
- contentComponentProps,
onCancel,
cancelText,
onSubmit,
@@ -54,9 +104,10 @@ export default class PageContainer extends Component {
onBackButtonClick={onBackButtonClick}
backButtonStyles={backButtonStyles}
backButtonString={backButtonString}
+ tabs={this.renderTabs()}
/>
<div className="page-container__content">
- <ContentComponent { ...contentComponentProps } />
+ { this.renderContent() }
</div>
<PageContainerFooter
onCancel={onCancel}
@@ -68,5 +119,4 @@ export default class PageContainer extends Component {
</div>
)
}
-
}
diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js
index bcb93d401..3612e676c 100644
--- a/ui/app/components/pages/add-token/add-token.component.js
+++ b/ui/app/components/pages/add-token/add-token.component.js
@@ -1,14 +1,14 @@
import React, { Component } from 'react'
-import classnames from 'classnames'
import PropTypes from 'prop-types'
import ethUtil from 'ethereumjs-util'
import { checkExistingAddresses } from './util'
import { tokenInfoGetter } from '../../../token-util'
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
-import Button from '../../button'
import TextField from '../../text-field'
import TokenList from './token-list'
import TokenSearch from './token-search'
+import PageContainer from '../../page-container'
+import { Tabs, Tab } from '../../tabs'
const emptyAddr = '0x0000000000000000000000000000000000000000'
const SEARCH_TAB = 'SEARCH'
@@ -206,7 +206,7 @@ class AddToken extends Component {
const validDecimals = customDecimals !== null &&
customDecimals !== '' &&
customDecimals >= 0 &&
- customDecimals < 36
+ customDecimals <= 36
let customDecimalsError = null
if (!validDecimals) {
@@ -285,65 +285,33 @@ class AddToken extends Component {
)
}
+ renderTabs () {
+ return (
+ <Tabs>
+ <Tab name={this.context.t('search')}>
+ { this.renderSearchToken() }
+ </Tab>
+ <Tab name={this.context.t('customToken')}>
+ { this.renderCustomTokenForm() }
+ </Tab>
+ </Tabs>
+ )
+ }
+
render () {
- const { displayedTab } = this.state
const { history, clearPendingTokens } = this.props
return (
- <div className="page-container">
- <div className="page-container__header page-container__header--no-padding-bottom">
- <div className="page-container__title">
- { this.context.t('addTokens') }
- </div>
- <div className="page-container__tabs">
- <div
- className={classnames('page-container__tab', {
- 'page-container__tab--selected': displayedTab === SEARCH_TAB,
- })}
- onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
- >
- { this.context.t('search') }
- </div>
- <div
- className={classnames('page-container__tab', {
- 'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
- })}
- onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
- >
- { this.context.t('customToken') }
- </div>
- </div>
- </div>
- <div className="page-container__content">
- {
- displayedTab === CUSTOM_TOKEN_TAB
- ? this.renderCustomTokenForm()
- : this.renderSearchToken()
- }
- </div>
- <div className="page-container__footer">
- <Button
- type="default"
- large
- className="page-container__footer-button"
- onClick={() => {
- clearPendingTokens()
- history.push(DEFAULT_ROUTE)
- }}
- >
- { this.context.t('cancel') }
- </Button>
- <Button
- type="primary"
- large
- className="page-container__footer-button"
- onClick={() => this.handleNext()}
- disabled={this.hasError() || !this.hasSelected()}
- >
- { this.context.t('next') }
- </Button>
- </div>
- </div>
+ <PageContainer
+ title={this.context.t('addTokens')}
+ tabsComponent={this.renderTabs()}
+ onSubmit={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ onCancel={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ />
)
}
}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
index 1611f817b..20f550927 100644
--- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
@@ -15,7 +15,7 @@ export default class TokenListPlaceholder extends Component {
</div>
<a
className="token-list-placeholder__link"
- href="https://consensys.zendesk.com/hc/en-us/articles/360004135092"
+ href="https://metamask.zendesk.com/hc/en-us/articles/360015489031"
target="_blank"
rel="noopener noreferrer"
>
diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
new file mode 100644
index 000000000..ee5d6fa64
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
@@ -0,0 +1,122 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE } from '../../../routes'
+import Button from '../../button'
+import Identicon from '../../../components/identicon'
+import TokenBalance from '../../token-balance'
+
+export default class ConfirmAddSuggestedToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addToken: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ removeSuggestedTokens: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { pendingTokens = {}, history } = this.props
+
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ getTokenName (name, symbol) {
+ return typeof name === 'undefined'
+ ? symbol
+ : `${name} (${symbol})`
+ }
+
+ render () {
+ const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props
+ const pendingTokenKey = Object.keys(pendingTokens)[0]
+ const pendingToken = pendingTokens[pendingTokenKey]
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addSuggestedTokens') }
+ </div>
+ <div className="page-container__subtitle">
+ { this.context.t('likeToAddTokens') }
+ </div>
+ </div>
+ <div className="page-container__content">
+ <div className="confirm-add-token">
+ <div className="confirm-add-token__header">
+ <div className="confirm-add-token__token">
+ { this.context.t('token') }
+ </div>
+ <div className="confirm-add-token__balance">
+ { this.context.t('balance') }
+ </div>
+ </div>
+ <div className="confirm-add-token__token-list">
+ {
+ Object.entries(pendingTokens)
+ .map(([ address, token ]) => {
+ const { name, symbol, image } = token
+
+ return (
+ <div
+ className="confirm-add-token__token-list-item"
+ key={address}
+ >
+ <div className="confirm-add-token__token confirm-add-token__data">
+ <Identicon
+ className="confirm-add-token__token-icon"
+ diameter={48}
+ address={address}
+ image={image}
+ />
+ <div className="confirm-add-token__name">
+ { this.getTokenName(name, symbol) }
+ </div>
+ </div>
+ <div className="confirm-add-token__balance">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__footer">
+ <header>
+ <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ removeSuggestedTokens()
+ .then(() => history.push(DEFAULT_ROUTE))
+ }}
+ >
+ { this.context.t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addToken(pendingToken)
+ .then(() => removeSuggestedTokens())
+ .then(() => history.push(DEFAULT_ROUTE))
+ }}
+ >
+ { this.context.t('addToken') }
+ </Button>
+ </header>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
new file mode 100644
index 000000000..1f2737e52
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
+import { withRouter } from 'react-router-dom'
+
+const extend = require('xtend')
+
+const { addToken, removeSuggestedTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens, suggestedTokens } = metamask
+ const params = extend(pendingTokens, suggestedTokens)
+
+ return {
+ pendingTokens: params,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)),
+ removeSuggestedTokens: () => dispatch(removeSuggestedTokens()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmAddSuggestedToken)
diff --git a/ui/app/components/pages/confirm-add-suggested-token/index.js b/ui/app/components/pages/confirm-add-suggested-token/index.js
new file mode 100644
index 000000000..2ca56b43c
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'
+module.exports = ConfirmAddSuggestedToken
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
index 65d654b92..d3fec79d7 100644
--- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
@@ -2,8 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button'
-import Identicon from '../../../components/identicon'
-import TokenBalance from './token-balance'
+import Identicon from '../../identicon'
+import TokenBalance from '../../token-balance'
export default class ConfirmAddToken extends Component {
static contextTypes = {
@@ -86,28 +86,30 @@ export default class ConfirmAddToken extends Component {
</div>
</div>
<div className="page-container__footer">
- <Button
- type="default"
- large
- className="page-container__footer-button"
- onClick={() => history.push(ADD_TOKEN_ROUTE)}
- >
- { this.context.t('back') }
- </Button>
- <Button
- type="primary"
- large
- className="page-container__footer-button"
- onClick={() => {
- addTokens(pendingTokens)
- .then(() => {
- clearPendingTokens()
- history.push(DEFAULT_ROUTE)
- })
- }}
- >
- { this.context.t('addTokens') }
- </Button>
+ <header>
+ <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={() => history.push(ADD_TOKEN_ROUTE)}
+ >
+ { this.context.t('back') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addTokens(pendingTokens)
+ .then(() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ })
+ }}
+ >
+ { this.context.t('addTokens') }
+ </Button>
+ </header>
</div>
</div>
)
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js
deleted file mode 100644
index 6fb5c8223..000000000
--- a/ui/app/components/pages/confirm-add-token/token-balance/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import TokenBalance from './token-balance.container'
-module.exports = TokenBalance
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
deleted file mode 100644
index 976788d4c..000000000
--- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-
-export default class TokenBalance extends Component {
- static propTypes = {
- string: PropTypes.string,
- symbol: PropTypes.string,
- error: PropTypes.string,
- }
-
- render () {
- return (
- <div className="hide-text-overflow">{ this.props.string }</div>
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
index 961aa304e..707dad62d 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -8,6 +8,7 @@ import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
} from '../../../constants/error-keys'
+import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@@ -21,6 +22,7 @@ export default class ConfirmTransactionBase extends Component {
// Redux props
balance: PropTypes.string,
cancelTransaction: PropTypes.func,
+ cancelAllTransactions: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
clearSend: PropTypes.func,
conversionRate: PropTypes.number,
@@ -38,15 +40,18 @@ export default class ConfirmTransactionBase extends Component {
isTxReprice: PropTypes.bool,
methodData: PropTypes.object,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
+ showRejectTransactionsConfirmationModal: PropTypes.func,
toAddress: PropTypes.string,
tokenData: PropTypes.object,
tokenProps: PropTypes.object,
toName: PropTypes.string,
transactionStatus: PropTypes.string,
txData: PropTypes.object,
+ unapprovedTxCount: PropTypes.number,
// Component props
action: PropTypes.string,
contentComponent: PropTypes.node,
@@ -73,6 +78,7 @@ export default class ConfirmTransactionBase extends Component {
state = {
submitting: false,
+ submitError: null,
}
componentDidUpdate () {
@@ -83,9 +89,9 @@ export default class ConfirmTransactionBase extends Component {
clearConfirmTransaction,
} = this.props
- if (transactionStatus === 'dropped') {
+ if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
showTransactionConfirmedModal({
- onHide: () => {
+ onSubmit: () => {
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
},
@@ -246,6 +252,25 @@ export default class ConfirmTransactionBase extends Component {
onEdit({ txData, tokenData, tokenProps })
}
+ handleCancelAll () {
+ const {
+ cancelAllTransactions,
+ clearConfirmTransaction,
+ history,
+ showRejectTransactionsConfirmationModal,
+ unapprovedTxCount,
+ } = this.props
+
+ showRejectTransactionsConfirmationModal({
+ unapprovedTxCount,
+ async onSubmit () {
+ await cancelAllTransactions()
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+ }
+
handleCancel () {
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
@@ -268,7 +293,7 @@ export default class ConfirmTransactionBase extends Component {
return
}
- this.setState({ submitting: true })
+ this.setState({ submitting: true, submitError: null })
if (onSubmit) {
Promise.resolve(onSubmit(txData))
@@ -280,7 +305,9 @@ export default class ConfirmTransactionBase extends Component {
this.setState({ submitting: false })
history.push(DEFAULT_ROUTE)
})
- .catch(() => this.setState({ submitting: false }))
+ .catch(error => {
+ this.setState({ submitting: false, submitError: error.message })
+ })
}
}
@@ -307,9 +334,11 @@ export default class ConfirmTransactionBase extends Component {
contentComponent,
onEdit,
nonce,
+ assetImage,
warning,
+ unapprovedTxCount,
} = this.props
- const { submitting } = this.state
+ const { submitting, submitError } = this.state
const { name } = methodData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
@@ -331,12 +360,15 @@ export default class ConfirmTransactionBase extends Component {
dataComponent={this.renderData()}
contentComponent={contentComponent}
nonce={nonce}
+ unapprovedTxCount={unapprovedTxCount}
+ assetImage={assetImage}
identiconAddress={identiconAddress}
- errorMessage={errorMessage}
+ errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey}
warning={warning}
disabled={!propsValid || !valid || submitting}
onEdit={() => this.handleEdit()}
+ onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}
/>
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
index 0c0deff18..b34067686 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -8,7 +8,7 @@ import {
clearConfirmTransaction,
updateGasAndCalculate,
} from '../../../ducks/confirm-transaction.duck'
-import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions'
+import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal } from '../../../actions'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY,
@@ -17,7 +17,7 @@ import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
import { isBalanceSufficient } from '../../send/send.utils'
import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
-import { addressSlicer } from '../../../util'
+import { addressSlicer, valuesFor } from '../../../util'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
@@ -52,8 +52,11 @@ const mapStateToProps = (state, props) => {
accounts,
selectedAddress,
selectedAddressTxList,
+ assetImages,
+ network,
+ unapprovedTxs,
} = metamask
-
+ const assetImage = assetImages[txParamsToAddress]
const { balance } = accounts[selectedAddress]
const { name: fromName } = identities[selectedAddress]
const toAddress = propsToAddress || txParamsToAddress
@@ -66,6 +69,12 @@ const mapStateToProps = (state, props) => {
const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
const transactionStatus = transaction ? transaction.status : ''
+ const currentNetworkUnapprovedTxs = R.filter(
+ ({ metamaskNetworkId }) => metamaskNetworkId === network,
+ valuesFor(unapprovedTxs),
+ )
+ const unapprovedTxCount = currentNetworkUnapprovedTxs.length
+
return {
balance,
fromAddress,
@@ -88,6 +97,9 @@ const mapStateToProps = (state, props) => {
conversionRate,
transactionStatus,
nonce,
+ assetImage,
+ unapprovedTxs,
+ unapprovedTxCount,
}
}
@@ -95,8 +107,8 @@ const mapDispatchToProps = dispatch => {
return {
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
clearSend: () => dispatch(clearSend()),
- showTransactionConfirmedModal: ({ onHide }) => {
- return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide }))
+ showTransactionConfirmedModal: ({ onSubmit }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
},
showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))
@@ -104,7 +116,11 @@ const mapDispatchToProps = dispatch => {
updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
},
+ showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => {
+ return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount }))
+ },
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
+ cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
}
}
@@ -154,8 +170,9 @@ const getValidateEditGas = ({ balance, conversionRate, txData }) => {
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { balance, conversionRate, txData } = stateProps
+ const { balance, conversionRate, txData, unapprovedTxs } = stateProps
const {
+ cancelAllTransactions: dispatchCancelAllTransactions,
showCustomizeGasModal: dispatchShowCustomizeGasModal,
updateGasAndCalculate: dispatchUpdateGasAndCalculate,
...otherDispatchProps
@@ -172,6 +189,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
validate: validateEditGas,
}),
+ cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
}
}
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
index 0280f73c6..2c44b6094 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -12,25 +12,27 @@ import {
CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH,
} from '../../../routes'
-import { isConfirmDeployContract } from './confirm-transaction-switch.util'
+import { isConfirmDeployContract } from '../../../helpers/transactions.util'
import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
-} from './confirm-transaction-switch.constants'
+} from '../../../constants/transactions'
export default class ConfirmTransactionSwitch extends Component {
static propTypes = {
txData: PropTypes.object,
methodData: PropTypes.object,
- fetchingMethodData: PropTypes.bool,
+ fetchingData: PropTypes.bool,
+ isEtherTransaction: PropTypes.bool,
}
redirectToTransaction () {
const {
txData,
methodData: { name },
- fetchingMethodData,
+ fetchingData,
+ isEtherTransaction,
} = this.props
const { id, txParams: { data } = {} } = txData
@@ -39,10 +41,15 @@ export default class ConfirmTransactionSwitch extends Component {
return <Redirect to={{ pathname }} />
}
- if (fetchingMethodData) {
+ if (fetchingData) {
return <Loading />
}
+ if (isEtherTransaction) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
if (data) {
const methodName = name && name.toLowerCase()
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
deleted file mode 100644
index 9db4a2f96..000000000
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const TOKEN_METHOD_TRANSFER = 'transfer'
-export const TOKEN_METHOD_APPROVE = 'approve'
-export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
index 3d7fc78cc..7f2c36af2 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
@@ -6,14 +6,16 @@ const mapStateToProps = state => {
confirmTransaction: {
txData,
methodData,
- fetchingMethodData,
+ fetchingData,
+ toSmartContract,
},
} = state
return {
txData,
methodData,
- fetchingMethodData,
+ fetchingData,
+ isEtherTransaction: !toSmartContract,
}
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
index 488a189ea..2767b2e1f 100644
--- a/ui/app/components/pages/create-account/connect-hardware/account-list.js
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const genAccountLink = require('../../../../../lib/account-link.js')
const Select = require('react-select').default
+import Button from '../../../button'
class AccountList extends Component {
constructor (props, context) {
@@ -143,22 +144,20 @@ class AccountList extends Component {
}
return h('div.new-account-connect-form__buttons', {}, [
- h(
- 'button.btn-default.btn--large.new-account-connect-form__button',
- {
- onClick: this.props.onCancel.bind(this),
- },
- [this.context.t('cancel')]
- ),
-
- h(
- `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
- {
- onClick: this.props.onUnlockAccount.bind(this, this.props.device),
- ...buttonProps,
- },
- [this.context.t('unlock')]
- ),
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-connect-form__button',
+ onClick: this.props.onCancel.bind(this),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-connect-form__button unlock',
+ disabled,
+ onClick: this.props.onUnlockAccount.bind(this, this.props.device),
+ }, [this.context.t('unlock')]),
])
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
index b3dfa4ee2..d3abf3119 100644
--- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -1,6 +1,7 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
+import Button from '../../../button'
class ConnectScreen extends Component {
constructor (props, context) {
@@ -60,13 +61,13 @@ class ConnectScreen extends Component {
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
]),
- h(
- 'button.btn-primary.btn--large',
- {
- onClick: () => global.platform.openWindow({
- url: 'https://google.com/chrome',
- }),
- },
+ h(Button, {
+ type: 'primary',
+ large: true,
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ },
this.context.t('downloadGoogleChrome')
),
])
diff --git a/ui/app/components/pages/create-account/import-account/index.js b/ui/app/components/pages/create-account/import-account/index.js
index e2e973af9..48d8f8838 100644
--- a/ui/app/components/pages/create-account/import-account/index.js
+++ b/ui/app/components/pages/create-account/import-account/index.js
@@ -46,7 +46,7 @@ AccountImportSubview.prototype.render = function () {
},
onClick: () => {
global.platform.openWindow({
- url: 'https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI',
+ url: 'https://metamask.zendesk.com/hc/en-us/articles/360015289932',
})
},
}, this.context.t('here')),
diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
index dd57256a3..90279bbbd 100644
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ b/ui/app/components/pages/create-account/import-account/json.js
@@ -8,6 +8,7 @@ const actions = require('../../../../actions')
const FileInput = require('react-simple-file-input').default
const { DEFAULT_ROUTE } = require('../../../../routes')
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
+import Button from '../../../button'
class JsonImportSubview extends Component {
constructor (props) {
@@ -51,17 +52,19 @@ class JsonImportSubview extends Component {
h('div.new-account-create-form__buttons', {}, [
- h('button.btn-default.new-account-create-form__button', {
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, [
- this.context.t('cancel'),
- ]),
+ }, [this.context.t('cancel')]),
- h('button.btn-primary.new-account-create-form__button', {
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => this.createNewKeychain(),
- }, [
- this.context.t('import'),
- ]),
+ }, [this.context.t('import')]),
]),
@@ -82,7 +85,7 @@ class JsonImportSubview extends Component {
}
createNewKeychain () {
- const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress } = this.props
+ const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress, history } = this.props
const state = this.state
if (!state) {
diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js
index 1db999f2f..8db1bfbdd 100644
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ b/ui/app/components/pages/create-account/import-account/private-key.js
@@ -7,6 +7,7 @@ const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { DEFAULT_ROUTE } = require('../../../../routes')
+import Button from '../../../button'
PrivateKeyImportView.contextTypes = {
t: PropTypes.func,
@@ -61,20 +62,22 @@ PrivateKeyImportView.prototype.render = function () {
h('div.new-account-import-form__buttons', {}, [
- h('button.btn-default.btn--large.new-account-create-form__button', {
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => {
displayWarning(null)
this.props.history.push(DEFAULT_ROUTE)
},
- }, [
- this.context.t('cancel'),
- ]),
+ }, [this.context.t('cancel')]),
- h('button.btn-primary.btn--large.new-account-create-form__button', {
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => this.createNewKeychain(),
- }, [
- this.context.t('import'),
- ]),
+ }, [this.context.t('import')]),
]),
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 402b8f03b..94a5fa487 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -4,6 +4,7 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
+import Button from '../../button'
class NewAccountCreateForm extends Component {
constructor (props, context) {
@@ -38,20 +39,22 @@ class NewAccountCreateForm extends Component {
h('div.new-account-create-form__buttons', {}, [
- h('button.btn-default.btn--large.new-account-create-form__button', {
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => history.push(DEFAULT_ROUTE),
- }, [
- this.context.t('cancel'),
- ]),
+ }, [this.context.t('cancel')]),
- h('button.btn-primary.btn--large.new-account-create-form__button', {
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
onClick: () => {
createAccount(newAccountName || defaultAccountName)
.then(() => history.push(DEFAULT_ROUTE))
},
- }, [
- this.context.t('create'),
- ]),
+ }, [this.context.t('create')]),
]),
diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js
deleted file mode 100644
index 5e3fdc9af..000000000
--- a/ui/app/components/pages/home.js
+++ /dev/null
@@ -1,239 +0,0 @@
-const { Component } = require('react')
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const { Redirect, withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const h = require('react-hyperscript')
-const actions = require('../../actions')
-const log = require('loglevel')
-
-// init
-const NewKeyChainScreen = require('../../new-keychain')
-// mascara
-const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
-
-// accounts
-const MainContainer = require('../../main-container')
-
-// other views
-const BuyView = require('../../components/buy-button-subview')
-const QrView = require('../../components/qr-code')
-
-// Routes
-const {
- INITIALIZE_BACKUP_PHRASE_ROUTE,
- RESTORE_VAULT_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- NOTICE_ROUTE,
-} = require('../../routes')
-
-const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
-
-class Home extends Component {
- componentDidMount () {
- const {
- history,
- unconfirmedTransactionsCount = 0,
- } = this.props
-
- // unapprovedTxs and unapproved messages
- if (unconfirmedTransactionsCount > 0) {
- history.push(CONFIRM_TRANSACTION_ROUTE)
- }
- }
-
- render () {
- log.debug('rendering primary')
- const {
- noActiveNotices,
- lostAccounts,
- forgottenPassword,
- currentView,
- activeAddress,
- seedWords,
- } = this.props
-
- // notices
- if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
- return h(Redirect, {
- to: {
- pathname: NOTICE_ROUTE,
- },
- })
- }
-
- // seed words
- if (seedWords) {
- log.debug('rendering seed words')
- return h(Redirect, {
- to: {
- pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
- },
- })
- }
-
- if (forgottenPassword) {
- log.debug('rendering restore vault screen')
- return h(Redirect, {
- to: {
- pathname: RESTORE_VAULT_ROUTE,
- },
- })
- }
-
- // show current view
- switch (currentView.name) {
-
- case 'accountDetail':
- log.debug('rendering main container')
- return h(MainContainer, {key: 'account-detail'})
-
- case 'newKeychain':
- log.debug('rendering new keychain screen')
- return h(NewKeyChainScreen, {key: 'new-keychain'})
-
- case 'buyEth':
- log.debug('rendering buy ether screen')
- return h(BuyView, {key: 'buyEthView'})
-
- case 'onboardingBuyEth':
- log.debug('rendering onboarding buy ether screen')
- return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
-
- case 'qr':
- log.debug('rendering show qr screen')
- return h('div', {
- style: {
- position: 'absolute',
- height: '100%',
- top: '0px',
- left: '0px',
- },
- }, [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
- style: {
- marginLeft: '10px',
- marginTop: '50px',
- },
- }),
- h('div', {
- style: {
- position: 'absolute',
- left: '44px',
- width: '285px',
- },
- }, [
- h(QrView, {key: 'qr'}),
- ]),
- ])
-
- default:
- log.debug('rendering default, account detail screen')
- return h(MainContainer, {key: 'account-detail'})
- }
- }
-}
-
-Home.propTypes = {
- currentCurrency: PropTypes.string,
- isLoading: PropTypes.bool,
- loadingMessage: PropTypes.string,
- network: PropTypes.string,
- provider: PropTypes.object,
- frequentRpcList: PropTypes.array,
- currentView: PropTypes.object,
- sidebarOpen: PropTypes.bool,
- isMascara: PropTypes.bool,
- isOnboarding: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- networkDropdownOpen: PropTypes.bool,
- history: PropTypes.object,
- dispatch: PropTypes.func,
- selectedAddress: PropTypes.string,
- noActiveNotices: PropTypes.bool,
- lostAccounts: PropTypes.array,
- isInitialized: PropTypes.bool,
- forgottenPassword: PropTypes.bool,
- activeAddress: PropTypes.string,
- unapprovedTxs: PropTypes.object,
- seedWords: PropTypes.string,
- unapprovedMsgCount: PropTypes.number,
- unapprovedPersonalMsgCount: PropTypes.number,
- unapprovedTypedMessagesCount: PropTypes.number,
- welcomeScreenSeen: PropTypes.bool,
- isPopup: PropTypes.bool,
- isMouseUser: PropTypes.bool,
- t: PropTypes.func,
- unconfirmedTransactionsCount: PropTypes.number,
-}
-
-function mapStateToProps (state) {
- const { appState, metamask } = state
- const {
- networkDropdownOpen,
- sidebarOpen,
- isLoading,
- loadingMessage,
- } = appState
-
- const {
- accounts,
- address,
- isInitialized,
- noActiveNotices,
- seedWords,
- unapprovedTxs,
- nextUnreadNotice,
- lostAccounts,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = metamask
- const selected = address || Object.keys(accounts)[0]
-
- return {
- // state from plugin
- networkDropdownOpen,
- sidebarOpen,
- isLoading,
- loadingMessage,
- noActiveNotices,
- isInitialized,
- isUnlocked: state.metamask.isUnlocked,
- selectedAddress: state.metamask.selectedAddress,
- currentView: state.appState.currentView,
- activeAddress: state.appState.activeAddress,
- transForward: state.appState.transForward,
- isMascara: state.metamask.isMascara,
- isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
- isPopup: state.metamask.isPopup,
- seedWords: state.metamask.seedWords,
- unapprovedTxs,
- unapprovedMsgs: state.metamask.unapprovedMsgs,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- menuOpen: state.appState.menuOpen,
- network: state.metamask.network,
- provider: state.metamask.provider,
- forgottenPassword: state.appState.forgottenPassword,
- nextUnreadNotice,
- lostAccounts,
- frequentRpcList: state.metamask.frequentRpcList || [],
- currentCurrency: state.metamask.currentCurrency,
- isMouseUser: state.appState.isMouseUser,
- isRevealingSeedWords: state.metamask.isRevealingSeedWords,
- Qr: state.appState.Qr,
- welcomeScreenSeen: state.metamask.welcomeScreenSeen,
-
- // state needed to get account dropdown temporarily rendering from app bar
- selected,
- unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(Home)
diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js
new file mode 100644
index 000000000..d3c71c4f6
--- /dev/null
+++ b/ui/app/components/pages/home/home.component.js
@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Media from 'react-media'
+import { Redirect } from 'react-router-dom'
+import WalletView from '../../wallet-view'
+import TransactionView from '../../transaction-view'
+import {
+ INITIALIZE_BACKUP_PHRASE_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ NOTICE_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+} from '../../../routes'
+
+export default class Home extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ noActiveNotices: PropTypes.bool,
+ lostAccounts: PropTypes.array,
+ forgottenPassword: PropTypes.bool,
+ seedWords: PropTypes.string,
+ suggestedTokens: PropTypes.object,
+ unconfirmedTransactionsCount: PropTypes.number,
+ }
+
+ componentDidMount () {
+ const {
+ history,
+ suggestedTokens = {},
+ unconfirmedTransactionsCount = 0,
+ } = this.props
+
+ // suggested new tokens
+ if (Object.keys(suggestedTokens).length > 0) {
+ history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
+ }
+
+ if (unconfirmedTransactionsCount > 0) {
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+ }
+
+ render () {
+ const {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ } = this.props
+
+ // notices
+ if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
+ return <Redirect to={{ pathname: NOTICE_ROUTE }} />
+ }
+
+ // seed words
+ if (seedWords) {
+ return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
+ }
+
+ if (forgottenPassword) {
+ return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
+ }
+
+ return (
+ <div className="main-container">
+ <div className="account-and-transaction-details">
+ <Media
+ query="(min-width: 576px)"
+ render={() => <WalletView />}
+ />
+ <TransactionView />
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/home/home.container.js b/ui/app/components/pages/home/home.container.js
new file mode 100644
index 000000000..58001df6b
--- /dev/null
+++ b/ui/app/components/pages/home/home.container.js
@@ -0,0 +1,30 @@
+import Home from './home.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask, appState } = state
+ const {
+ noActiveNotices,
+ lostAccounts,
+ seedWords,
+ suggestedTokens,
+ } = metamask
+ const { forgottenPassword } = appState
+
+ return {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ suggestedTokens,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Home)
diff --git a/ui/app/components/pages/home/index.js b/ui/app/components/pages/home/index.js
new file mode 100644
index 000000000..4474ba5b8
--- /dev/null
+++ b/ui/app/components/pages/home/index.js
@@ -0,0 +1 @@
+export { default } from './home.container'
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
index b15c59863..6551278f5 100644
--- a/ui/app/components/pages/index.scss
+++ b/ui/app/components/pages/index.scss
@@ -3,3 +3,5 @@
@import './add-token/index';
@import './confirm-add-token/index';
+
+@import './settings/index';
diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js
index 7d7d3f462..32557066f 100644
--- a/ui/app/components/pages/keychains/reveal-seed.js
+++ b/ui/app/components/pages/keychains/reveal-seed.js
@@ -8,6 +8,8 @@ const { requestRevealSeedWords } = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
const ExportTextContainer = require('../../export-text-container')
+import Button from '../../button'
+
const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
@@ -106,13 +108,21 @@ class RevealSeedPage extends Component {
renderPasswordPromptFooter () {
return (
h('.page-container__footer', [
- h('button.btn-default.btn--large.page-container__footer-button', {
- onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, this.context.t('cancel')),
- h('button.btn-primary.btn--large.page-container__footer-button', {
- onClick: event => this.handleSubmit(event),
- disabled: this.state.password === '',
- }, this.context.t('next')),
+ h('header', [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: event => this.handleSubmit(event),
+ disabled: this.state.password === '',
+ }, this.context.t('next')),
+ ]),
])
)
}
@@ -120,7 +130,10 @@ class RevealSeedPage extends Component {
renderRevealSeedFooter () {
return (
h('.page-container__footer', [
- h('button.btn-default.btn--large.page-container__footer-button', {
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('close')),
])
diff --git a/ui/app/components/pages/settings/index.js b/ui/app/components/pages/settings/index.js
index aee17e0e8..44a9ffa63 100644
--- a/ui/app/components/pages/settings/index.js
+++ b/ui/app/components/pages/settings/index.js
@@ -1,64 +1 @@
-const { Component } = require('react')
-const { Switch, Route, matchPath } = require('react-router-dom')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const TabBar = require('../../tab-bar')
-const Settings = require('./settings')
-const Info = require('./info')
-const { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } = require('../../../routes')
-
-class Config extends Component {
- renderTabs () {
- const { history, location } = this.props
-
- return h('div.settings__tabs', [
- h(TabBar, {
- tabs: [
- { content: this.context.t('settings'), key: SETTINGS_ROUTE },
- { content: this.context.t('info'), key: INFO_ROUTE },
- ],
- isActive: key => matchPath(location.pathname, { path: key, exact: true }),
- onSelect: key => history.push(key),
- }),
- ])
- }
-
- render () {
- const { history } = this.props
-
- return (
- h('.main-container.settings', {}, [
- h('.settings__header', [
- h('div.settings__close-button', {
- onClick: () => history.push(DEFAULT_ROUTE),
- }),
- this.renderTabs(),
- ]),
- h(Switch, [
- h(Route, {
- exact: true,
- path: INFO_ROUTE,
- component: Info,
- }),
- h(Route, {
- exact: true,
- path: SETTINGS_ROUTE,
- component: Settings,
- }),
- ]),
- ])
- )
- }
-}
-
-Config.propTypes = {
- location: PropTypes.object,
- history: PropTypes.object,
- t: PropTypes.func,
-}
-
-Config.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = Config
+export { default } from './settings.component'
diff --git a/ui/app/components/pages/settings/index.scss b/ui/app/components/pages/settings/index.scss
new file mode 100644
index 000000000..138ebcfc5
--- /dev/null
+++ b/ui/app/components/pages/settings/index.scss
@@ -0,0 +1,80 @@
+@import './info-tab/index';
+
+@import './settings-tab/index';
+
+.settings-page {
+ position: relative;
+ background: $white;
+ display: flex;
+ flex-flow: column nowrap;
+
+ &__header {
+ padding: 25px 25px 0;
+ }
+
+ &__close-button::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ position: absolute;
+ top: 25px;
+ right: 30px;
+ cursor: pointer;
+ }
+
+ &__content {
+ padding: 25px;
+ height: auto;
+ overflow: auto;
+ }
+
+ &__content-row {
+ display: flex;
+ flex-direction: row;
+ padding: 10px 0 20px;
+
+ @media screen and (max-width: 575px) {
+ flex-direction: column;
+ padding: 10px 0;
+ }
+ }
+
+ &__content-item {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 0 5px;
+ min-height: 71px;
+
+ @media screen and (max-width: 575px) {
+ height: initial;
+ padding: 5px 0;
+ }
+
+ &--without-height {
+ height: initial;
+ }
+ }
+
+ &__content-label {
+ text-transform: capitalize;
+ }
+
+ &__content-description {
+ font-size: 14px;
+ color: $dusty-gray;
+ padding-top: 5px;
+ }
+
+ &__content-item-col {
+ max-width: 300px;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: 575px) {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+}
diff --git a/ui/app/components/pages/settings/info-tab/index.js b/ui/app/components/pages/settings/info-tab/index.js
new file mode 100644
index 000000000..7556a258d
--- /dev/null
+++ b/ui/app/components/pages/settings/info-tab/index.js
@@ -0,0 +1 @@
+export { default } from './info-tab.component'
diff --git a/ui/app/components/pages/settings/info-tab/index.scss b/ui/app/components/pages/settings/info-tab/index.scss
new file mode 100644
index 000000000..43ad6f652
--- /dev/null
+++ b/ui/app/components/pages/settings/info-tab/index.scss
@@ -0,0 +1,56 @@
+.info-tab {
+ &__logo-wrapper {
+ height: 80px;
+ margin-bottom: 20px;
+ }
+
+ &__logo {
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ &__item {
+ padding: 10px 0;
+ }
+
+ &__link-header {
+ padding-bottom: 15px;
+
+ @media screen and (max-width: 575px) {
+ padding-bottom: 5px;
+ }
+ }
+
+ &__link-item {
+ padding: 15px 0;
+
+ @media screen and (max-width: 575px) {
+ padding: 5px 0;
+ }
+ }
+
+ &__link-text {
+ color: $curious-blue;
+ }
+
+ &__version-number {
+ padding-top: 5px;
+ font-size: 13px;
+ color: $dusty-gray;
+ }
+
+ &__separator {
+ margin: 15px 0;
+ width: 80px;
+ border-color: $alto;
+ border: none;
+ height: 1px;
+ background-color: $alto;
+ color: $alto;
+ }
+
+ &__about {
+ color: $dusty-gray;
+ margin-bottom: 15px;
+ }
+}
diff --git a/ui/app/components/pages/settings/info-tab/info-tab.component.js b/ui/app/components/pages/settings/info-tab/info-tab.component.js
new file mode 100644
index 000000000..72f7d835e
--- /dev/null
+++ b/ui/app/components/pages/settings/info-tab/info-tab.component.js
@@ -0,0 +1,136 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+
+export default class InfoTab extends PureComponent {
+ state = {
+ version: global.platform.getVersion(),
+ }
+
+ static propTypes = {
+ tab: PropTypes.string,
+ metamask: PropTypes.object,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ warning: PropTypes.string,
+ location: PropTypes.object,
+ history: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderInfoLinks () {
+ const { t } = this.context
+
+ return (
+ <div className="settings-page__content-item settings-page__content-item--without-height">
+ <div className="info-tab__link-header">
+ { t('links') }
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('privacyMsg') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('terms') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/attributions.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('attributions') }
+ </span>
+ </a>
+ </div>
+ <hr className="info-tab__separator" />
+ <div className="info-tab__link-item">
+ <a
+ href="https://support.metamask.io"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('supportCenter') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('visitWebSite') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="mailto:help@metamask.io?subject=Feedback"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('emailUs') }
+ </span>
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="settings-page__content">
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item settings-page__content-item--without-height">
+ <div className="info-tab__logo-wrapper">
+ <img
+ src="images/info-logo.png"
+ className="info-tab__logo"
+ />
+ </div>
+ <div className="info-tab__item">
+ <div className="info-tab__version-header">
+ { t('metamaskVersion') }
+ </div>
+ <div className="info-tab__version-number">
+ { this.state.version }
+ </div>
+ </div>
+ <div className="info-tab__item">
+ <div className="info-tab__about">
+ { t('builtInCalifornia') }
+ </div>
+ </div>
+ </div>
+ { this.renderInfoLinks() }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/settings/info.js b/ui/app/components/pages/settings/info.js
deleted file mode 100644
index bd9040499..000000000
--- a/ui/app/components/pages/settings/info.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-
-class Info extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- version: global.platform.getVersion(),
- }
- }
-
- renderLogo () {
- return (
- h('div.settings__info-logo-wrapper', [
- h('img.settings__info-logo', { src: 'images/info-logo.png' }),
- ])
- )
- }
-
- renderInfoLinks () {
- return (
- h('div.settings__content-item.settings__content-item--without-height', [
- h('div.settings__info-link-header', this.context.t('links')),
- h('div.settings__info-link-item', [
- h('a', {
- href: 'https://metamask.io/privacy.html',
- target: '_blank',
- }, [
- h('span.settings__info-link', this.context.t('privacyMsg')),
- ]),
- ]),
- h('div.settings__info-link-item', [
- h('a', {
- href: 'https://metamask.io/terms.html',
- target: '_blank',
- }, [
- h('span.settings__info-link', this.context.t('terms')),
- ]),
- ]),
- h('div.settings__info-link-item', [
- h('a', {
- href: 'https://metamask.io/attributions.html',
- target: '_blank',
- }, [
- h('span.settings__info-link', this.context.t('attributions')),
- ]),
- ]),
- h('hr.settings__info-separator'),
- h('div.settings__info-link-item', [
- h('a', {
- href: 'https://support.metamask.io',
- target: '_blank',
- }, [
- h('span.settings__info-link', this.context.t('supportCenter')),
- ]),
- ]),
- h('div.settings__info-link-item', [
- h('a', {
- href: 'https://metamask.io/',
- target: '_blank',
- }, [
- h('span.settings__info-link', this.context.t('visitWebSite')),
- ]),
- ]),
- h('div.settings__info-link-item', [
- h('a', {
- target: '_blank',
- href: 'mailto:help@metamask.io?subject=Feedback',
- }, [
- h('span.settings__info-link', this.context.t('emailUs')),
- ]),
- ]),
- ])
- )
- }
-
- render () {
- return (
- h('div.settings__content', [
- h('div.settings__content-row', [
- h('div.settings__content-item.settings__content-item--without-height', [
- this.renderLogo(),
- h('div.settings__info-item', [
- h('div.settings__info-version-header', 'MetaMask Version'),
- h('div.settings__info-version-number', this.state.version),
- ]),
- h('div.settings__info-item', [
- h(
- 'div.settings__info-about',
- this.context.t('builtInCalifornia')
- ),
- ]),
- ]),
- this.renderInfoLinks(),
- ]),
- ])
- )
- }
-}
-
-Info.propTypes = {
- tab: PropTypes.string,
- metamask: PropTypes.object,
- setCurrentCurrency: PropTypes.func,
- setRpcTarget: PropTypes.func,
- displayWarning: PropTypes.func,
- revealSeedConfirmation: PropTypes.func,
- warning: PropTypes.string,
- location: PropTypes.object,
- history: PropTypes.object,
- t: PropTypes.func,
-}
-
-Info.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = Info
diff --git a/ui/app/components/pages/settings/settings-tab/index.js b/ui/app/components/pages/settings/settings-tab/index.js
new file mode 100644
index 000000000..9fdaafd3f
--- /dev/null
+++ b/ui/app/components/pages/settings/settings-tab/index.js
@@ -0,0 +1 @@
+export { default } from './settings-tab.container'
diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss
new file mode 100644
index 000000000..76a0cec6f
--- /dev/null
+++ b/ui/app/components/pages/settings/settings-tab/index.scss
@@ -0,0 +1,51 @@
+.settings-tab {
+ &__error {
+ padding-bottom: 20px;
+ text-align: center;
+ color: $crimson;
+ }
+
+ &__rpc-save-button {
+ align-self: flex-end;
+ padding: 5px;
+ text-transform: uppercase;
+ color: $dusty-gray;
+ cursor: pointer;
+ }
+
+ &__rpc-save-button {
+ align-self: flex-end;
+ padding: 5px;
+ text-transform: uppercase;
+ color: $dusty-gray;
+ cursor: pointer;
+ }
+
+ &__button--red {
+ border-color: lighten($monzo, 10%);
+ color: $monzo;
+
+ &:active {
+ background: lighten($monzo, 55%);
+ border-color: $monzo;
+ }
+
+ &:hover {
+ border-color: $monzo;
+ }
+ }
+
+ &__button--orange {
+ border-color: lighten($ecstasy, 20%);
+ color: $ecstasy;
+
+ &:active {
+ background: lighten($ecstasy, 40%);
+ border-color: $ecstasy;
+ }
+
+ &:hover {
+ border-color: $ecstasy;
+ }
+ }
+}
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
new file mode 100644
index 000000000..9da624f56
--- /dev/null
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
@@ -0,0 +1,360 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import infuraCurrencies from '../../../../infura-conversion.json'
+import validUrl from 'valid-url'
+import { exportAsFile } from '../../../../util'
+import SimpleDropdown from '../../../dropdowns/simple-dropdown'
+import ToggleButton from 'react-toggle-button'
+import { REVEAL_SEED_ROUTE } from '../../../../routes'
+import locales from '../../../../../../app/_locales/index.json'
+import TextField from '../../../text-field'
+import Button from '../../../button'
+
+const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
+ return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
+})
+
+const infuraCurrencyOptions = sortedCurrencies.map(({ quote: { code, name } }) => {
+ return {
+ displayValue: `${code.toUpperCase()} - ${name}`,
+ key: code,
+ value: code,
+ }
+})
+
+const localeOptions = locales.map(locale => {
+ return {
+ displayValue: `${locale.name}`,
+ key: locale.code,
+ value: locale.code,
+ }
+})
+
+export default class SettingsTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ metamask: PropTypes.object,
+ setUseBlockie: PropTypes.func,
+ setHexDataFeatureFlag: PropTypes.func,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ delRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ setFeatureFlagToBeta: PropTypes.func,
+ showResetAccountConfirmationModal: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ isMascara: PropTypes.bool,
+ updateCurrentLocale: PropTypes.func,
+ currentLocale: PropTypes.string,
+ useBlockie: PropTypes.bool,
+ sendHexData: PropTypes.bool,
+ currentCurrency: PropTypes.string,
+ conversionDate: PropTypes.number,
+ }
+
+ state = {
+ newRpc: '',
+ }
+
+ renderCurrentConversion () {
+ const { t } = this.context
+ const { currentCurrency, conversionDate, setCurrentCurrency } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('currentConversion') }</span>
+ <span className="settings-page__content-description">
+ { t('updatedWithDate', [Date(conversionDate)]) }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <SimpleDropdown
+ placeholder={t('selectCurrency')}
+ options={infuraCurrencyOptions}
+ selectedOption={currentCurrency}
+ onSelect={newCurrency => setCurrentCurrency(newCurrency)}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderCurrentLocale () {
+ const { t } = this.context
+ const { updateCurrentLocale, currentLocale } = this.props
+ const currentLocaleMeta = locales.find(locale => locale.code === currentLocale)
+ const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span className="settings-page__content-label">
+ { t('currentLanguage') }
+ </span>
+ <span className="settings-page__content-description">
+ { currentLocaleName }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <SimpleDropdown
+ placeholder={t('selectLocale')}
+ options={localeOptions}
+ selectedOption={currentLocale}
+ onSelect={async newLocale => updateCurrentLocale(newLocale)}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderNewRpcUrl () {
+ const { t } = this.context
+ const { newRpc } = this.state
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('newRPC') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <TextField
+ type="text"
+ id="new-rpc"
+ placeholder={t('newRPC')}
+ value={newRpc}
+ onChange={e => this.setState({ newRpc: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc)
+ }
+ }}
+ fullWidth
+ margin="none"
+ />
+ <div
+ className="settings-tab__rpc-save-button"
+ onClick={e => {
+ e.preventDefault()
+ this.validateRpc(newRpc)
+ }}
+ >
+ { t('save') }
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ validateRpc (newRpc) {
+ const { setRpcTarget, displayWarning } = this.props
+
+ if (validUrl.isWebUri(newRpc)) {
+ setRpcTarget(newRpc)
+ } else {
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning(this.context.t('uriErrorMsg'))
+ } else {
+ displayWarning(this.context.t('invalidRPC'))
+ }
+ }
+ }
+
+ renderStateLogs () {
+ const { t } = this.context
+ const { displayWarning } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('stateLogs') }</span>
+ <span className="settings-page__content-description">
+ { t('stateLogsDescription') }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="primary"
+ large
+ onClick={() => {
+ window.logStateString((err, result) => {
+ if (err) {
+ displayWarning(t('stateLogError'))
+ } else {
+ exportAsFile('MetaMask State Logs.json', result)
+ }
+ })
+ }}
+ >
+ { t('downloadStateLogs') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderSeedWords () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('revealSeedWords') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ onClick={event => {
+ event.preventDefault()
+ history.push(REVEAL_SEED_ROUTE)
+ }}
+ >
+ { t('revealSeedWords') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderOldUI () {
+ const { t } = this.context
+ const { setFeatureFlagToBeta } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('useOldUI') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ className="settings-tab__button--orange"
+ onClick={event => {
+ event.preventDefault()
+ setFeatureFlagToBeta()
+ }}
+ >
+ { t('useOldUI') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderResetAccount () {
+ const { t } = this.context
+ const { showResetAccountConfirmationModal } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('resetAccount') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ className="settings-tab__button--orange"
+ onClick={event => {
+ event.preventDefault()
+ showResetAccountConfirmationModal()
+ }}
+ >
+ { t('resetAccount') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderBlockieOptIn () {
+ const { useBlockie, setUseBlockie } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ this.context.t('blockiesIdenticon') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={useBlockie}
+ onToggle={value => setUseBlockie(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderHexDataOptIn () {
+ const { t } = this.context
+ const { sendHexData, setHexDataFeatureFlag } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showHexData') }</span>
+ <div className="settings-page__content-description">
+ { t('showHexDataDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={sendHexData}
+ onToggle={value => setHexDataFeatureFlag(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { warning, isMascara } = this.props
+
+ return (
+ <div className="settings-page__content">
+ { warning && <div className="settings-tab__error">{ warning }</div> }
+ { this.renderCurrentConversion() }
+ { this.renderCurrentLocale() }
+ { this.renderNewRpcUrl() }
+ { this.renderStateLogs() }
+ { this.renderSeedWords() }
+ { !isMascara && this.renderOldUI() }
+ { this.renderResetAccount() }
+ { this.renderBlockieOptIn() }
+ { this.renderHexDataOptIn() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
new file mode 100644
index 000000000..665b56f5c
--- /dev/null
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
@@ -0,0 +1,59 @@
+import SettingsTab from './settings-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ setCurrentCurrency,
+ setRpcTarget,
+ displayWarning,
+ revealSeedConfirmation,
+ setUseBlockie,
+ updateCurrentLocale,
+ setFeatureFlag,
+ showModal,
+} from '../../../../actions'
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask } = state
+ const {
+ currentCurrency,
+ conversionDate,
+ useBlockie,
+ featureFlags: { sendHexData } = {},
+ provider = {},
+ isMascara,
+ currentLocale,
+ } = metamask
+
+ return {
+ warning,
+ isMascara,
+ currentLocale,
+ currentCurrency,
+ conversionDate,
+ useBlockie,
+ sendHexData,
+ provider,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)),
+ setRpcTarget: newRpc => dispatch(setRpcTarget(newRpc)),
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ revealSeedConfirmation: () => dispatch(revealSeedConfirmation()),
+ setUseBlockie: value => dispatch(setUseBlockie(value)),
+ updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
+ setFeatureFlagToBeta: () => {
+ return dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
+ },
+ setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
+ showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SettingsTab)
diff --git a/ui/app/components/pages/settings/settings.component.js b/ui/app/components/pages/settings/settings.component.js
new file mode 100644
index 000000000..94a97bba1
--- /dev/null
+++ b/ui/app/components/pages/settings/settings.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route, matchPath } from 'react-router-dom'
+import TabBar from '../../tab-bar'
+import SettingsTab from './settings-tab'
+import InfoTab from './info-tab'
+import { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } from '../../../routes'
+
+export default class SettingsPage extends PureComponent {
+ static propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { history, location } = this.props
+
+ return (
+ <div className="main-container settings-page">
+ <div className="settings-page__header">
+ <div
+ className="settings-page__close-button"
+ onClick={() => history.push(DEFAULT_ROUTE)}
+ />
+ <TabBar
+ tabs={[
+ { content: this.context.t('settings'), key: SETTINGS_ROUTE },
+ { content: this.context.t('info'), key: INFO_ROUTE },
+ ]}
+ isActive={key => matchPath(location.pathname, { path: key, exact: true })}
+ onSelect={key => history.push(key)}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INFO_ROUTE}
+ component={InfoTab}
+ />
+ <Route
+ exact
+ path={SETTINGS_ROUTE}
+ component={SettingsTab}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js
deleted file mode 100644
index ff42a13de..000000000
--- a/ui/app/components/pages/settings/settings.js
+++ /dev/null
@@ -1,365 +0,0 @@
-const { Component } = require('react')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('../../../actions')
-const infuraCurrencies = require('../../../infura-conversion.json')
-const validUrl = require('valid-url')
-const { exportAsFile } = require('../../../util')
-const SimpleDropdown = require('../../dropdowns/simple-dropdown')
-const ToggleButton = require('react-toggle-button')
-const { REVEAL_SEED_ROUTE } = require('../../../routes')
-const locales = require('../../../../../app/_locales/index.json')
-
-const getInfuraCurrencyOptions = () => {
- const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
- return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
- })
-
- return sortedCurrencies.map(({ quote: { code, name } }) => {
- return {
- displayValue: `${code.toUpperCase()} - ${name}`,
- key: code,
- value: code,
- }
- })
-}
-
-const getLocaleOptions = () => {
- return locales.map((locale) => {
- return {
- displayValue: `${locale.name}`,
- key: locale.code,
- value: locale.code,
- }
- })
-}
-
-class Settings extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- newRpc: '',
- }
- }
-
- renderBlockieOptIn () {
- const { metamask: { useBlockie }, setUseBlockie } = this.props
-
- return h('div.settings__content-row', [
- h('div.settings__content-item', [
- h('span', this.context.t('blockiesIdenticon')),
- ]),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h(ToggleButton, {
- value: useBlockie,
- onToggle: (value) => setUseBlockie(!value),
- activeLabel: '',
- inactiveLabel: '',
- }),
- ]),
- ]),
- ])
- }
-
- renderCurrentConversion () {
- const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
-
- return h('div.settings__content-row', [
- h('div.settings__content-item', [
- h('span', this.context.t('currentConversion')),
- h('span.settings__content-description', `Updated ${Date(conversionDate)}`),
- ]),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h(SimpleDropdown, {
- placeholder: this.context.t('selectCurrency'),
- options: getInfuraCurrencyOptions(),
- selectedOption: currentCurrency,
- onSelect: newCurrency => setCurrentCurrency(newCurrency),
- }),
- ]),
- ]),
- ])
- }
-
- renderCurrentLocale () {
- const { updateCurrentLocale, currentLocale } = this.props
- const currentLocaleMeta = locales.find(locale => locale.code === currentLocale)
- const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''
-
- return h('div.settings__content-row', [
- h('div.settings__content-item', [
- h('span', 'Current Language'),
- h('span.settings__content-description', `${currentLocaleName}`),
- ]),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h(SimpleDropdown, {
- placeholder: 'Select Locale',
- options: getLocaleOptions(),
- selectedOption: currentLocale,
- onSelect: async (newLocale) => {
- updateCurrentLocale(newLocale)
- },
- }),
- ]),
- ]),
- ])
- }
-
- renderCurrentProvider () {
- const { metamask: { provider = {} } } = this.props
- let title, value, color
-
- switch (provider.type) {
-
- case 'mainnet':
- title = this.context.t('currentNetwork')
- value = this.context.t('mainnet')
- color = '#038789'
- break
-
- case 'ropsten':
- title = this.context.t('currentNetwork')
- value = this.context.t('ropsten')
- color = '#e91550'
- break
-
- case 'kovan':
- title = this.context.t('currentNetwork')
- value = this.context.t('kovan')
- color = '#690496'
- break
-
- case 'rinkeby':
- title = this.context.t('currentNetwork')
- value = this.context.t('rinkeby')
- color = '#ebb33f'
- break
-
- default:
- title = this.context.t('currentRpc')
- value = provider.rpcTarget
- }
-
- return h('div.settings__content-row', [
- h('div.settings__content-item', title),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('div.settings__provider-wrapper', [
- h('div.settings__provider-icon', { style: { background: color } }),
- h('div', value),
- ]),
- ]),
- ]),
- ])
- }
-
- renderNewRpcUrl () {
- return (
- h('div.settings__content-row', [
- h('div.settings__content-item', [
- h('span', this.context.t('newRPC')),
- ]),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('input.settings__input', {
- placeholder: this.context.t('newRPC'),
- onChange: event => this.setState({ newRpc: event.target.value }),
- onKeyPress: event => {
- if (event.key === 'Enter') {
- this.validateRpc(this.state.newRpc)
- }
- },
- }),
- h('div.settings__rpc-save-button', {
- onClick: event => {
- event.preventDefault()
- this.validateRpc(this.state.newRpc)
- },
- }, this.context.t('save')),
- ]),
- ]),
- ])
- )
- }
-
- validateRpc (newRpc) {
- const { setRpcTarget, displayWarning } = this.props
-
- if (validUrl.isWebUri(newRpc)) {
- setRpcTarget(newRpc)
- } else {
- const appendedRpc = `http://${newRpc}`
-
- if (validUrl.isWebUri(appendedRpc)) {
- displayWarning(this.context.t('uriErrorMsg'))
- } else {
- displayWarning(this.context.t('invalidRPC'))
- }
- }
- }
-
- renderStateLogs () {
- return (
- h('div.settings__content-row', [
- h('div.settings__content-item', [
- h('div', this.context.t('stateLogs')),
- h(
- 'div.settings__content-description',
- this.context.t('stateLogsDescription')
- ),
- ]),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('button.btn-primary.btn--large.settings__button', {
- onClick (event) {
- window.logStateString((err, result) => {
- if (err) {
- this.state.dispatch(actions.displayWarning(this.context.t('stateLogError')))
- } else {
- exportAsFile('MetaMask State Logs.json', result)
- }
- })
- },
- }, this.context.t('downloadStateLogs')),
- ]),
- ]),
- ])
- )
- }
-
- renderSeedWords () {
- const { history } = this.props
-
- return (
- h('div.settings__content-row', [
- h('div.settings__content-item', this.context.t('revealSeedWords')),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('button.btn-primary.btn--large.settings__button--red', {
- onClick: event => {
- event.preventDefault()
- history.push(REVEAL_SEED_ROUTE)
- },
- }, this.context.t('revealSeedWords')),
- ]),
- ]),
- ])
- )
- }
-
- renderOldUI () {
- const { setFeatureFlagToBeta } = this.props
-
- return (
- h('div.settings__content-row', [
- h('div.settings__content-item', this.context.t('useOldUI')),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('button.btn-primary.btn--large.settings__button--orange', {
- onClick (event) {
- event.preventDefault()
- setFeatureFlagToBeta()
- },
- }, this.context.t('useOldUI')),
- ]),
- ]),
- ])
- )
- }
-
- renderResetAccount () {
- const { showResetAccountConfirmationModal } = this.props
-
- return h('div.settings__content-row', [
- h('div.settings__content-item', this.context.t('resetAccount')),
- h('div.settings__content-item', [
- h('div.settings__content-item-col', [
- h('button.btn-primary.btn--large.settings__button--orange', {
- onClick (event) {
- event.preventDefault()
- showResetAccountConfirmationModal()
- },
- }, this.context.t('resetAccount')),
- ]),
- ]),
- ])
- }
-
- render () {
- const { warning, isMascara } = this.props
-
- return (
- h('div.settings__content', [
- warning && h('div.settings__error', warning),
- this.renderCurrentConversion(),
- this.renderCurrentLocale(),
- // this.renderCurrentProvider(),
- this.renderNewRpcUrl(),
- this.renderStateLogs(),
- this.renderSeedWords(),
- !isMascara && this.renderOldUI(),
- this.renderResetAccount(),
- this.renderBlockieOptIn(),
- ])
- )
- }
-}
-
-Settings.propTypes = {
- metamask: PropTypes.object,
- setUseBlockie: PropTypes.func,
- setCurrentCurrency: PropTypes.func,
- setRpcTarget: PropTypes.func,
- displayWarning: PropTypes.func,
- revealSeedConfirmation: PropTypes.func,
- setFeatureFlagToBeta: PropTypes.func,
- showResetAccountConfirmationModal: PropTypes.func,
- warning: PropTypes.string,
- history: PropTypes.object,
- isMascara: PropTypes.bool,
- updateCurrentLocale: PropTypes.func,
- currentLocale: PropTypes.string,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- return {
- metamask: state.metamask,
- warning: state.appState.warning,
- isMascara: state.metamask.isMascara,
- currentLocale: state.metamask.currentLocale,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)),
- setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)),
- displayWarning: warning => dispatch(actions.displayWarning(warning)),
- revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()),
- setUseBlockie: value => dispatch(actions.setUseBlockie(value)),
- updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)),
- setFeatureFlagToBeta: () => {
- return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- },
- showResetAccountConfirmationModal: () => {
- return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))
- },
- }
-}
-
-Settings.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(Settings)
diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/components/pages/unlock-page/index.scss
index 3d44bd037..6bd52282d 100644
--- a/ui/app/components/pages/unlock-page/index.scss
+++ b/ui/app/components/pages/unlock-page/index.scss
@@ -14,6 +14,7 @@
align-self: stretch;
justify-content: center;
flex: 1 0 auto;
+ height: 100vh;
}
&__mascot-container {
diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js
deleted file mode 100644
index f16fcb1c7..000000000
--- a/ui/app/components/pending-msg-details.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-
-const AccountPanel = require('./account-panel')
-
-PendingMsgDetails.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(PendingMsgDetails)
-
-
-inherits(PendingMsgDetails, Component)
-function PendingMsgDetails () {
- Component.call(this)
-}
-
-PendingMsgDetails.prototype.render = function () {
- var state = this.props
- var msgData = state.txData
-
- var msgParams = msgData.msgParams || {}
- var address = msgParams.from || state.selectedAddress
- var identity = state.identities[address] || { address: address }
- var account = state.accounts[address] || { address: address }
-
- return (
- h('div', {
- key: msgData.id,
- style: {
- margin: '10px 20px',
- },
- }, [
-
- // account that will sign
- h(AccountPanel, {
- showFullAddress: true,
- identity: identity,
- account: account,
- imageifyIdenticons: state.imageifyIdenticons,
- }),
-
- // message data
- h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
- h('.flex-column.flex-space-between', [
- h('label.font-small.allcaps', this.context.t('message')),
- h('span.font-small', msgParams.data),
- ]),
- ]),
-
- ])
- )
-}
diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js
deleted file mode 100644
index 21a7864e4..000000000
--- a/ui/app/components/pending-msg.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const PendingTxDetails = require('./pending-msg-details')
-const connect = require('react-redux').connect
-
-PendingMsg.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(PendingMsg)
-
-
-inherits(PendingMsg, Component)
-function PendingMsg () {
- Component.call(this)
-}
-
-PendingMsg.prototype.render = function () {
- var state = this.props
- var msgData = state.txData
-
- return (
-
- h('div', {
- key: msgData.id,
- style: {
- maxWidth: '350px',
- },
- }, [
-
- // header
- h('h3', {
- style: {
- fontWeight: 'bold',
- textAlign: 'center',
- },
- }, this.context.t('signMessage')),
-
- h('.error', {
- style: {
- margin: '10px',
- },
- }, [
- this.context.t('signNotice'),
- h('a', {
- href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
- style: { color: 'rgb(247, 134, 28)' },
- onClick: (event) => {
- event.preventDefault()
- const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
- global.platform.openWindow({ url })
- },
- }, this.context.t('readMore')),
- ]),
-
- // message details
- h(PendingTxDetails, state),
-
- // sign + cancel
- h('.flex-row.flex-space-around', [
- h('button', {
- onClick: state.cancelMessage,
- }, this.context.t('cancel')),
- h('button', {
- onClick: state.signMessage,
- }, this.context.t('sign')),
- ]),
- ])
-
- )
-}
diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js
index 3b2c62f49..d3242ddf5 100644
--- a/ui/app/components/qr-code.js
+++ b/ui/app/components/qr-code.js
@@ -26,7 +26,7 @@ function QrCodeView () {
QrCodeView.prototype.render = function () {
const props = this.props
const { message, data } = props.Qr
- const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${data}`
+ const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data)}`
const qrImage = qrCode(4, 'M')
qrImage.addData(address)
qrImage.make()
diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js
index 6c202141e..4f98df9b6 100644
--- a/ui/app/components/selected-account/selected-account.component.js
+++ b/ui/app/components/selected-account/selected-account.component.js
@@ -1,9 +1,9 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import copyToClipboard from 'copy-to-clipboard'
-import { addressSlicer } from '../../util'
+import { addressSlicer, checksumAddress } from '../../util'
-const Tooltip = require('../tooltip-v2.js')
+const Tooltip = require('../tooltip-v2.js').default
class SelectedAccount extends Component {
state = {
@@ -22,6 +22,7 @@ class SelectedAccount extends Component {
render () {
const { t } = this.context
const { selectedAddress, selectedIdentity } = this.props
+ const checksummedAddress = checksumAddress(selectedAddress)
return (
<div className="selected-account">
@@ -34,14 +35,14 @@ class SelectedAccount extends Component {
onClick={() => {
this.setState({ copied: true })
setTimeout(() => this.setState({ copied: false }), 3000)
- copyToClipboard(selectedAddress)
+ copyToClipboard(checksummedAddress)
}}
>
<div className="selected-account__name">
{ selectedIdentity.name }
</div>
<div className="selected-account__address">
- { addressSlicer(selectedAddress) }
+ { addressSlicer(checksummedAddress) }
</div>
</div>
</Tooltip>
diff --git a/ui/app/components/selected-account/tests/selected-account-component.test.js b/ui/app/components/selected-account/tests/selected-account-component.test.js
new file mode 100644
index 000000000..78a94d1c8
--- /dev/null
+++ b/ui/app/components/selected-account/tests/selected-account-component.test.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import assert from 'assert'
+import { render } from 'enzyme'
+import SelectedAccount from '../selected-account.component'
+
+describe('SelectedAccount Component', () => {
+ it('should render checksummed address', () => {
+ const wrapper = render(<SelectedAccount
+ selectedAddress="0x1b82543566f41a7db9a9a75fc933c340ffb55c9d"
+ selectedIdentity={{ name: 'testName' }}
+ />, { context: { t: () => {}}})
+ // Checksummed version of address is displayed
+ assert.equal(wrapper.find('.selected-account__address').text(), '0x1B82...5C9D')
+ assert.equal(wrapper.find('.selected-account__name').text(), 'testName')
+ })
+})
diff --git a/ui/app/components/send/currency-display/tests/currency-display.test.js b/ui/app/components/send/currency-display/tests/currency-display.test.js
new file mode 100644
index 000000000..c9560b81c
--- /dev/null
+++ b/ui/app/components/send/currency-display/tests/currency-display.test.js
@@ -0,0 +1,91 @@
+import React from 'react'
+import assert from 'assert'
+import sinon from 'sinon'
+import { shallow, mount } from 'enzyme'
+import CurrencyDisplay from '../currency-display'
+
+describe('', () => {
+
+ const token = {
+ address: '0xTest',
+ symbol: 'TST',
+ decimals: '13',
+ }
+
+ it('retuns ETH value for wei value', () => {
+ const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}})
+
+ const value = wrapper.instance().getValueToRender({
+ // 1000000000000000000
+ value: 'DE0B6B3A7640000',
+ })
+
+ assert.equal(value, 1)
+ })
+
+ it('returns value of token based on token decimals', () => {
+ const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}})
+
+ const value = wrapper.instance().getValueToRender({
+ selectedToken: token,
+ // 1000000000000000000
+ value: 'DE0B6B3A7640000',
+ })
+
+ assert.equal(value, 100000)
+ })
+
+ it('returns hex value with decimal adjustment', () => {
+
+ const wrapper = mount(
+ <CurrencyDisplay
+ selectedToken={token}
+ />, {context: {t: str => str + '_t'}})
+
+ const value = wrapper.instance().getAmount(1)
+ // 10000000000000
+ assert.equal(value, '9184e72a000')
+ })
+
+ it('#getConvertedValueToRender converts input value based on conversionRate', () => {
+
+ const wrapper = mount(
+ <CurrencyDisplay
+ primaryCurrency={'usd'}
+ convertedCurrency={'ja'}
+ conversionRate={2}
+ />, {context: {t: str => str + '_t'}})
+
+ const value = wrapper.instance().getConvertedValueToRender(32)
+
+ assert.equal(value, 64)
+ })
+
+ it('#onlyRenderConversions renders single element for converted currency and value', () => {
+ const wrapper = mount(
+ <CurrencyDisplay
+ convertedCurrency={'test'}
+ />, {context: {t: str => str + '_t'}})
+
+ const value = wrapper.instance().onlyRenderConversions(10)
+ assert.equal(value.props.className, 'currency-display__converted-value')
+ assert.equal(value.props.children, '10 TEST')
+ })
+
+ it('simulates change value in input', () => {
+ const handleChangeSpy = sinon.spy()
+
+ const wrapper = shallow(
+ <CurrencyDisplay
+ onChange={handleChangeSpy}
+ />, {context: {t: str => str + '_t'}})
+
+ const input = wrapper.find('input')
+ input.simulate('focus')
+ input.simulate('change', { target: { value: '100' } })
+
+ assert.equal(wrapper.state().valueToRender, '100')
+ assert.equal(wrapper.find('input').prop('value'), '100')
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
index df7bcb7cc..1b03ffd2b 100644
--- a/ui/app/components/send/send-content/send-content.component.js
+++ b/ui/app/components/send/send-content/send-content.component.js
@@ -12,20 +12,27 @@ export default class SendContent extends Component {
static propTypes = {
updateGas: PropTypes.func,
scanQrCode: PropTypes.func,
+ showHexData: PropTypes.bool,
};
+ updateGas = (updateData) => this.props.updateGas(updateData)
+
render () {
return (
<PageContainerContent>
<div className="send-v2__form">
<SendFromRow />
<SendToRow
- updateGas={(updateData) => this.props.updateGas(updateData)}
+ updateGas={this.updateGas}
scanQrCode={ _ => this.props.scanQrCode()}
/>
- <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
+ <SendAmountRow updateGas={this.updateGas} />
<SendGasRow />
- <SendHexDataRow />
+ {(this.props.showHexData && (
+ <SendHexDataRow
+ updateGas={this.updateGas}
+ />
+ ))}
</div>
</PageContainerContent>
)
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
index 063930db3..62a74a77b 100644
--- a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
+++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
@@ -7,6 +7,7 @@ export default class SendHexDataRow extends Component {
data: PropTypes.string,
inError: PropTypes.bool,
updateSendHexData: PropTypes.func.isRequired,
+ updateGas: PropTypes.func.isRequired,
};
static contextTypes = {
@@ -14,9 +15,10 @@ export default class SendHexDataRow extends Component {
};
onInput = (event) => {
- const {updateSendHexData} = this.props
- event.target.value = event.target.value.replace(/\n/g, '')
- updateSendHexData(event.target.value || null)
+ const {updateSendHexData, updateGas} = this.props
+ const data = event.target.value.replace(/\n/g, '') || null
+ updateSendHexData(data)
+ updateGas({ data })
}
render () {
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 1163dcffc..17c75c817 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -8,6 +8,7 @@ export default class SendToRow extends Component {
static propTypes = {
closeToDropdown: PropTypes.func,
+ hasHexData: PropTypes.bool.isRequired,
inError: PropTypes.bool,
network: PropTypes.string,
openToDropdown: PropTypes.func,
@@ -25,8 +26,8 @@ export default class SendToRow extends Component {
};
handleToChange (to, nickname = '', toError) {
- const { updateSendTo, updateSendToError, updateGas } = this.props
- const toErrorObject = getToErrorObject(to, toError)
+ const { hasHexData, updateSendTo, updateSendToError, updateGas } = this.props
+ const toErrorObject = getToErrorObject(to, toError, hasHexData)
updateSendTo(to, nickname)
updateSendToError(toErrorObject)
if (toErrorObject.to === null) {
@@ -48,7 +49,7 @@ export default class SendToRow extends Component {
return (
<SendRowWrapper
errorType={'to'}
- label={`${this.context.t('to')}`}
+ label={`${this.context.t('to')}: `}
showError={inError}
>
<EnsInput
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
index 1c9c9d518..3ee188bad 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
@@ -3,6 +3,7 @@ import {
getCurrentNetwork,
getSendTo,
getSendToAccounts,
+ getSendHexData,
} from '../../send.selectors.js'
import {
getToDropdownOpen,
@@ -22,6 +23,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
function mapStateToProps (state) {
return {
+ hasHexData: Boolean(getSendHexData(state)),
inError: sendToIsInError(state),
network: getCurrentNetwork(state),
to: getSendTo(state),
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
index 6b90a9f09..0eeaa3a11 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
@@ -4,9 +4,11 @@ const {
} = require('../../send.constants')
const { isValidAddress } = require('../../../../util')
-function getToErrorObject (to, toError = null) {
+function getToErrorObject (to, toError = null, hasHexData = false) {
if (!to) {
- toError = REQUIRED_ERROR
+ if (!hasHexData) {
+ toError = REQUIRED_ERROR
+ }
} else if (!isValidAddress(to) && !toError) {
toError = INVALID_RECIPIENT_ADDRESS_ERROR
}
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
index 781371004..591229deb 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
@@ -102,7 +102,7 @@ describe('SendToRow Component', function () {
assert.equal(errorType, 'to')
- assert.equal(label, 'to_t')
+ assert.equal(label, 'to_t: ')
assert.equal(showError, false)
})
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
index 92355c00a..dfce7652f 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
@@ -24,6 +24,7 @@ proxyquire('../send-to-row.container.js', {
},
'../../send.selectors.js': {
getCurrentNetwork: (s) => `mockNetwork:${s}`,
+ getSendHexData: (s) => s,
getSendTo: (s) => `mockTo:${s}`,
getSendToAccounts: (s) => `mockToAccounts:${s}`,
},
@@ -41,6 +42,7 @@ describe('send-to-row container', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
+ hasHexData: true,
inError: 'mockInError:mockState',
network: 'mockNetwork:mockState',
to: 'mockTo:mockState',
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
index 4d2447c32..c779aeb76 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
@@ -29,6 +29,12 @@ describe('send-to-row utils', () => {
})
})
+ it('should return null if to is falsy and hexData is truthy', () => {
+ assert.deepEqual(getToErrorObject(null, undefined, true), {
+ to: null,
+ })
+ })
+
it('should return an invalid recipient error if to is truthy but invalid', () => {
assert.deepEqual(getToErrorObject('mockInvalidTo'), {
to: INVALID_RECIPIENT_ADDRESS_ERROR,
diff --git a/ui/app/components/send/send-content/tests/send-content-component.test.js b/ui/app/components/send/send-content/tests/send-content-component.test.js
index d5bb6693c..c5a11c8bb 100644
--- a/ui/app/components/send/send-content/tests/send-content-component.test.js
+++ b/ui/app/components/send/send-content/tests/send-content-component.test.js
@@ -8,12 +8,13 @@ import SendAmountRow from '../send-amount-row/send-amount-row.container'
import SendFromRow from '../send-from-row/send-from-row.container'
import SendGasRow from '../send-gas-row/send-gas-row.container'
import SendToRow from '../send-to-row/send-to-row.container'
+import SendHexDataRow from '../send-hex-data-row/send-hex-data-row.container'
describe('SendContent Component', function () {
let wrapper
beforeEach(() => {
- wrapper = shallow(<SendContent />)
+ wrapper = shallow(<SendContent showHexData={true} />)
})
describe('render', () => {
@@ -33,6 +34,17 @@ describe('SendContent Component', function () {
assert(PageContainerContentChild.childAt(1).is(SendToRow))
assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
assert(PageContainerContentChild.childAt(3).is(SendGasRow))
+ assert(PageContainerContentChild.childAt(4).is(SendHexDataRow))
+ })
+
+ it('should not render the SendHexDataRow if props.showHexData is false', () => {
+ wrapper.setProps({ showHexData: false })
+ const PageContainerContentChild = wrapper.find(PageContainerContent).children()
+ assert(PageContainerContentChild.childAt(0).is(SendFromRow))
+ assert(PageContainerContentChild.childAt(1).is(SendToRow))
+ assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
+ assert(PageContainerContentChild.childAt(3).is(SendGasRow))
+ assert.equal(PageContainerContentChild.childAt(4).exists(), false)
})
})
})
diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js
index 518cff06e..230bf450f 100644
--- a/ui/app/components/send/send-footer/send-footer.component.js
+++ b/ui/app/components/send/send-footer/send-footer.component.js
@@ -86,9 +86,9 @@ export default class SendFooter extends Component {
}
formShouldBeDisabled () {
- const { inError, selectedToken, tokenBalance, gasTotal, to } = this.props
+ const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
const missingTokenBalance = selectedToken && !tokenBalance
- return inError || !gasTotal || missingTokenBalance || !to
+ return inError || !gasTotal || missingTokenBalance || !(data || to)
}
render () {
diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js
index 0d8ffd179..fb7beca16 100644
--- a/ui/app/components/send/send.component.js
+++ b/ui/app/components/send/send.component.js
@@ -62,7 +62,7 @@ export default class SendTransactionScreen extends PersistentForm {
}
}
- updateGas ({ to: updatedToAddress, amount: value } = {}) {
+ updateGas ({ to: updatedToAddress, amount: value, data } = {}) {
const {
amount,
blockGasLimit,
@@ -86,6 +86,7 @@ export default class SendTransactionScreen extends PersistentForm {
selectedToken,
to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
value: value || amount,
+ data,
})
}
@@ -193,7 +194,7 @@ export default class SendTransactionScreen extends PersistentForm {
}
render () {
- const { history } = this.props
+ const { history, showHexData } = this.props
return (
<div className="page-container">
@@ -201,6 +202,7 @@ export default class SendTransactionScreen extends PersistentForm {
<SendContent
updateGas={(updateData) => this.updateGas(updateData)}
scanQrCode={_ => this.props.scanQrCode()}
+ showHexData={showHexData}
/>
<SendFooter history={history}/>
</div>
diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js
index 41735de64..87056499f 100644
--- a/ui/app/components/send/send.container.js
+++ b/ui/app/components/send/send.container.js
@@ -18,6 +18,7 @@ import {
getSelectedTokenToFiatRate,
getSendAmount,
getSendEditingTransactionId,
+ getSendHexDataFeatureFlagState,
getSendFromObject,
getSendTo,
getTokenBalance,
@@ -64,6 +65,7 @@ function mapStateToProps (state) {
recentBlocks: getRecentBlocks(state),
selectedAddress: getSelectedAddress(state),
selectedToken: getSelectedToken(state),
+ showHexData: getSendHexDataFeatureFlagState(state),
to: getSendTo(state),
tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state),
@@ -84,9 +86,10 @@ function mapDispatchToProps (dispatch) {
selectedToken,
to,
value,
+ data,
}) => {
!editingTransactionId
- ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value }))
+ ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data }))
: dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
},
updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {
diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js
index ab3f6d34b..22e379693 100644
--- a/ui/app/components/send/send.selectors.js
+++ b/ui/app/components/send/send.selectors.js
@@ -34,6 +34,7 @@ const selectors = {
getSelectedTokenToFiatRate,
getSendAmount,
getSendHexData,
+ getSendHexDataFeatureFlagState,
getSendEditingTransactionId,
getSendErrors,
getSendFrom,
@@ -216,6 +217,10 @@ function getSendHexData (state) {
return state.metamask.send.data
}
+function getSendHexDataFeatureFlagState (state) {
+ return state.metamask.featureFlags.sendHexData
+}
+
function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId
}
diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js
index aa255c3d4..a18a9e4b3 100644
--- a/ui/app/components/send/send.utils.js
+++ b/ui/app/components/send/send.utils.js
@@ -200,16 +200,20 @@ function doesAmountErrorRequireUpdate ({
return amountErrorRequiresUpdate
}
-async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, value, gasPrice, estimateGasMethod }) {
+async function estimateGas ({
+ selectedAddress,
+ selectedToken,
+ blockGasLimit,
+ to,
+ value,
+ data,
+ gasPrice,
+ estimateGasMethod,
+}) {
const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }
- if (selectedToken) {
- paramsForGasEstimate.value = '0x0'
- paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, selectedToken })
- }
-
// if recipient has no code, gas is 21k max:
- if (!selectedToken) {
+ if (!selectedToken && !data) {
const code = Boolean(to) && await global.eth.getCode(to)
if (!code || code === '0x') {
return SIMPLE_GAS_COST
@@ -218,7 +222,19 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
return BASE_TOKEN_GAS_COST
}
- paramsForGasEstimate.to = selectedToken ? selectedToken.address : to
+ if (selectedToken) {
+ paramsForGasEstimate.value = '0x0'
+ paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, selectedToken })
+ paramsForGasEstimate.to = selectedToken.address
+ } else {
+ if (data) {
+ paramsForGasEstimate.data = data
+ }
+
+ if (to) {
+ paramsForGasEstimate.to = to
+ }
+ }
// if not, fall back to block gasLimit
paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, {
diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js
index 6194ec508..f4943e707 100644
--- a/ui/app/components/send/tests/send-component.test.js
+++ b/ui/app/components/send/tests/send-component.test.js
@@ -47,6 +47,7 @@ describe('Send Component', function () {
recentBlocks={['mockBlock']}
selectedAddress={'mockSelectedAddress'}
selectedToken={'mockSelectedToken'}
+ showHexData={true}
tokenBalance={'mockTokenBalance'}
tokenContract={'mockTokenContract'}
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
@@ -288,6 +289,7 @@ describe('Send Component', function () {
selectedToken: 'mockSelectedToken',
to: '',
value: 'mockAmount',
+ data: undefined,
}
)
})
@@ -328,5 +330,9 @@ describe('Send Component', function () {
}
)
})
+
+ it('should pass showHexData to SendContent', () => {
+ assert.equal(wrapper.find(SendContent).props().showHexData, true)
+ })
})
})
diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js
index 57e332780..6aa4bf826 100644
--- a/ui/app/components/send/tests/send-container.test.js
+++ b/ui/app/components/send/tests/send-container.test.js
@@ -39,6 +39,7 @@ proxyquire('../send.container.js', {
getSelectedToken: (s) => `mockSelectedToken:${s}`,
getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`,
+ getSendHexDataFeatureFlagState: (s) => `mockSendHexDataFeatureFlagState:${s}`,
getSendAmount: (s) => `mockAmount:${s}`,
getSendTo: (s) => `mockTo:${s}`,
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
@@ -73,6 +74,7 @@ describe('send container', () => {
recentBlocks: 'mockRecentBlocks:mockState',
selectedAddress: 'mockSelectedAddress:mockState',
selectedToken: 'mockSelectedToken:mockState',
+ showHexData: 'mockSendHexDataFeatureFlagState:mockState',
to: 'mockTo:mockState',
tokenBalance: 'mockTokenBalance:mockState',
tokenContract: 'mockTokenContract:mockState',
@@ -103,6 +105,7 @@ describe('send container', () => {
selectedToken: { address: '0x1' },
to: 'mockTo',
value: 'mockValue',
+ data: undefined,
}
it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
@@ -115,14 +118,14 @@ describe('send container', () => {
})
it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
- const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps
+ const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
mapDispatchToPropsObject.updateAndSetGasTotal(
Object.assign({}, mockProps, {editingTransactionId: false})
)
assert(dispatchSpy.calledOnce)
assert.deepEqual(
actionSpies.updateGasData.getCall(0).args[0],
- { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value }
+ { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data }
)
})
})
diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js
index 8f9c19314..8b939dadb 100644
--- a/ui/app/components/send/tests/send-selectors-test-data.js
+++ b/ui/app/components/send/tests/send-selectors-test-data.js
@@ -2,7 +2,7 @@ module.exports = {
'metamask': {
'isInitialized': true,
'isUnlocked': true,
- 'featureFlags': {'betaUI': true},
+ 'featureFlags': {'betaUI': true, 'sendHexData': true},
'rpcTarget': 'https://rawtestrpc.metamask.io/',
'identities': {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js
index 218da656b..1a47cd209 100644
--- a/ui/app/components/send/tests/send-selectors.test.js
+++ b/ui/app/components/send/tests/send-selectors.test.js
@@ -31,6 +31,7 @@ const {
getSendFrom,
getSendFromBalance,
getSendFromObject,
+ getSendHexDataFeatureFlagState,
getSendMaxModeState,
getSendTo,
getSendToAccounts,
@@ -379,6 +380,15 @@ describe('send selectors', () => {
})
})
+ describe('getSendHexDataFeatureFlagState()', () => {
+ it('should return the sendHexData feature flag state', () => {
+ assert.deepEqual(
+ getSendHexDataFeatureFlagState(mockState),
+ true
+ )
+ })
+ })
+
describe('getSendFrom()', () => {
it('should return the send.from', () => {
assert.deepEqual(
diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js
index 18dde495a..b72d87eee 100644
--- a/ui/app/components/send/tests/send-utils.test.js
+++ b/ui/app/components/send/tests/send-utils.test.js
@@ -304,10 +304,13 @@ describe('send utils', () => {
selectedAddress: 'mockAddress',
to: '0xisContract',
estimateGasMethod: sinon.stub().callsFake(
- (data, cb) => cb(
- data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null,
- { toString: (n) => `0xabc${n}` }
- )
+ ({to}, cb) => {
+ const err = typeof to === 'string' && to.match(/willFailBecauseOf:/)
+ ? new Error(to.match(/:(.+)$/)[1])
+ : null
+ const result = { toString: (n) => `0xabc${n}` }
+ return cb(err, result)
+ }
),
}
const baseExpectedCall = {
@@ -364,6 +367,18 @@ describe('send utils', () => {
assert.equal(result, '0xabc16')
})
+ it('should call ethQuery.estimateGas without a recipient if the recipient is empty and data passed', async () => {
+ const data = 'mockData'
+ const to = ''
+ const result = await estimateGas({...baseMockParams, data, to})
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ { gasPrice: undefined, value: undefined, data, from: baseExpectedCall.from, gas: baseExpectedCall.gas},
+ )
+ assert.equal(result, '0xabc16')
+ })
+
it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' }))
@@ -407,7 +422,7 @@ describe('send utils', () => {
to: 'isContract willFailBecauseOf:some other error',
}))
} catch (err) {
- assert.deepEqual(err, { message: 'some other error' })
+ assert.equal(err.message, 'some other error')
}
})
})
diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js
index 49ebf49d9..39d15dfa7 100644
--- a/ui/app/components/send/to-autocomplete/to-autocomplete.js
+++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js
@@ -5,6 +5,7 @@ const inherits = require('util').inherits
const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
const Tooltip = require('../../tooltip')
+const checksumAddress = require('../../../util').checksumAddress
ToAutoComplete.contextTypes = {
t: PropTypes.func,
@@ -48,7 +49,7 @@ ToAutoComplete.prototype.renderDropdown = function () {
account,
className: 'account-list-item__dropdown',
handleClick: () => {
- onChange(account.address)
+ onChange(checksumAddress(account.address))
closeDropdown()
},
icon: this.getListItemIcon(account.address, to),
diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/sender-to-recipient/index.scss
index a97393b8f..0ab0413be 100644
--- a/ui/app/components/sender-to-recipient/index.scss
+++ b/ui/app/components/sender-to-recipient/index.scss
@@ -1,5 +1,5 @@
.sender-to-recipient {
- &__container {
+ &--default {
width: 100%;
display: flex;
flex-direction: row;
@@ -8,67 +8,113 @@
position: relative;
flex: 0 0 auto;
height: 42px;
- }
- &__tooltip-wrapper {
- min-width: 0;
- }
+ .sender-to-recipient {
+ &__tooltip-wrapper {
+ min-width: 0;
+ }
- &__tooltip-container {
- max-width: 100%;
- }
+ &__tooltip-container {
+ max-width: 100%;
+ }
- &__sender,
- &__recipient {
- display: flex;
- flex-direction: row;
- align-items: center;
- flex: 1;
- padding: 0 16px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex: 1;
+ padding: 0 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
- &__sender {
- padding-right: 30px;
- cursor: pointer;
- }
+ &--sender {
+ padding-right: 30px;
+ cursor: pointer;
+ }
+
+ &--recipient {
+ padding-left: 30px;
+ border-left: 1px solid $geyser;
+
+ &-with-address {
+ cursor: pointer;
+ }
+ }
+ }
- &__recipient {
- padding-left: 30px;
- border-left: 1px solid $geyser;
+ &__arrow-container {
+ position: absolute;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
- &--with-address {
- cursor: pointer;
+ &__arrow-circle {
+ background: $white;
+ padding: 5px;
+ border: 1px solid $geyser;
+ border-radius: 20px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__name {
+ padding-left: 14px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .875rem;
+ }
}
}
- &__arrow-container {
- position: absolute;
- height: 100%;
+ &--cards {
+ width: 100%;
display: flex;
- align-items: center;
+ flex-direction: row;
justify-content: center;
- }
+ position: relative;
+ flex: 0 0 auto;
- &__arrow-circle {
- background: $white;
- padding: 5px;
- border: 1px solid $geyser;
- border-radius: 20px;
- height: 32px;
- width: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
+ .sender-to-recipient {
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ border-radius: 4px;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
+ padding: 6px;
+ background: $white;
+ cursor: pointer;
+ min-width: 0;
+ color: $dusty-gray;
+ }
+
+ &__tooltip-wrapper {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
- &__name {
- padding-left: 14px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: .875rem;
+ &__name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .5rem;
+ }
+
+ &__arrow-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
}
}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
index cae173b56..e71bd7406 100644
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -1,16 +1,31 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
+import classnames from 'classnames'
import Identicon from '../identicon'
import Tooltip from '../tooltip-v2'
import copyToClipboard from 'copy-to-clipboard'
+import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants'
+import { checksumAddress } from '../../util'
-export default class SenderToRecipient extends Component {
+const variantHash = {
+ [DEFAULT_VARIANT]: 'sender-to-recipient--default',
+ [CARDS_VARIANT]: 'sender-to-recipient--cards',
+}
+
+export default class SenderToRecipient extends PureComponent {
static propTypes = {
senderName: PropTypes.string,
senderAddress: PropTypes.string,
recipientName: PropTypes.string,
recipientAddress: PropTypes.string,
t: PropTypes.func,
+ variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]),
+ addressOnly: PropTypes.bool,
+ assetImage: PropTypes.string,
+ }
+
+ static defaultProps = {
+ variant: DEFAULT_VARIANT,
}
static contextTypes = {
@@ -22,24 +37,66 @@ export default class SenderToRecipient extends Component {
recipientAddressCopied: false,
}
+ renderSenderIdenticon () {
+ return !this.props.addressOnly && (
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={checksumAddress(this.props.senderAddress)}
+ diameter={24}
+ />
+ </div>
+ )
+ }
+
+ renderSenderAddress () {
+ const { t } = this.context
+ const { senderName, senderAddress, addressOnly } = this.props
+ const checksummedSenderAddress = checksumAddress(senderAddress)
+
+ return (
+ <Tooltip
+ position="bottom"
+ title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ senderAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name">
+ { addressOnly ? `${t('from')}: ${checksummedSenderAddress}` : senderName }
+ </div>
+ </Tooltip>
+ )
+ }
+
+ renderRecipientIdenticon () {
+ const { recipientAddress, assetImage } = this.props
+ const checksummedRecipientAddress = checksumAddress(recipientAddress)
+
+ return !this.props.addressOnly && (
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={checksummedRecipientAddress}
+ diameter={24}
+ image={assetImage}
+ />
+ </div>
+ )
+ }
+
renderRecipientWithAddress () {
const { t } = this.context
- const { recipientName, recipientAddress } = this.props
+ const { recipientName, recipientAddress, addressOnly } = this.props
+ const checksummedRecipientAddress = checksumAddress(recipientAddress)
return (
<div
- className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
+ className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
onClick={() => {
this.setState({ recipientAddressCopied: true })
- copyToClipboard(recipientAddress)
+ copyToClipboard(checksummedRecipientAddress)
}}
>
- <div className="sender-to-recipient__sender-icon">
- <Identicon
- address={recipientAddress}
- diameter={24}
- />
- </div>
+ { this.renderRecipientIdenticon() }
<Tooltip
position="bottom"
title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
@@ -47,8 +104,12 @@ export default class SenderToRecipient extends Component {
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ recipientAddressCopied: false })}
>
- <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
- { recipientName || this.context.t('newContract') }
+ <div className="sender-to-recipient__name">
+ {
+ addressOnly
+ ? `${t('to')}: ${checksummedRecipientAddress}`
+ : (recipientName || this.context.t('newContract'))
+ }
</div>
</Tooltip>
</div>
@@ -57,46 +118,25 @@ export default class SenderToRecipient extends Component {
renderRecipientWithoutAddress () {
return (
- <div className="sender-to-recipient__recipient">
- <i className="fa fa-file-text-o" />
- <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ <div className="sender-to-recipient__party sender-to-recipient__party--recipient">
+ { !this.props.addressOnly && <i className="fa fa-file-text-o" /> }
+ <div className="sender-to-recipient__name">
{ this.context.t('newContract') }
</div>
</div>
)
}
- render () {
- const { t } = this.context
- const { senderName, senderAddress, recipientAddress } = this.props
-
- return (
- <div className="sender-to-recipient__container">
- <div
- className="sender-to-recipient__sender"
- onClick={() => {
- this.setState({ senderAddressCopied: true })
- copyToClipboard(senderAddress)
- }}
- >
- <div className="sender-to-recipient__sender-icon">
- <Identicon
- address={senderAddress}
- diameter={24}
- />
- </div>
- <Tooltip
- position="bottom"
- title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
- wrapperClassName="sender-to-recipient__tooltip-wrapper"
- containerClassName="sender-to-recipient__tooltip-container"
- onHidden={() => this.setState({ senderAddressCopied: false })}
- >
- <div className="sender-to-recipient__name sender-to-recipient__sender-name">
- { senderName }
- </div>
- </Tooltip>
+ renderArrow () {
+ return this.props.variant === CARDS_VARIANT
+ ? (
+ <div className="sender-to-recipient__arrow-container">
+ <img
+ height={20}
+ src="./images/caret-right.svg"
+ />
</div>
+ ) : (
<div className="sender-to-recipient__arrow-container">
<div className="sender-to-recipient__arrow-circle">
<img
@@ -106,6 +146,26 @@ export default class SenderToRecipient extends Component {
/>
</div>
</div>
+ )
+ }
+
+ render () {
+ const { senderAddress, recipientAddress, variant } = this.props
+ const checksummedSenderAddress = checksumAddress(senderAddress)
+
+ return (
+ <div className={classnames(variantHash[variant])}>
+ <div
+ className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
+ onClick={() => {
+ this.setState({ senderAddressCopied: true })
+ copyToClipboard(checksummedSenderAddress)
+ }}
+ >
+ { this.renderSenderIdenticon() }
+ { this.renderSenderAddress() }
+ </div>
+ { this.renderArrow() }
{
recipientAddress
? this.renderRecipientWithAddress()
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
new file mode 100644
index 000000000..166228932
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
@@ -0,0 +1,3 @@
+// Component design variants
+export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
+export const CARDS_VARIANT = 'CARDS_VARIANT'
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
index 2c4ba40bf..a842bcc8b 100644
--- a/ui/app/components/shapeshift-form.js
+++ b/ui/app/components/shapeshift-form.js
@@ -9,6 +9,8 @@ const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../actions
const { isValidAddress } = require('../util')
const SimpleDropdown = require('./dropdowns/simple-dropdown')
+import Button from './button'
+
function mapStateToProps (state) {
const {
coinOptions,
@@ -242,8 +244,10 @@ ShapeshiftForm.prototype.render = function () {
]),
- !depositAddress && h('button.btn-primary.btn--large.shapeshift-form__shapeshift-buy-btn', {
- className: btnClass,
+ !depositAddress && h(Button, {
+ type: 'primary',
+ large: true,
+ className: `${btnClass} shapeshift-form__shapeshift-buy-btn`,
disabled: !token,
onClick: () => this.onBuyWithShapeShift(),
}, [this.context.t('buy')]),
diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js
index 4334aacba..b87bf959e 100644
--- a/ui/app/components/shift-list-item.js
+++ b/ui/app/components/shift-list-item.js
@@ -35,12 +35,13 @@ function ShiftListItem () {
}
ShiftListItem.prototype.render = function () {
- return h('div.tx-list-item.tx-list-clickable', {
+ return h('div.transaction-list-item.tx-list-clickable', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
justifyContent: 'space-around',
alignItems: 'center',
+ flexDirection: 'row',
},
}, [
h('div', {
diff --git a/ui/app/components/sidebars/index.js b/ui/app/components/sidebars/index.js
new file mode 100644
index 000000000..732925f69
--- /dev/null
+++ b/ui/app/components/sidebars/index.js
@@ -0,0 +1 @@
+export { default } from './sidebar.component'
diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/sidebars/index.scss
new file mode 100644
index 000000000..5ab0664df
--- /dev/null
+++ b/ui/app/components/sidebars/index.scss
@@ -0,0 +1,74 @@
+.sidebar-right-enter {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(-100%);
+}
+
+.sidebar-right-enter.sidebar-right-enter-active {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(0%);
+}
+
+.sidebar-right-leave {
+ transition: transform 200ms ease-out;
+ transform: translateX(0%);
+}
+
+.sidebar-right-leave.sidebar-right-leave-active {
+ transition: transform 200ms ease-out;
+ transform: translateX(-100%);
+}
+
+.sidebar-left-enter {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(100%);
+}
+
+.sidebar-left-enter.sidebar-left-enter-active {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(0%);
+}
+
+.sidebar-left-leave {
+ transition: transform 200ms ease-out;
+ transform: translateX(0%);
+}
+
+.sidebar-left-leave.sidebar-left-leave-active {
+ transition: transform 200ms ease-out;
+ transform: translateX(100%);
+}
+
+.sidebar-left {
+ flex: 1 0 230px;
+ background: rgb(250, 250, 250);
+ z-index: $sidebar-z-index;
+ position: fixed;
+ left: 15%;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ will-change: transform;
+ overflow-y: auto;
+ box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px;
+ width: 85%;
+ height: 100%;
+
+ @media screen and (min-width: 769px) {
+ width: 408px;
+ left: calc(100% - 408px);
+ }
+}
+
+.sidebar-overlay {
+ z-index: $sidebar-overlay-z-index;
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ background-color: rgba(0, 0, 0, .3);
+} \ No newline at end of file
diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js
new file mode 100644
index 000000000..57cdd7111
--- /dev/null
+++ b/ui/app/components/sidebars/sidebar.component.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import WalletView from '../wallet-view'
+import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
+
+export default class Sidebar extends Component {
+
+ static propTypes = {
+ sidebarOpen: PropTypes.bool,
+ hideSidebar: PropTypes.func,
+ transitionName: PropTypes.string,
+ type: PropTypes.string,
+ };
+
+ renderOverlay () {
+ return <div className="sidebar-overlay" onClick={() => this.props.hideSidebar()} />
+ }
+
+ renderSidebarContent () {
+ const { type } = this.props
+
+ switch (type) {
+ case WALLET_VIEW_SIDEBAR:
+ return <WalletView responsiveDisplayClassname={'sidebar-right' } />
+ default:
+ return null
+ }
+
+ }
+
+ render () {
+ const { transitionName, sidebarOpen } = this.props
+
+ return (
+ <div>
+ <ReactCSSTransitionGroup
+ transitionName={transitionName}
+ transitionEnterTimeout={300}
+ transitionLeaveTimeout={200}
+ >
+ { sidebarOpen ? this.renderSidebarContent() : null }
+ </ReactCSSTransitionGroup>
+ { sidebarOpen ? this.renderOverlay() : null }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/sidebars/sidebar.constants.js b/ui/app/components/sidebars/sidebar.constants.js
new file mode 100644
index 000000000..1613a8245
--- /dev/null
+++ b/ui/app/components/sidebars/sidebar.constants.js
@@ -0,0 +1 @@
+export const WALLET_VIEW_SIDEBAR = 'wallet-view'
diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/sidebars/tests/sidebars-component.test.js
new file mode 100644
index 000000000..e2d77518a
--- /dev/null
+++ b/ui/app/components/sidebars/tests/sidebars-component.test.js
@@ -0,0 +1,88 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import Sidebar from '../sidebar.component.js'
+
+import WalletView from '../../wallet-view'
+
+const propsMethodSpies = {
+ hideSidebar: sinon.spy(),
+}
+
+describe('Sidebar Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<Sidebar
+ sidebarOpen={false}
+ hideSidebar={propsMethodSpies.hideSidebar}
+ transitionName={'someTransition'}
+ type={'wallet-view'}
+ />)
+ })
+
+ afterEach(() => {
+ propsMethodSpies.hideSidebar.resetHistory()
+ })
+
+ describe('renderOverlay', () => {
+ let renderOverlay
+
+ beforeEach(() => {
+ renderOverlay = shallow(wrapper.instance().renderOverlay())
+ })
+
+ it('should render a overlay element', () => {
+ assert(renderOverlay.hasClass('sidebar-overlay'))
+ })
+
+ it('should pass the correct onClick function to the element', () => {
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 0)
+ renderOverlay.props().onClick()
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 1)
+ })
+ })
+
+ describe('renderSidebarContent', () => {
+ let renderSidebarContent
+
+ beforeEach(() => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ })
+
+ it('should render sidebar content with the correct props', () => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
+ })
+
+ it('should not render with an unrecognized type', () => {
+ wrapper.setProps({ type: 'foobar' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent, undefined)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with one child', () => {
+ assert(wrapper.is('div'))
+ assert.equal(wrapper.children().length, 1)
+ })
+
+ it('should render the ReactCSSTransitionGroup without any children', () => {
+ assert(wrapper.children().at(0).is(ReactCSSTransitionGroup))
+ assert.equal(wrapper.children().at(0).children().length, 0)
+ })
+
+ it('should render sidebar content and the overlay if sidebarOpen is true', () => {
+ wrapper.setProps({ sidebarOpen: true })
+ assert.equal(wrapper.children().length, 2)
+ assert(wrapper.children().at(1).hasClass('sidebar-overlay'))
+ assert.equal(wrapper.children().at(0).children().length, 1)
+ assert(wrapper.children().at(0).children().at(0).is(WalletView))
+ })
+ })
+})
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index 2e0102d1a..d76eb5ef8 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -8,6 +8,7 @@ const ethUtil = require('ethereumjs-util')
const classnames = require('classnames')
const { compose } = require('recompose')
const { withRouter } = require('react-router-dom')
+const { ObjectInspector } = require('react-inspector')
const AccountDropdownMini = require('./dropdowns/account-dropdown-mini')
@@ -23,6 +24,7 @@ const {
} = require('../selectors.js')
import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
+import Button from './button'
const { DEFAULT_ROUTE } = require('../routes')
@@ -168,12 +170,29 @@ SignatureRequest.prototype.msgHexToText = function (hex) {
}
}
+// eslint-disable-next-line react/display-name
+SignatureRequest.prototype.renderTypedDataV3 = function (data) {
+ const { domain, message } = JSON.parse(data)
+ return [
+ h('div.request-signature__typed-container', [
+ domain ? h('div', [
+ h('h1', 'Domain'),
+ h(ObjectInspector, { data: domain, expandLevel: 1, name: 'domain' }),
+ ]) : '',
+ message ? h('div', [
+ h('h1', 'Message'),
+ h(ObjectInspector, { data: message, expandLevel: 1, name: 'message' }),
+ ]) : '',
+ ]),
+ ]
+}
+
SignatureRequest.prototype.renderBody = function () {
let rows
let notice = this.context.t('youSign') + ':'
const { txData } = this.props
- const { type, msgParams: { data } } = txData
+ const { type, msgParams: { data, version } } = txData
if (type === 'personal_sign') {
rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }]
@@ -185,7 +204,7 @@ SignatureRequest.prototype.renderBody = function () {
h('span.request-signature__help-link', {
onClick: () => {
global.platform.openWindow({
- url: 'https://consensys.zendesk.com/hc/en-us/articles/360004427792',
+ url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751',
})
},
}, this.context.t('learnMore'))]
@@ -204,9 +223,9 @@ SignatureRequest.prototype.renderBody = function () {
}),
}, [notice]),
- h('div.request-signature__rows', [
-
- ...rows.map(({ name, value }) => {
+ h('div.request-signature__rows', type === 'eth_signTypedData' && version === 'V3' ?
+ this.renderTypedDataV3(data) :
+ rows.map(({ name, value }) => {
if (typeof value === 'boolean') {
value = value.toString()
}
@@ -215,9 +234,7 @@ SignatureRequest.prototype.renderBody = function () {
h('div.request-signature__row-value', value),
])
}),
-
- ]),
-
+ ),
])
}
@@ -248,7 +265,10 @@ SignatureRequest.prototype.renderFooter = function () {
}
return h('div.request-signature__footer', [
- h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'request-signature__footer__cancel-button',
onClick: event => {
cancel(event).then(() => {
this.props.clearConfirmTransaction()
@@ -256,7 +276,9 @@ SignatureRequest.prototype.renderFooter = function () {
})
},
}, this.context.t('cancel')),
- h('button.btn-primary.btn--large', {
+ h(Button, {
+ type: 'primary',
+ large: true,
onClick: event => {
sign(event).then(() => {
this.props.clearConfirmTransaction()
diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/tabs/tab/tab.component.js
index a59da8904..9e590391c 100644
--- a/ui/app/components/tabs/tab/tab.component.js
+++ b/ui/app/components/tabs/tab/tab.component.js
@@ -3,13 +3,13 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
const Tab = props => {
- const { name, onClick, isActive, tabIndex } = props
+ const { name, onClick, isActive, tabIndex, className, activeClassName } = props
return (
<li
className={classnames(
- 'tab',
- isActive && 'tab--active',
+ className,
+ { [activeClassName]: isActive },
)}
onClick={event => {
event.preventDefault()
@@ -26,6 +26,13 @@ Tab.propTypes = {
onClick: PropTypes.func,
isActive: PropTypes.bool,
tabIndex: PropTypes.number,
+ className: PropTypes.string,
+ activeClassName: PropTypes.string,
+}
+
+Tab.defaultProps = {
+ className: 'tab',
+ activeClassName: 'tab--active',
}
export default Tab
diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js
deleted file mode 100644
index 99ca7335c..000000000
--- a/ui/app/components/token-balance.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const TokenTracker = require('eth-token-tracker')
-const connect = require('react-redux').connect
-const selectors = require('../selectors')
-const log = require('loglevel')
-
-function mapStateToProps (state) {
- return {
- userAddress: selectors.getSelectedAddress(state),
- }
-}
-
-module.exports = connect(mapStateToProps)(TokenBalance)
-
-
-inherits(TokenBalance, Component)
-function TokenBalance () {
- this.state = {
- string: '',
- symbol: '',
- isLoading: true,
- error: null,
- }
- Component.call(this)
-}
-
-TokenBalance.prototype.render = function () {
- const state = this.state
- const { symbol, string, isLoading } = state
- const { balanceOnly } = this.props
-
- return isLoading
- ? h('span', '')
- : h('span.token-balance', [
- h('span.hide-text-overflow.token-balance__amount', string),
- !balanceOnly && h('span.token-balance__symbol', symbol),
- ])
-}
-
-TokenBalance.prototype.componentDidMount = function () {
- this.createFreshTokenTracker()
-}
-
-TokenBalance.prototype.createFreshTokenTracker = function () {
- if (this.tracker) {
- // Clean up old trackers when refreshing:
- this.tracker.stop()
- this.tracker.removeListener('update', this.balanceUpdater)
- this.tracker.removeListener('error', this.showError)
- }
-
- if (!global.ethereumProvider) return
- const { userAddress, token } = this.props
-
- this.tracker = new TokenTracker({
- userAddress,
- provider: global.ethereumProvider,
- tokens: [token],
- pollingInterval: 8000,
- })
-
-
- // Set up listener instances for cleaning up
- this.balanceUpdater = this.updateBalance.bind(this)
- this.showError = error => {
- this.setState({ error, isLoading: false })
- }
- this.tracker.on('update', this.balanceUpdater)
- this.tracker.on('error', this.showError)
-
- this.tracker.updateBalances()
- .then(() => {
- this.updateBalance(this.tracker.serialize())
- })
- .catch((reason) => {
- log.error(`Problem updating balances`, reason)
- this.setState({ isLoading: false })
- })
-}
-
-TokenBalance.prototype.componentDidUpdate = function (nextProps) {
- const {
- userAddress: oldAddress,
- token: { address: oldTokenAddress },
- } = this.props
- const {
- userAddress: newAddress,
- token: { address: newTokenAddress },
- } = nextProps
-
- if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
- if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
-
- this.setState({ isLoading: true })
- this.createFreshTokenTracker()
-}
-
-TokenBalance.prototype.updateBalance = function (tokens = []) {
- if (!this.tracker.running) {
- return
- }
-
- const [{ string, symbol }] = tokens
-
- this.setState({
- string,
- symbol,
- isLoading: false,
- })
-}
-
-TokenBalance.prototype.componentWillUnmount = function () {
- if (!this.tracker) return
- this.tracker.stop()
- this.tracker.removeListener('update', this.balanceUpdater)
- this.tracker.removeListener('error', this.showError)
-}
-
diff --git a/ui/app/components/token-balance/index.js b/ui/app/components/token-balance/index.js
new file mode 100644
index 000000000..f7da15cf8
--- /dev/null
+++ b/ui/app/components/token-balance/index.js
@@ -0,0 +1 @@
+export { default } from './token-balance.container'
diff --git a/ui/app/components/token-balance/token-balance.component.js b/ui/app/components/token-balance/token-balance.component.js
new file mode 100644
index 000000000..2b4f73980
--- /dev/null
+++ b/ui/app/components/token-balance/token-balance.component.js
@@ -0,0 +1,23 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class TokenBalance extends PureComponent {
+ static propTypes = {
+ string: PropTypes.string,
+ symbol: PropTypes.string,
+ error: PropTypes.string,
+ className: PropTypes.string,
+ withSymbol: PropTypes.bool,
+ }
+
+ render () {
+ const { className, string, withSymbol, symbol } = this.props
+
+ return (
+ <div className={classnames('hide-text-overflow', className)}>
+ { string + (withSymbol ? ` ${symbol}` : '') }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/token-balance/token-balance.container.js
index bc1289ce1..adc001f83 100644
--- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
+++ b/ui/app/components/token-balance/token-balance.container.js
@@ -1,8 +1,8 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
-import withTokenTracker from '../../../../helpers/with-token-tracker'
+import withTokenTracker from '../../higher-order-components/with-token-tracker'
import TokenBalance from './token-balance.component'
-import selectors from '../../../../selectors'
+import selectors from '../../selectors'
const mapStateToProps = state => {
return {
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
index 4100d76a5..477d97597 100644
--- a/ui/app/components/token-cell.js
+++ b/ui/app/components/token-cell.js
@@ -18,7 +18,7 @@ function mapStateToProps (state) {
userAddress: selectors.getSelectedAddress(state),
contractExchangeRates: state.metamask.contractExchangeRates,
conversionRate: state.metamask.conversionRate,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
}
}
@@ -56,8 +56,8 @@ TokenCell.prototype.render = function () {
sidebarOpen,
currentCurrency,
// userAddress,
+ image,
} = props
-
let currentTokenToFiatRate
let currentTokenInFiat
let formattedFiat = ''
@@ -97,6 +97,7 @@ TokenCell.prototype.render = function () {
diameter: 50,
address,
network,
+ image,
}),
h('div.token-list-item__balance-ellipsis', null, [
diff --git a/ui/app/components/token-currency-display/index.js b/ui/app/components/token-currency-display/index.js
new file mode 100644
index 000000000..6065cae1f
--- /dev/null
+++ b/ui/app/components/token-currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './token-currency-display.component'
diff --git a/ui/app/components/token-currency-display/token-currency-display.component.js b/ui/app/components/token-currency-display/token-currency-display.component.js
new file mode 100644
index 000000000..4bb09a4b6
--- /dev/null
+++ b/ui/app/components/token-currency-display/token-currency-display.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../currency-display/currency-display.component'
+import { getTokenData } from '../../helpers/transactions.util'
+import { getTokenValue, calcTokenAmount } from '../../token-util'
+
+export default class TokenCurrencyDisplay extends PureComponent {
+ static propTypes = {
+ transactionData: PropTypes.string,
+ token: PropTypes.object,
+ }
+
+ state = {
+ displayValue: '',
+ }
+
+ componentDidMount () {
+ this.setDisplayValue()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { transactionData } = this.props
+ const { transactionData: prevTransactionData } = prevProps
+
+ if (transactionData !== prevTransactionData) {
+ this.setDisplayValue()
+ }
+ }
+
+ setDisplayValue () {
+ const { transactionData: data, token } = this.props
+ const { decimals = '', symbol = '' } = token
+ const tokenData = getTokenData(data)
+
+ let displayValue
+
+ if (tokenData.params && tokenData.params.length) {
+ const tokenValue = getTokenValue(tokenData.params)
+ const tokenAmount = calcTokenAmount(tokenValue, decimals)
+ displayValue = `${tokenAmount} ${symbol}`
+ }
+
+ this.setState({ displayValue })
+ }
+
+ render () {
+ return (
+ <CurrencyDisplay
+ {...this.props}
+ displayValue={this.state.displayValue}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 42351cf89..6a88f30bf 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -13,6 +13,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: selectors.getSelectedAddress(state),
+ assetImages: state.metamask.assetImages,
}
}
@@ -44,10 +45,9 @@ function TokenList () {
}
TokenList.prototype.render = function () {
- const { userAddress } = this.props
+ const { userAddress, assetImages } = this.props
const state = this.state
const { tokens, isLoading, error } = state
-
if (isLoading) {
return this.message(this.context.t('loadingTokens'))
}
@@ -74,7 +74,10 @@ TokenList.prototype.render = function () {
])
}
- return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
+ return h('div', tokens.map((tokenData) => {
+ tokenData.image = assetImages[tokenData.address]
+ return h(TokenCell, tokenData)
+ }))
}
diff --git a/ui/app/components/tooltip-v2.js b/ui/app/components/tooltip-v2.js
index 05a5efc80..054782203 100644
--- a/ui/app/components/tooltip-v2.js
+++ b/ui/app/components/tooltip-v2.js
@@ -1,33 +1,66 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const ReactTippy = require('react-tippy').Tooltip
+import PropTypes from 'prop-types'
+import React, {PureComponent} from 'react'
+import {Tooltip as ReactTippy} from 'react-tippy'
-module.exports = Tooltip
+export default class Tooltip extends PureComponent {
+ static defaultProps = {
+ arrow: true,
+ children: null,
+ containerClassName: '',
+ hideOnClick: false,
+ onHidden: null,
+ position: 'left',
+ size: 'small',
+ title: null,
+ trigger: 'mouseenter',
+ wrapperClassName: '',
+ }
-inherits(Tooltip, Component)
-function Tooltip () {
- Component.call(this)
-}
-
-Tooltip.prototype.render = function () {
- const props = this.props
- const { position, title, children, wrapperClassName, containerClassName, onHidden } = props
+ static propTypes = {
+ arrow: PropTypes.bool,
+ children: PropTypes.node,
+ containerClassName: PropTypes.string,
+ onHidden: PropTypes.func,
+ position: PropTypes.oneOf([
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ ]),
+ size: PropTypes.oneOf([
+ 'small', 'regular', 'big',
+ ]),
+ title: PropTypes.string,
+ trigger: PropTypes.any,
+ wrapperClassName: PropTypes.string,
+ }
- return h('div', {
- className: wrapperClassName,
- }, [
+ render () {
+ const {arrow, children, containerClassName, position, size, title, trigger, onHidden, wrapperClassName } = this.props
- h(ReactTippy, {
- title,
- position: position || 'left',
- trigger: 'mouseenter',
- hideOnClick: false,
- size: 'small',
- arrow: true,
- className: containerClassName,
- onHidden,
- }, children),
+ if (!title) {
+ return (
+ <div className={wrapperClassName}>
+ {children}
+ </div>
+ )
+ }
- ])
+ return (
+ <div className={wrapperClassName}>
+ <ReactTippy
+ className={containerClassName}
+ title={title}
+ position={position}
+ trigger={trigger}
+ hideOnClick={false}
+ size={size}
+ arrow={arrow}
+ onHidden={onHidden}
+ >
+ {children}
+ </ReactTippy>
+ </div>
+ )
+ }
}
diff --git a/ui/app/components/transaction-action/index.js b/ui/app/components/transaction-action/index.js
new file mode 100644
index 000000000..a6e9097f1
--- /dev/null
+++ b/ui/app/components/transaction-action/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-action.component'
diff --git a/ui/app/components/transaction-action/tests/transaction-action.component.test.js b/ui/app/components/transaction-action/tests/transaction-action.component.test.js
new file mode 100644
index 000000000..b22a9db39
--- /dev/null
+++ b/ui/app/components/transaction-action/tests/transaction-action.component.test.js
@@ -0,0 +1,162 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TransactionAction from '../transaction-action.component'
+
+describe('TransactionAction Component', () => {
+ const t = key => key
+
+
+ describe('Outgoing transaction', () => {
+ beforeEach(() => {
+ global.eth = {
+ getCode: sinon.stub().callsFake(address => {
+ const code = address === 'approveAddress' ? 'contract' : '0x'
+ return Promise.resolve(code)
+ }),
+ }
+ })
+
+ it('should render -- when methodData is still fetching', () => {
+ const methodData = { data: {}, done: false, error: null }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(<TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="transaction-action"
+ />, { context: { t }})
+
+ assert.equal(wrapper.find('.transaction-action').length, 1)
+ assert.equal(wrapper.text(), '--')
+ })
+
+ it('should render Sent Ether', () => {
+ const methodData = { data: {}, done: true, error: null }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: 'sentEtherAddress',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(<TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="transaction-action"
+ />, { context: { t }})
+
+ assert.equal(wrapper.find('.transaction-action').length, 1)
+ wrapper.setState({ transactionAction: 'sentEther' })
+ assert.equal(wrapper.text(), 'sentEther')
+ })
+
+ it('should render Approved', async () => {
+ const methodData = {
+ data: {
+ name: 'Approve',
+ params: [
+ { type: 'address' },
+ { type: 'uint256' },
+ ],
+ },
+ done: true,
+ error: null,
+ }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: 'approveAddress',
+ value: '0x2386f26fc10000',
+ data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="test-class"
+ />,
+ { context: { t } }
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.test-class').length, 1)
+ await wrapper.instance().getTransactionAction()
+ assert.equal(wrapper.state('transactionAction'), 'approve')
+ })
+
+ it('should render Accept Fulfillment', async () => {
+ const methodData = {
+ data: {
+ name: 'AcceptFulfillment',
+ params: [
+ { type: 'address' },
+ { type: 'uint256' },
+ ],
+ },
+ done: true,
+ error: null,
+ }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: 'approveAddress',
+ value: '0x2386f26fc10000',
+ data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="test-class"
+ />,
+ { context: { t }}
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.test-class').length, 1)
+ await wrapper.instance().getTransactionAction()
+ assert.equal(wrapper.state('transactionAction'), ' Accept Fulfillment')
+ })
+ })
+})
diff --git a/ui/app/components/transaction-action/transaction-action.component.js b/ui/app/components/transaction-action/transaction-action.component.js
new file mode 100644
index 000000000..1de91cb71
--- /dev/null
+++ b/ui/app/components/transaction-action/transaction-action.component.js
@@ -0,0 +1,58 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { getTransactionActionKey } from '../../helpers/transactions.util'
+import { camelCaseToCapitalize } from '../../helpers/common.util'
+
+export default class TransactionAction extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ className: PropTypes.string,
+ transaction: PropTypes.object,
+ methodData: PropTypes.object,
+ }
+
+ state = {
+ transactionAction: '',
+ }
+
+ componentDidMount () {
+ this.getTransactionAction()
+ }
+
+ componentDidUpdate () {
+ this.getTransactionAction()
+ }
+
+ async getTransactionAction () {
+ const { transactionAction } = this.state
+ const { transaction, methodData } = this.props
+ const { data, done } = methodData
+ const { name = '' } = data
+
+ if (!done || transactionAction) {
+ return
+ }
+
+ const actionKey = await getTransactionActionKey(transaction, data)
+ const action = actionKey
+ ? this.context.t(actionKey)
+ : camelCaseToCapitalize(name)
+
+ this.setState({ transactionAction: action })
+ }
+
+ render () {
+ const { className, methodData: { done } } = this.props
+ const { transactionAction } = this.state
+
+ return (
+ <div className={classnames('transaction-action', className)}>
+ { (done && transactionAction) || '--' }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/index.js b/ui/app/components/transaction-activity-log/index.js
new file mode 100644
index 000000000..a33da15a3
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-activity-log.container'
diff --git a/ui/app/components/transaction-activity-log/index.scss b/ui/app/components/transaction-activity-log/index.scss
new file mode 100644
index 000000000..27f3006b3
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/index.scss
@@ -0,0 +1,70 @@
+.transaction-activity-log {
+ &__card {
+ background: $white;
+ height: 100%;
+ }
+
+ &__activities-container {
+ padding-top: 8px;
+ }
+
+ &__activity {
+ padding: 4px 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 6px;
+ border-right: 1px solid $scorpion;
+ }
+
+ &:first-child::after {
+ height: 50%;
+ top: 50%;
+ }
+
+ &:last-child::after {
+ height: 50%;
+ }
+
+ &:first-child:last-child::after {
+ display: none;
+ }
+ }
+
+ &__activity-icon {
+ width: 13px;
+ height: 13px;
+ margin-right: 6px;
+ border-radius: 50%;
+ background: $scorpion;
+ flex: 0 0 auto;
+ }
+
+ &__activity-text {
+ color: $scorpion;
+ font-size: .75rem;
+
+ @media screen and (min-width: $break-large) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ &__value {
+ display: inline;
+ font-weight: 500;
+ }
+
+ b {
+ font-weight: 500;
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
new file mode 100644
index 000000000..8687dbbc7
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionActivityLog from '../transaction-activity-log.component'
+import Card from '../../card'
+
+describe('TransactionActivityLog Component', () => {
+ it('should render properly', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionActivityLog
+ transaction={transaction}
+ className="test-class"
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-activity-log'))
+ assert.ok(wrapper.hasClass('test-class'))
+ assert.equal(wrapper.find(Card).length, 1)
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
new file mode 100644
index 000000000..85d56a6a2
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
@@ -0,0 +1,27 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../transaction-activity-log.container.js', {
+ 'react-redux': {
+ connect: ms => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+})
+
+describe('TransactionActivityLog container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 280.45,
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45 })
+ })
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
new file mode 100644
index 000000000..586500408
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
@@ -0,0 +1,208 @@
+import assert from 'assert'
+import { getActivities } from '../transaction-activity-log.util'
+
+describe('getActivities', () => {
+ it('should return no activities for an empty history', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ assert.deepEqual(getActivities(transaction), [])
+ })
+
+ it('should return activities for a transaction\'s history', () => {
+ const transaction = {
+ history: [
+ {
+ id: 5559712943815343,
+ loadingDefaults: true,
+ metamaskNetworkId: '3',
+ status: 'unapproved',
+ time: 1535507561452,
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ },
+ [
+ {
+ op: 'replace',
+ path: '/loadingDefaults',
+ timestamp: 1535507561515,
+ value: false,
+ },
+ {
+ op: 'add',
+ path: '/gasPriceSpecified',
+ value: true,
+ },
+ {
+ op: 'add',
+ path: '/gasLimitSpecified',
+ value: true,
+ },
+ {
+ op: 'add',
+ path: '/estimatedGas',
+ value: '0x5208',
+ },
+ ],
+ [
+ {
+ note: '#newUnapprovedTransaction - adding the origin',
+ op: 'add',
+ path: '/origin',
+ timestamp: 1535507561516,
+ value: 'MetaMask',
+ },
+ [],
+ ],
+ [
+ {
+ note: 'confTx: user approved transaction',
+ op: 'replace',
+ path: '/txParams/gasPrice',
+ timestamp: 1535664571504,
+ value: '0x77359400',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to approved',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564302,
+ value: 'approved',
+ },
+ ],
+ [
+ {
+ note: 'transactions#approveTransaction',
+ op: 'add',
+ path: '/txParams/nonce',
+ timestamp: 1535507564439,
+ value: '0xa4',
+ },
+ {
+ op: 'add',
+ path: '/nonceDetails',
+ value: {
+ local: {},
+ network: {},
+ params: {},
+ },
+ },
+ ],
+ [
+ {
+ note: 'transactions#publishTransaction',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564518,
+ value: 'signed',
+ },
+ {
+ op: 'add',
+ path: '/rawTx',
+ value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf',
+ },
+ ],
+ [],
+ [
+ {
+ note: 'transactions#setTxHash',
+ op: 'add',
+ path: '/hash',
+ timestamp: 1535507564658,
+ value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager - add submitted time stamp',
+ op: 'add',
+ path: '/submittedTime',
+ timestamp: 1535507564660,
+ value: 1535507564660,
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to submitted',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564665,
+ value: 'submitted',
+ },
+ ],
+ [
+ {
+ note: 'transactions/pending-tx-tracker#event: tx:block-update',
+ op: 'add',
+ path: '/firstRetryBlockNumber',
+ timestamp: 1535507575476,
+ value: '0x3bf624',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to confirmed',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507615993,
+ value: 'confirmed',
+ },
+ ],
+ ],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const expectedResult = [
+ {
+ 'eventKey': 'transactionCreated',
+ 'timestamp': 1535507561452,
+ 'value': '0x2386f26fc10000',
+ },
+ {
+ 'eventKey': 'transactionUpdatedGas',
+ 'timestamp': 1535664571504,
+ 'value': '0x77359400',
+ },
+ {
+ 'eventKey': 'transactionSubmitted',
+ 'timestamp': 1535507564665,
+ 'value': undefined,
+ },
+ {
+ 'eventKey': 'transactionConfirmed',
+ 'timestamp': 1535507615993,
+ 'value': undefined,
+ },
+ ]
+
+ assert.deepEqual(getActivities(transaction), expectedResult)
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
new file mode 100644
index 000000000..c4cf57d14
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
@@ -0,0 +1,91 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { getActivities } from './transaction-activity-log.util'
+import Card from '../card'
+import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
+import { ETH } from '../../constants/common'
+import { formatDate } from '../../util'
+
+export default class TransactionActivityLog extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ transaction: PropTypes.object,
+ className: PropTypes.string,
+ conversionRate: PropTypes.number,
+ }
+
+ state = {
+ activities: [],
+ }
+
+ componentDidMount () {
+ this.setActivites()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { transaction: { history: prevHistory = [] } = {} } = prevProps
+ const { transaction: { history = [] } = {} } = this.props
+
+ if (prevHistory.length !== history.length) {
+ this.setActivites()
+ }
+ }
+
+ setActivites () {
+ const activities = getActivities(this.props.transaction)
+ this.setState({ activities })
+ }
+
+ renderActivity (activity, index) {
+ const { conversionRate } = this.props
+ const { eventKey, value, timestamp } = activity
+ const ethValue = index === 0
+ ? `${getValueFromWeiHex({
+ value,
+ toCurrency: ETH,
+ conversionRate,
+ numberOfDecimals: 6,
+ })} ${ETH}`
+ : getEthConversionFromWeiHex({ value, toCurrency: ETH, conversionRate })
+ const formattedTimestamp = formatDate(timestamp)
+ const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
+
+ return (
+ <div
+ key={index}
+ className="transaction-activity-log__activity"
+ >
+ <div className="transaction-activity-log__activity-icon" />
+ <div
+ className="transaction-activity-log__activity-text"
+ title={activityText}
+ >
+ { activityText }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { className } = this.props
+ const { activities } = this.state
+
+ return (
+ <div className={classnames('transaction-activity-log', className)}>
+ <Card
+ title={t('activityLog')}
+ className="transaction-activity-log__card"
+ >
+ <div className="transaction-activity-log__activities-container">
+ { activities.map((activity, index) => this.renderActivity(activity, index)) }
+ </div>
+ </Card>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
new file mode 100644
index 000000000..4c8b6d971
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import TransactionActivityLog from './transaction-activity-log.component'
+import { conversionRateSelector } from '../../selectors'
+
+const mapStateToProps = state => {
+ return {
+ conversionRate: conversionRateSelector(state),
+ }
+}
+
+export default connect(mapStateToProps)(TransactionActivityLog)
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
new file mode 100644
index 000000000..32834ff47
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
@@ -0,0 +1,82 @@
+// path constants
+const STATUS_PATH = '/status'
+const GAS_PRICE_PATH = '/txParams/gasPrice'
+
+// status constants
+const UNAPPROVED_STATUS = 'unapproved'
+const SUBMITTED_STATUS = 'submitted'
+const CONFIRMED_STATUS = 'confirmed'
+const DROPPED_STATUS = 'dropped'
+
+// op constants
+const REPLACE_OP = 'replace'
+
+// event constants
+const TRANSACTION_CREATED_EVENT = 'transactionCreated'
+const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas'
+const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
+const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
+const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
+const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
+
+const eventPathsHash = {
+ [STATUS_PATH]: true,
+ [GAS_PRICE_PATH]: true,
+}
+
+const statusHash = {
+ [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
+ [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
+ [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
+}
+
+function eventCreator (eventKey, timestamp, value) {
+ return {
+ eventKey,
+ timestamp,
+ value,
+ }
+}
+
+export function getActivities (transaction) {
+ const { history = [] } = transaction
+
+ return history.reduce((acc, base) => {
+ // First history item should be transaction creation
+ if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
+ const { time, txParams: { value } = {} } = base
+ return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
+ } else if (Array.isArray(base)) {
+ const events = []
+
+ base.forEach(entry => {
+ const { op, path, value, timestamp } = entry
+
+ if (path in eventPathsHash && op === REPLACE_OP) {
+ switch (path) {
+ case STATUS_PATH: {
+ if (value in statusHash) {
+ events.push(eventCreator(statusHash[value], timestamp))
+ }
+
+ break
+ }
+
+ case GAS_PRICE_PATH: {
+ events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value))
+ break
+ }
+
+ default: {
+ events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp))
+ }
+ }
+ }
+ })
+
+ return acc.concat(events)
+ }
+
+ return acc
+ }, [])
+}
diff --git a/ui/app/components/transaction-breakdown/index.js b/ui/app/components/transaction-breakdown/index.js
new file mode 100644
index 000000000..c887f504f
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-breakdown.component'
diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/transaction-breakdown/index.scss
new file mode 100644
index 000000000..1bb108943
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/index.scss
@@ -0,0 +1,23 @@
+@import './transaction-breakdown-row/index';
+
+.transaction-breakdown {
+ &__card {
+ background: $white;
+ height: 100%;
+ }
+
+ &__row-title {
+ text-transform: capitalize;
+ }
+
+ &__value {
+ text-align: end;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &--eth-total {
+ font-weight: 500;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
new file mode 100644
index 000000000..d18cd420c
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionBreakdown from '../transaction-breakdown.component'
+import TransactionBreakdownRow from '../transaction-breakdown-row'
+import Card from '../../card'
+
+describe('TransactionBreakdown Component', () => {
+ it('should render properly', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionBreakdown
+ transaction={transaction}
+ className="test-class"
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-breakdown'))
+ assert.ok(wrapper.hasClass('test-class'))
+ assert.equal(wrapper.find(Card).length, 1)
+ assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4)
+ })
+})
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
new file mode 100644
index 000000000..557bf75fb
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-breakdown-row.component'
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
new file mode 100644
index 000000000..8c73be1a6
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
@@ -0,0 +1,19 @@
+.transaction-breakdown-row {
+ font-size: .75rem;
+ color: $scorpion;
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid #d8d8d8;
+ }
+
+ &__title {
+ padding-right: 8px;
+ }
+
+ &__value {
+ min-width: 0;
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
new file mode 100644
index 000000000..c19399dbb
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionBreakdownRow from '../transaction-breakdown-row.component'
+import Button from '../../../button'
+
+describe('TransactionBreakdownRow Component', () => {
+ it('should render text properly', () => {
+ const wrapper = shallow(
+ <TransactionBreakdownRow
+ title="test"
+ className="test-class"
+ >
+ Test
+ </TransactionBreakdownRow>,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-breakdown-row'))
+ assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
+ assert.equal(wrapper.find('.transaction-breakdown-row__value').text(), 'Test')
+ })
+
+ it('should render components properly', () => {
+ const wrapper = shallow(
+ <TransactionBreakdownRow
+ title="test"
+ className="test-class"
+ >
+ <Button onClick={() => {}} >Button</Button>
+ </TransactionBreakdownRow>,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-breakdown-row'))
+ assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
+ assert.ok(wrapper.find('.transaction-breakdown-row__value').find(Button))
+ })
+})
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
new file mode 100644
index 000000000..c11ff8efa
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
@@ -0,0 +1,26 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class TransactionBreakdownRow extends PureComponent {
+ static propTypes = {
+ title: PropTypes.string,
+ children: PropTypes.node,
+ className: PropTypes.string,
+ }
+
+ render () {
+ const { title, children, className } = this.props
+
+ return (
+ <div className={classnames('transaction-breakdown-row', className)}>
+ <div className="transaction-breakdown-row__title">
+ { title }
+ </div>
+ <div className="transaction-breakdown-row__value">
+ { children }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
new file mode 100644
index 000000000..bb6075e9f
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
@@ -0,0 +1,82 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import TransactionBreakdownRow from './transaction-breakdown-row'
+import Card from '../card'
+import CurrencyDisplay from '../currency-display'
+import HexToDecimal from '../hex-to-decimal'
+import { ETH, GWEI } from '../../constants/common'
+import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
+import { sumHexes } from '../../helpers/transactions.util'
+
+export default class TransactionBreakdown extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ transaction: PropTypes.object,
+ className: PropTypes.string,
+ }
+
+ static defaultProps = {
+ transaction: {},
+ }
+
+ render () {
+ const { t } = this.context
+ const { transaction, className } = this.props
+ const { txParams: { gas, gasPrice, value } = {} } = transaction
+ const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice })
+ const totalInHex = sumHexes(hexGasTotal, value)
+
+ return (
+ <div className={classnames('transaction-breakdown', className)}>
+ <Card
+ title={t('transaction')}
+ className="transaction-breakdown__card"
+ >
+ <TransactionBreakdownRow title={t('amount')}>
+ <CurrencyDisplay
+ className="transaction-breakdown__value"
+ currency={ETH}
+ value={value}
+ />
+ </TransactionBreakdownRow>
+ <TransactionBreakdownRow
+ title={`${t('gasLimit')} (${t('units')})`}
+ className="transaction-breakdown__row-title"
+ >
+ <HexToDecimal
+ className="transaction-breakdown__value"
+ value={gas}
+ />
+ </TransactionBreakdownRow>
+ <TransactionBreakdownRow title={t('gasPrice')}>
+ <CurrencyDisplay
+ className="transaction-breakdown__value"
+ currency={ETH}
+ denomination={GWEI}
+ value={gasPrice}
+ hideLabel
+ />
+ </TransactionBreakdownRow>
+ <TransactionBreakdownRow title={t('total')}>
+ <div>
+ <CurrencyDisplay
+ className="transaction-breakdown__value transaction-breakdown__value--eth-total"
+ currency={ETH}
+ value={totalInHex}
+ numberOfDecimals={6}
+ />
+ <CurrencyDisplay
+ className="transaction-breakdown__value"
+ value={totalInHex}
+ />
+ </div>
+ </TransactionBreakdownRow>
+ </Card>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item-details/index.js b/ui/app/components/transaction-list-item-details/index.js
new file mode 100644
index 000000000..0e878d032
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list-item-details.component'
diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/transaction-list-item-details/index.scss
new file mode 100644
index 000000000..54cf834cc
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/index.scss
@@ -0,0 +1,49 @@
+.transaction-list-item-details {
+ &__header {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__header-buttons {
+ display: flex;
+ flex-direction: row;
+ }
+
+ &__header-button {
+ font-size: .625rem;
+
+ &:not(:last-child) {
+ margin-right: 8px;
+ }
+ }
+
+ &__sender-to-recipient-container {
+ margin-bottom: 8px;
+ }
+
+ &__cards-container {
+ display: flex;
+ flex-direction: row;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ &__transaction-breakdown {
+ flex: 1;
+ margin-right: 8px;
+ min-width: 0;
+
+ @media screen and (max-width: $break-small) {
+ margin: 0 0 8px 0;
+ }
+ }
+
+ &__transaction-activity-log {
+ flex: 2;
+ min-width: 0;
+ }
+}
diff --git a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
new file mode 100644
index 000000000..f2bbe8789
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
@@ -0,0 +1,66 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionListItemDetails from '../transaction-list-item-details.component'
+import Button from '../../button'
+import SenderToRecipient from '../../sender-to-recipient'
+import TransactionBreakdown from '../../transaction-breakdown'
+import TransactionActivityLog from '../../transaction-activity-log'
+
+describe('TransactionListItemDetails Component', () => {
+ it('should render properly', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionListItemDetails
+ transaction={transaction}
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-list-item-details'))
+ assert.equal(wrapper.find(Button).length, 1)
+ assert.equal(wrapper.find(SenderToRecipient).length, 1)
+ assert.equal(wrapper.find(TransactionBreakdown).length, 1)
+ assert.equal(wrapper.find(TransactionActivityLog).length, 1)
+ })
+
+ it('should render a retry button', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionListItemDetails
+ transaction={transaction}
+ showRetry={true}
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-list-item-details'))
+ assert.equal(wrapper.find(Button).length, 2)
+ })
+})
diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
new file mode 100644
index 000000000..13cb51349
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
@@ -0,0 +1,108 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import SenderToRecipient from '../sender-to-recipient'
+import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
+import TransactionActivityLog from '../transaction-activity-log'
+import TransactionBreakdown from '../transaction-breakdown'
+import Button from '../button'
+import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
+
+export default class TransactionListItemDetails extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onCancel: PropTypes.func,
+ onRetry: PropTypes.func,
+ showCancel: PropTypes.bool,
+ showRetry: PropTypes.bool,
+ transaction: PropTypes.object,
+ }
+
+ handleEtherscanClick = () => {
+ const { hash, metamaskNetworkId } = this.props.transaction
+
+ const prefix = prefixForNetwork(metamaskNetworkId)
+ const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
+ global.platform.openWindow({ url: etherscanUrl })
+ this.setState({ showTransactionDetails: true })
+ }
+
+ handleCancel = event => {
+ const { onCancel } = this.props
+
+ event.stopPropagation()
+ onCancel()
+ }
+
+ handleRetry = event => {
+ const { onRetry } = this.props
+
+ event.stopPropagation()
+ onRetry()
+ }
+
+ render () {
+ const { t } = this.context
+ const { transaction, showCancel, showRetry } = this.props
+ const { txParams: { to, from } = {} } = transaction
+
+ return (
+ <div className="transaction-list-item-details">
+ <div className="transaction-list-item-details__header">
+ <div>Details</div>
+ <div className="transaction-list-item-details__header-buttons">
+ {
+ showRetry && (
+ <Button
+ type="raised"
+ onClick={this.handleRetry}
+ className="transaction-list-item-details__header-button"
+ >
+ { t('speedUp') }
+ </Button>
+ )
+ }
+ {
+ showCancel && (
+ <Button
+ type="raised"
+ onClick={this.handleCancel}
+ className="transaction-list-item-details__header-button"
+ >
+ { t('cancel') }
+ </Button>
+ )
+ }
+ <Button
+ type="raised"
+ onClick={this.handleEtherscanClick}
+ className="transaction-list-item-details__header-button"
+ >
+ <img src="/images/arrow-popout.svg" />
+ </Button>
+ </div>
+ </div>
+ <div className="transaction-list-item-details__sender-to-recipient-container">
+ <SenderToRecipient
+ variant={CARDS_VARIANT}
+ addressOnly
+ recipientAddress={to}
+ senderAddress={from}
+ />
+ </div>
+ <div className="transaction-list-item-details__cards-container">
+ <TransactionBreakdown
+ transaction={transaction}
+ className="transaction-list-item-details__transaction-breakdown"
+ />
+ <TransactionActivityLog
+ transaction={transaction}
+ className="transaction-list-item-details__transaction-activity-log"
+ />
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/index.js b/ui/app/components/transaction-list-item/index.js
new file mode 100644
index 000000000..697cc55e9
--- /dev/null
+++ b/ui/app/components/transaction-list-item/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list-item.container'
diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss
new file mode 100644
index 000000000..9d694546b
--- /dev/null
+++ b/ui/app/components/transaction-list-item/index.scss
@@ -0,0 +1,131 @@
+.transaction-list-item {
+ box-sizing: border-box;
+ min-height: 74px;
+ border-bottom: 1px solid $geyser;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ background: $white;
+
+ &__grid {
+ cursor: pointer;
+ width: 100%;
+ padding: 16px 20px;
+ display: grid;
+ grid-template-columns: 45px 1fr 1fr 1fr;
+ grid-template-areas:
+ "identicon action status primary-amount"
+ "identicon nonce status secondary-amount";
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 20px 12px;
+ grid-template-columns: 45px 5fr 3fr;
+ grid-template-areas:
+ "nonce nonce nonce"
+ "identicon action primary-amount"
+ "identicon status secondary-amount";
+ }
+
+ &:hover {
+ background: rgba($alto, .2);
+ }
+ }
+
+ &__identicon {
+ grid-area: identicon;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 2 / span 2;
+ }
+ }
+
+ &__action {
+ text-transform: capitalize;
+ padding: 0 8px 2px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: action;
+ align-self: end;
+ }
+
+ &__status {
+ grid-area: status;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 3;
+ }
+ }
+
+ &__nonce {
+ font-size: .75rem;
+ color: #5e6064;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: nonce;
+ align-self: start;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 4px;
+ }
+ }
+
+ &__amount {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &--primary {
+ text-align: end;
+ grid-area: primary-amount;
+ align-self: end;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 2px;
+ }
+ }
+
+ &--secondary {
+ text-align: end;
+ font-size: .75rem;
+ color: #5e6064;
+ grid-area: secondary-amount;
+ align-self: start;
+ }
+ }
+
+ &__retry {
+ background: #d1edff;
+ border-radius: 12px;
+ font-size: .75rem;
+ padding: 4px 12px;
+ cursor: pointer;
+ margin-top: 8px;
+
+ @media screen and (max-width: $break-small) {
+ font-size: .5rem;
+ }
+ }
+
+ &__details-container {
+ padding: 8px 16px 16px;
+ background: #f3f4f7;
+ width: 100%;
+ }
+
+ &__expander {
+ max-height: 0px;
+ width: 100%;
+
+ &--show {
+ max-height: 1000px;
+ transition: max-height 700ms ease-out;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js
new file mode 100644
index 000000000..c1c69f59b
--- /dev/null
+++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js
@@ -0,0 +1,200 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../identicon'
+import TransactionStatus from '../transaction-status'
+import TransactionAction from '../transaction-action'
+import CurrencyDisplay from '../currency-display'
+import TokenCurrencyDisplay from '../token-currency-display'
+import TransactionListItemDetails from '../transaction-list-item-details'
+import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
+import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
+import { ETH } from '../../constants/common'
+import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums'
+
+export default class TransactionListItem extends PureComponent {
+ static propTypes = {
+ assetImages: PropTypes.object,
+ history: PropTypes.object,
+ methodData: PropTypes.object,
+ nonceAndDate: PropTypes.string,
+ retryTransaction: PropTypes.func,
+ setSelectedToken: PropTypes.func,
+ showCancelModal: PropTypes.func,
+ showCancel: PropTypes.bool,
+ showRetry: PropTypes.bool,
+ showTransactionDetailsModal: PropTypes.func,
+ token: PropTypes.object,
+ tokenData: PropTypes.object,
+ transaction: PropTypes.object,
+ value: PropTypes.string,
+ }
+
+ state = {
+ showTransactionDetails: false,
+ }
+
+ handleClick = () => {
+ const {
+ transaction,
+ history,
+ showTransactionDetailsModal,
+ methodData,
+ showCancel,
+ showRetry,
+ } = this.props
+ const { id, status } = transaction
+ const { showTransactionDetails } = this.state
+ const windowType = window.METAMASK_UI_TYPE
+
+ if (status === UNAPPROVED_STATUS) {
+ history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
+ return
+ }
+
+ if (windowType === ENVIRONMENT_TYPE_FULLSCREEN) {
+ this.setState({ showTransactionDetails: !showTransactionDetails })
+ } else {
+ showTransactionDetailsModal({
+ transaction,
+ onRetry: this.handleRetry,
+ showRetry: showRetry && methodData.done,
+ onCancel: this.handleCancel,
+ showCancel,
+ })
+ }
+ }
+
+ handleCancel = () => {
+ const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props
+ showCancelModal(id, gasPrice)
+ }
+
+ handleRetry = () => {
+ const {
+ transaction: { txParams: { to } = {} },
+ methodData: { name } = {},
+ setSelectedToken,
+ } = this.props
+
+ if (name === TOKEN_METHOD_TRANSFER) {
+ setSelectedToken(to)
+ }
+
+ return this.resubmit()
+ }
+
+ resubmit () {
+ const { transaction: { id }, retryTransaction, history } = this.props
+ return retryTransaction(id)
+ .then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
+ }
+
+ renderPrimaryCurrency () {
+ const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
+
+ return token
+ ? (
+ <TokenCurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ token={token}
+ transactionData={data}
+ prefix="-"
+ />
+ ) : (
+ <CurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ value={value}
+ prefix="-"
+ numberOfDecimals={2}
+ currency={ETH}
+ />
+ )
+ }
+
+ renderSecondaryCurrency () {
+ const { token, value } = this.props
+
+ return token
+ ? null
+ : (
+ <CurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--secondary"
+ prefix="-"
+ value={value}
+ />
+ )
+ }
+
+ render () {
+ const {
+ assetImages,
+ methodData,
+ nonceAndDate,
+ showCancel,
+ showRetry,
+ tokenData,
+ transaction,
+ } = this.props
+ const { txParams = {} } = transaction
+ const { showTransactionDetails } = this.state
+ const toAddress = tokenData
+ ? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to
+ : txParams.to
+
+ return (
+ <div className="transaction-list-item">
+ <div
+ className="transaction-list-item__grid"
+ onClick={this.handleClick}
+ >
+ <Identicon
+ className="transaction-list-item__identicon"
+ address={toAddress}
+ diameter={34}
+ image={assetImages[toAddress]}
+ />
+ <TransactionAction
+ transaction={transaction}
+ methodData={methodData}
+ className="transaction-list-item__action"
+ />
+ <div
+ className="transaction-list-item__nonce"
+ title={nonceAndDate}
+ >
+ { nonceAndDate }
+ </div>
+ <TransactionStatus
+ className="transaction-list-item__status"
+ statusKey={transaction.status}
+ title={(
+ (transaction.err && transaction.err.rpc)
+ ? transaction.err.rpc.message
+ : transaction.err && transaction.err.message
+ )}
+ />
+ { this.renderPrimaryCurrency() }
+ { this.renderSecondaryCurrency() }
+ </div>
+ <div className={classnames('transaction-list-item__expander', {
+ 'transaction-list-item__expander--show': showTransactionDetails,
+ })}>
+ {
+ showTransactionDetails && (
+ <div className="transaction-list-item__details-container">
+ <TransactionListItemDetails
+ transaction={transaction}
+ onRetry={this.handleRetry}
+ showRetry={showRetry && methodData.done}
+ onCancel={this.handleCancel}
+ showCancel={showCancel}
+ />
+ </div>
+ )
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js
new file mode 100644
index 000000000..72f5f5d61
--- /dev/null
+++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js
@@ -0,0 +1,48 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import withMethodData from '../../higher-order-components/with-method-data'
+import TransactionListItem from './transaction-list-item.component'
+import { setSelectedToken, retryTransaction, showModal } from '../../actions'
+import { hexToDecimal } from '../../helpers/conversions.util'
+import { getTokenData } from '../../helpers/transactions.util'
+import { formatDate } from '../../util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
+
+ const tokenData = data && getTokenData(data)
+ const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
+
+ return {
+ value,
+ nonceAndDate,
+ tokenData,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
+ retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
+ showCancelModal: (transactionId, originalGasPrice) => {
+ return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
+ },
+ showTransactionDetailsModal: ({ transaction, onRetry, showRetry, onCancel, showCancel }) => {
+ return dispatch(showModal({
+ name: 'TRANSACTION_DETAILS',
+ transaction,
+ onRetry,
+ showRetry,
+ onCancel,
+ showCancel,
+ }))
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+ withMethodData,
+)(TransactionListItem)
diff --git a/ui/app/components/transaction-list/index.js b/ui/app/components/transaction-list/index.js
new file mode 100644
index 000000000..688994367
--- /dev/null
+++ b/ui/app/components/transaction-list/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list.container'
diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss
new file mode 100644
index 000000000..777f701f9
--- /dev/null
+++ b/ui/app/components/transaction-list/index.scss
@@ -0,0 +1,49 @@
+.transaction-list {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow-y: hidden;
+ margin-top: 8px;
+ border-top: 1px solid $geyser;
+
+ &__completed-transactions {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ }
+
+ &__header {
+ flex: 0 0 auto;
+ font-size: .875rem;
+ color: $dusty-gray;
+ border-bottom: 1px solid $geyser;
+ padding: 8px 0 8px 20px;
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 0 8px 16px;
+ }
+ }
+
+ &__transactions {
+ flex: 1;
+ overflow-y: auto;
+ }
+
+ &__pending-transactions {
+ margin-bottom: 16px;
+ }
+
+ &__empty {
+ flex: 1;
+ display: grid;
+ grid-template-rows: 35% 1fr;
+ padding-top: 8px;
+ }
+
+ &__empty-text {
+ grid-row-start: 2;
+ display: flex;
+ justify-content: center;
+ color: $silver;
+ }
+}
diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js
new file mode 100644
index 000000000..eef60186d
--- /dev/null
+++ b/ui/app/components/transaction-list/transaction-list.component.js
@@ -0,0 +1,119 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TransactionListItem from '../transaction-list-item'
+import ShapeShiftTransactionListItem from '../shift-list-item'
+import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions'
+
+export default class TransactionList extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ pendingTransactions: [],
+ completedTransactions: [],
+ transactionToRetry: {},
+ }
+
+ static propTypes = {
+ pendingTransactions: PropTypes.array,
+ completedTransactions: PropTypes.array,
+ transactionToRetry: PropTypes.object,
+ selectedToken: PropTypes.object,
+ updateNetworkNonce: PropTypes.func,
+ assetImages: PropTypes.object,
+ }
+
+ componentDidMount () {
+ this.props.updateNetworkNonce()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { pendingTransactions: prevPendingTransactions = [] } = prevProps
+ const { pendingTransactions = [], updateNetworkNonce } = this.props
+
+ if (pendingTransactions.length > prevPendingTransactions.length) {
+ updateNetworkNonce()
+ }
+ }
+
+ shouldShowRetry = transaction => {
+ const { transactionToRetry } = this.props
+ const { id, submittedTime } = transaction
+ return id === transactionToRetry.id && Date.now() - submittedTime > 30000
+ }
+
+ renderTransactions () {
+ const { t } = this.context
+ const { pendingTransactions = [], completedTransactions = [] } = this.props
+ return (
+ <div className="transaction-list__transactions">
+ {
+ pendingTransactions.length > 0 && (
+ <div className="transaction-list__pending-transactions">
+ <div className="transaction-list__header">
+ { `${t('queue')} (${pendingTransactions.length})` }
+ </div>
+ {
+ pendingTransactions.map((transaction, index) => (
+ this.renderTransaction(transaction, index, true)
+ ))
+ }
+ </div>
+ )
+ }
+ <div className="transaction-list__completed-transactions">
+ <div className="transaction-list__header">
+ { t('history') }
+ </div>
+ {
+ completedTransactions.length > 0
+ ? completedTransactions.map((transaction, index) => (
+ this.renderTransaction(transaction, index)
+ ))
+ : this.renderEmpty()
+ }
+ </div>
+ </div>
+ )
+ }
+
+ renderTransaction (transaction, index, showCancel) {
+ const { selectedToken, assetImages } = this.props
+
+ return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
+ ? (
+ <ShapeShiftTransactionListItem
+ { ...transaction }
+ key={`shapeshift${index}`}
+ />
+ ) : (
+ <TransactionListItem
+ transaction={transaction}
+ key={transaction.id}
+ showRetry={this.shouldShowRetry(transaction)}
+ showCancel={showCancel}
+ token={selectedToken}
+ assetImages={assetImages}
+ />
+ )
+ }
+
+ renderEmpty () {
+ return (
+ <div className="transaction-list__empty">
+ <div className="transaction-list__empty-text">
+ { this.context.t('noTransactions') }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ return (
+ <div className="transaction-list">
+ { this.renderTransactions() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/transaction-list/transaction-list.container.js
new file mode 100644
index 000000000..2e946c67d
--- /dev/null
+++ b/ui/app/components/transaction-list/transaction-list.container.js
@@ -0,0 +1,51 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import TransactionList from './transaction-list.component'
+import {
+ pendingTransactionsSelector,
+ submittedPendingTransactionsSelector,
+ completedTransactionsSelector,
+} from '../../selectors/transactions'
+import { getSelectedAddress, getAssetImages } from '../../selectors'
+import { selectedTokenSelector } from '../../selectors/tokens'
+import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
+import { updateNetworkNonce } from '../../actions'
+
+const mapStateToProps = state => {
+ const pendingTransactions = pendingTransactionsSelector(state)
+ const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
+ const networkNonce = state.appState.networkNonce
+
+ return {
+ completedTransactions: completedTransactionsSelector(state),
+ pendingTransactions,
+ transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
+ selectedToken: selectedTokenSelector(state),
+ selectedAddress: getSelectedAddress(state),
+ assetImages: getAssetImages(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { selectedAddress, ...restStateProps } = stateProps
+ const { updateNetworkNonce, ...restDispatchProps } = dispatchProps
+
+ return {
+ ...restStateProps,
+ ...restDispatchProps,
+ ...ownProps,
+ updateNetworkNonce: () => updateNetworkNonce(selectedAddress),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(TransactionList)
diff --git a/ui/app/components/transaction-status/index.js b/ui/app/components/transaction-status/index.js
new file mode 100644
index 000000000..dece41e9c
--- /dev/null
+++ b/ui/app/components/transaction-status/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-status.component'
diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss
new file mode 100644
index 000000000..35be550f7
--- /dev/null
+++ b/ui/app/components/transaction-status/index.scss
@@ -0,0 +1,28 @@
+.transaction-status {
+ height: 26px;
+ width: 81px;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ color: #5e6064;
+ font-size: .625rem;
+ text-transform: uppercase;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ height: 16px;
+ width: 70px;
+ font-size: .5rem;
+ }
+
+ &--confirmed {
+ background-color: #eafad7;
+ color: #609a1c;
+ }
+
+ &--approved, &--submitted {
+ background-color: #FFF2DB;
+ color: #CA810A;
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/transaction-status/transaction-status.component.js
new file mode 100644
index 000000000..c22baf18a
--- /dev/null
+++ b/ui/app/components/transaction-status/transaction-status.component.js
@@ -0,0 +1,59 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Tooltip from '../tooltip-v2'
+import {
+ UNAPPROVED_STATUS,
+ REJECTED_STATUS,
+ APPROVED_STATUS,
+ SIGNED_STATUS,
+ SUBMITTED_STATUS,
+ CONFIRMED_STATUS,
+ FAILED_STATUS,
+ DROPPED_STATUS,
+} from '../../constants/transactions'
+
+const statusToClassNameHash = {
+ [UNAPPROVED_STATUS]: 'transaction-status--unapproved',
+ [REJECTED_STATUS]: 'transaction-status--rejected',
+ [APPROVED_STATUS]: 'transaction-status--approved',
+ [SIGNED_STATUS]: 'transaction-status--signed',
+ [SUBMITTED_STATUS]: 'transaction-status--submitted',
+ [CONFIRMED_STATUS]: 'transaction-status--confirmed',
+ [FAILED_STATUS]: 'transaction-status--failed',
+ [DROPPED_STATUS]: 'transaction-status--dropped',
+}
+
+const statusToTextHash = {
+ [APPROVED_STATUS]: 'pending',
+ [SUBMITTED_STATUS]: 'pending',
+}
+
+export default class TransactionStatus extends PureComponent {
+ static defaultProps = {
+ title: null,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ statusKey: PropTypes.string,
+ className: PropTypes.string,
+ title: PropTypes.string,
+ }
+
+ render () {
+ const { className, statusKey, title } = this.props
+ const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
+
+ return (
+ <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
+ <Tooltip position="top" title={title}>
+ { statusText }
+ </Tooltip>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/index.js b/ui/app/components/transaction-view-balance/index.js
new file mode 100644
index 000000000..8824737f7
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-view-balance.container'
diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss
new file mode 100644
index 000000000..12045ab6d
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/index.scss
@@ -0,0 +1,76 @@
+.transaction-view-balance {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex: 1;
+ height: 54px;
+
+ &__balance {
+ margin-left: 12px;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: $break-small) {
+ align-items: center;
+ margin: 16px 0;
+ }
+ }
+
+ &__token-balance {
+ margin-left: 12px;
+ font-size: 1.5rem;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 12px;
+ font-size: 1.75rem;
+ }
+ }
+
+ &__primary-balance {
+ font-size: 1.5rem;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 12px;
+ font-size: 1.75rem;
+ }
+ }
+
+ &__secondary-balance {
+ font-size: 1.15rem;
+ color: #a0a0a0;
+ }
+
+ &__balance-container {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__button {
+ min-width: initial;
+ width: 100px;
+
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ height: initial
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
new file mode 100644
index 000000000..bb95cb27e
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TokenBalance from '../../token-balance'
+import CurrencyDisplay from '../../currency-display'
+import { SEND_ROUTE } from '../../../routes'
+import TransactionViewBalance from '../transaction-view-balance.component'
+
+const propsMethodSpies = {
+ showDepositModal: sinon.spy(),
+}
+
+const historySpies = {
+ push: sinon.spy(),
+}
+
+const t = (str1, str2) => str2 ? str1 + str2 : str1
+
+describe('TransactionViewBalance Component', () => {
+ afterEach(() => {
+ propsMethodSpies.showDepositModal.resetHistory()
+ historySpies.push.resetHistory()
+ })
+
+ it('should render ETH balance properly', () => {
+ const wrapper = shallow(<TransactionViewBalance
+ showDepositModal={propsMethodSpies.showDepositModal}
+ history={historySpies}
+ network="3"
+ ethBalance={123}
+ fiatBalance={456}
+ currentCurrency="usd"
+ />, { context: { t } })
+
+ assert.equal(wrapper.find('.transaction-view-balance').length, 1)
+ assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
+ assert.equal(wrapper.find(CurrencyDisplay).length, 2)
+
+ const buttons = wrapper.find('.transaction-view-balance__buttons')
+ assert.equal(propsMethodSpies.showDepositModal.callCount, 0)
+ buttons.childAt(0).simulate('click')
+ assert.equal(propsMethodSpies.showDepositModal.callCount, 1)
+ assert.equal(historySpies.push.callCount, 0)
+ buttons.childAt(1).simulate('click')
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE)
+ })
+
+ it('should render token balance properly', () => {
+ const token = {
+ address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5',
+ decimals: '2',
+ symbol: 'ABC',
+ }
+
+ const wrapper = shallow(<TransactionViewBalance
+ showDepositModal={propsMethodSpies.showDepositModal}
+ history={historySpies}
+ network="3"
+ ethBalance={123}
+ fiatBalance={456}
+ currentCurrency="usd"
+ selectedToken={token}
+ />, { context: { t } })
+
+ assert.equal(wrapper.find('.transaction-view-balance').length, 1)
+ assert.equal(wrapper.find('.transaction-view-balance__button').length, 1)
+ assert.equal(wrapper.find(TokenBalance).length, 1)
+ })
+})
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
new file mode 100644
index 000000000..1b7a29c87
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
@@ -0,0 +1,96 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../button'
+import Identicon from '../identicon'
+import TokenBalance from '../token-balance'
+import CurrencyDisplay from '../currency-display'
+import { SEND_ROUTE } from '../../routes'
+import { ETH } from '../../constants/common'
+
+export default class TransactionViewBalance extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ showDepositModal: PropTypes.func,
+ selectedToken: PropTypes.object,
+ history: PropTypes.object,
+ network: PropTypes.string,
+ balance: PropTypes.string,
+ assetImage: PropTypes.string,
+ }
+
+ renderBalance () {
+ const { selectedToken, balance } = this.props
+
+ return selectedToken
+ ? (
+ <TokenBalance
+ token={selectedToken}
+ withSymbol
+ className="transaction-view-balance__token-balance"
+ />
+ ) : (
+ <div className="transaction-view-balance__balance">
+ <CurrencyDisplay
+ className="transaction-view-balance__primary-balance"
+ value={balance}
+ currency={ETH}
+ numberOfDecimals={3}
+ />
+ <CurrencyDisplay
+ className="transaction-view-balance__secondary-balance"
+ value={balance}
+ />
+ </div>
+ )
+ }
+
+ renderButtons () {
+ const { t } = this.context
+ const { selectedToken, showDepositModal, history } = this.props
+
+ return (
+ <div className="transaction-view-balance__buttons">
+ {
+ !selectedToken && (
+ <Button
+ type="primary"
+ className="transaction-view-balance__button"
+ onClick={() => showDepositModal()}
+ >
+ { t('deposit') }
+ </Button>
+ )
+ }
+ <Button
+ type="primary"
+ className="transaction-view-balance__button"
+ onClick={() => history.push(SEND_ROUTE)}
+ >
+ { t('send') }
+ </Button>
+ </div>
+ )
+ }
+
+ render () {
+ const { network, selectedToken, assetImage } = this.props
+
+ return (
+ <div className="transaction-view-balance">
+ <div className="transaction-view-balance__balance-container">
+ <Identicon
+ diameter={50}
+ address={selectedToken && selectedToken.address}
+ network={network}
+ image={assetImage}
+ />
+ { this.renderBalance() }
+ </div>
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
new file mode 100644
index 000000000..30c5cab16
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import TransactionViewBalance from './transaction-view-balance.component'
+import { getSelectedToken, getSelectedAddress, getSelectedTokenAssetImage } from '../../selectors'
+import { showModal } from '../../actions'
+
+const mapStateToProps = state => {
+ const selectedAddress = getSelectedAddress(state)
+ const { metamask: { network, accounts } } = state
+ const account = accounts[selectedAddress]
+ const { balance } = account
+
+ return {
+ selectedToken: getSelectedToken(state),
+ network,
+ balance,
+ assetImage: getSelectedTokenAssetImage(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(TransactionViewBalance)
diff --git a/ui/app/components/transaction-view/index.js b/ui/app/components/transaction-view/index.js
new file mode 100644
index 000000000..9eb0c3c83
--- /dev/null
+++ b/ui/app/components/transaction-view/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-view.component'
diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/transaction-view/index.scss
new file mode 100644
index 000000000..af9771ce0
--- /dev/null
+++ b/ui/app/components/transaction-view/index.scss
@@ -0,0 +1,27 @@
+.transaction-view {
+ flex: 1 1 66.5%;
+ background: $white;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+
+ &__balance-wrapper {
+ @media screen and (max-width: $break-small) {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ flex: 0 0 auto;
+ padding-top: 16px;
+ }
+
+ @media screen and (min-width: $break-large) {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ margin: 2.3em 2.37em .8em;
+ flex: 0 0 auto;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-view/transaction-view.component.js b/ui/app/components/transaction-view/transaction-view.component.js
new file mode 100644
index 000000000..7014ca173
--- /dev/null
+++ b/ui/app/components/transaction-view/transaction-view.component.js
@@ -0,0 +1,27 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Media from 'react-media'
+import MenuBar from '../menu-bar'
+import TransactionViewBalance from '../transaction-view-balance'
+import TransactionList from '../transaction-list'
+
+export default class TransactionView extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ return (
+ <div className="transaction-view">
+ <Media
+ query="(max-width: 575px)"
+ render={() => <MenuBar />}
+ />
+ <div className="transaction-view__balance-wrapper">
+ <TransactionViewBalance />
+ </div>
+ <TransactionList />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
deleted file mode 100644
index 474d62638..000000000
--- a/ui/app/components/tx-list-item.js
+++ /dev/null
@@ -1,356 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const { compose } = require('recompose')
-const { withRouter } = require('react-router-dom')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const inherits = require('util').inherits
-const classnames = require('classnames')
-const abi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(abi)
-const Identicon = require('./identicon')
-const contractMap = require('eth-contract-metadata')
-const { checksumAddress } = require('../util')
-
-const actions = require('../actions')
-const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
-const { calcTokenAmount } = require('../token-util')
-
-const { getCurrentCurrency } = require('../selectors')
-const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
-
-TxListItem.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxListItem)
-
-function mapStateToProps (state) {
- return {
- tokens: state.metamask.tokens,
- currentCurrency: getCurrentCurrency(state),
- contractExchangeRates: state.metamask.contractExchangeRates,
- selectedAddressTxList: state.metamask.selectedAddressTxList,
- networkNonce: state.appState.networkNonce,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)),
- retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
- }
-}
-
-inherits(TxListItem, Component)
-function TxListItem () {
- Component.call(this)
-
- this.state = {
- total: null,
- fiatTotal: null,
- isTokenTx: null,
- }
-
- this.unmounted = false
-}
-
-TxListItem.prototype.componentDidMount = async function () {
- const { txParams = {} } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { name: txDataName } = decodedData || {}
- const isTokenTx = txDataName === 'transfer'
-
- const { total, fiatTotal } = isTokenTx
- ? await this.getSendTokenTotal()
- : this.getSendEtherTotal()
-
- if (this.unmounted) {
- return
- }
- this.setState({ total, fiatTotal, isTokenTx })
-}
-
-TxListItem.prototype.componentWillUnmount = function () {
- this.unmounted = true
-}
-
-TxListItem.prototype.getAddressText = function () {
- const {
- address,
- txParams = {},
- isMsg,
- } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { name: txDataName, params = [] } = decodedData || {}
- const { value } = params[0] || {}
- const checksummedAddress = checksumAddress(address)
- const checksummedValue = checksumAddress(value)
-
- let addressText
- if (txDataName === 'transfer' || address) {
- const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress
- addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}`
- } else if (isMsg) {
- addressText = this.context.t('sigRequest')
- } else {
- addressText = this.context.t('contractDeployment')
- }
-
- return addressText
-}
-
-TxListItem.prototype.getSendEtherTotal = function () {
- const {
- transactionAmount,
- conversionRate,
- address,
- currentCurrency,
- } = this.props
-
- if (!address) {
- return {}
- }
-
- const totalInFiat = conversionUtil(transactionAmount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- fromDenomination: 'WEI',
- numberOfDecimals: 2,
- conversionRate,
- })
- const totalInETH = conversionUtil(transactionAmount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- total: `${totalInETH} ETH`,
- fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`,
- }
-}
-
-TxListItem.prototype.getTokenInfo = async function () {
- const { txParams = {}, tokenInfoGetter, tokens } = this.props
- const toAddress = txParams.to
-
- let decimals
- let symbol
-
- ({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {})
-
- if (!decimals && !symbol) {
- ({ decimals, symbol } = contractMap[toAddress] || {})
- }
-
- if (!decimals && !symbol) {
- ({ decimals, symbol } = await tokenInfoGetter(toAddress))
- }
-
- return { decimals, symbol, address: toAddress }
-}
-
-TxListItem.prototype.getSendTokenTotal = async function () {
- const {
- txParams = {},
- conversionRate,
- contractExchangeRates,
- currentCurrency,
- } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { params = [] } = decodedData || {}
- const { value } = params[1] || {}
- const { decimals, symbol, address } = await this.getTokenInfo()
- const total = calcTokenAmount(value, decimals)
-
- let tokenToFiatRate
- let totalInFiat
-
- if (contractExchangeRates[address]) {
- tokenToFiatRate = multiplyCurrencies(
- contractExchangeRates[address],
- conversionRate
- )
-
- totalInFiat = conversionUtil(total, {
- fromNumericBase: 'dec',
- toNumericBase: 'dec',
- fromCurrency: symbol,
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate: tokenToFiatRate,
- })
- }
-
- const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol
-
- return {
- total: `${total} ${symbol}`,
- fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`,
- }
-}
-
-TxListItem.prototype.showRetryButton = function () {
- const {
- transactionSubmittedTime,
- selectedAddressTxList,
- transactionId,
- txParams,
- networkNonce,
- } = this.props
- if (!txParams) {
- return false
- }
- let currentTxSharesEarliestNonce = false
- const currentNonce = txParams.nonce
- const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
- const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
- const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
- const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
- const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
- lastSubmittedTxWithCurrentNonce.id === transactionId
- if (currentSubmittedTxs.length > 0) {
- currentTxSharesEarliestNonce = currentNonce === networkNonce
- }
-
- return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
-}
-
-TxListItem.prototype.setSelectedToken = function (tokenAddress) {
- this.props.setSelectedToken(tokenAddress)
-}
-
-TxListItem.prototype.resubmit = function () {
- const { transactionId } = this.props
- this.props.retryTransaction(transactionId)
- .then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
-}
-
-TxListItem.prototype.render = function () {
- const {
- transactionStatus,
- onClick,
- transactionId,
- dateString,
- address,
- className,
- txParams,
- } = this.props
- const { total, fiatTotal, isTokenTx } = this.state
-
- return h(`div${className || ''}`, {
- key: transactionId,
- onClick: () => onClick && onClick(transactionId),
- }, [
- h(`div.flex-column.tx-list-item-wrapper`, {}, [
-
- h('div.tx-list-date-wrapper', {
- style: {},
- }, [
- h('span.tx-list-date', {}, [
- dateString,
- ]),
- ]),
-
- h('div.flex-row.tx-list-content-wrapper', {
- style: {},
- }, [
-
- h('div.tx-list-identicon-wrapper', {
- style: {},
- }, [
- h(Identicon, {
- address,
- diameter: 28,
- }),
- ]),
-
- h('div.tx-list-account-and-status-wrapper', {}, [
- h('div.tx-list-account-wrapper', {
- style: {},
- }, [
- h('span.tx-list-account', {}, [
- this.getAddressText(address),
- ]),
- ]),
-
- h('div.tx-list-status-wrapper', {
- style: {},
- }, [
- h('span', {
- className: classnames('tx-list-status', {
- 'tx-list-status--rejected': transactionStatus === 'rejected',
- 'tx-list-status--failed': transactionStatus === 'failed',
- 'tx-list-status--dropped': transactionStatus === 'dropped',
- }),
- },
- this.txStatusIndicator(),
- ),
- ]),
- ]),
-
- h('div.flex-column.tx-list-details-wrapper', {
- style: {},
- }, [
-
- h('span.tx-list-value', total),
-
- fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
-
- ]),
- ]),
-
- this.showRetryButton() && h('.tx-list-item-retry-container', {
- onClick: (event) => {
- event.stopPropagation()
- if (isTokenTx) {
- this.setSelectedToken(txParams.to)
- }
- this.resubmit()
- },
- }, [
- h('span', 'Taking too long? Increase the gas price on your transaction'),
- ]),
-
- ]), // holding on icon from design
- ])
-}
-
-TxListItem.prototype.txStatusIndicator = function () {
- const { transactionStatus } = this.props
-
- let name
-
- if (transactionStatus === 'unapproved') {
- name = this.context.t('unapproved')
- } else if (transactionStatus === 'rejected') {
- name = this.context.t('rejected')
- } else if (transactionStatus === 'approved') {
- name = this.context.t('approved')
- } else if (transactionStatus === 'signed') {
- name = this.context.t('signed')
- } else if (transactionStatus === 'submitted') {
- name = this.context.t('submitted')
- } else if (transactionStatus === 'confirmed') {
- name = this.context.t('confirmed')
- } else if (transactionStatus === 'failed') {
- name = this.context.t('failed')
- } else if (transactionStatus === 'dropped') {
- name = this.context.t('dropped')
- }
- return name
-}
diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js
deleted file mode 100644
index d8c4a9d19..000000000
--- a/ui/app/components/tx-list.js
+++ /dev/null
@@ -1,171 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
-const selectors = require('../selectors')
-const TxListItem = require('./tx-list-item')
-const ShiftListItem = require('./shift-list-item')
-const { formatDate } = require('../util')
-const { showConfTxPage, updateNetworkNonce } = require('../actions')
-const classnames = require('classnames')
-const { tokenInfoGetter } = require('../token-util')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxList)
-
-TxList.contextTypes = {
- t: PropTypes.func,
-}
-
-function mapStateToProps (state) {
- return {
- txsToRender: selectors.transactionsSelector(state),
- conversionRate: selectors.conversionRateSelector(state),
- selectedAddress: selectors.getSelectedAddress(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
- updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
- }
-}
-
-inherits(TxList, Component)
-function TxList () {
- Component.call(this)
-}
-
-TxList.prototype.componentWillMount = function () {
- this.tokenInfoGetter = tokenInfoGetter()
- this.props.updateNetworkNonce(this.props.selectedAddress)
-}
-
-TxList.prototype.componentDidUpdate = function (prevProps) {
- const oldTxsToRender = prevProps.txsToRender
- const {
- txsToRender: newTxsToRender,
- selectedAddress,
- updateNetworkNonce,
- } = this.props
-
- if (newTxsToRender.length > oldTxsToRender.length) {
- updateNetworkNonce(selectedAddress)
- }
-}
-
-TxList.prototype.render = function () {
- return h('div.flex-column', [
- h('div.flex-row.tx-list-header-wrapper', [
- h('div.flex-row.tx-list-header', [
- h('div', this.context.t('transactions')),
- ]),
- ]),
- h('div.flex-column.tx-list-container', {}, [
- this.renderTransaction(),
- ]),
- ])
-}
-
-TxList.prototype.renderTransaction = function () {
- const { txsToRender, conversionRate } = this.props
-
- return txsToRender.length
- ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate, i))
- : [h(
- 'div.tx-list-item.tx-list-item--empty',
- { key: 'tx-list-none' },
- [ this.context.t('noTransactions') ],
- )]
-}
-
-// TODO: Consider moving TxListItem into a separate component
-TxList.prototype.renderTransactionListItem = function (transaction, conversionRate, index) {
- // console.log({transaction})
- // refer to transaction-list.js:line 58
-
- if (transaction.key === 'shapeshift') {
- return h(ShiftListItem, { ...transaction, key: `shapeshift${index}` })
- }
-
- const props = {
- dateString: formatDate(transaction.time),
- address: transaction.txParams && transaction.txParams.to,
- transactionStatus: transaction.status,
- transactionAmount: transaction.txParams && transaction.txParams.value,
- transactionId: transaction.id,
- transactionHash: transaction.hash,
- transactionNetworkId: transaction.metamaskNetworkId,
- transactionSubmittedTime: transaction.submittedTime,
- }
-
- const {
- address,
- transactionStatus,
- transactionAmount,
- dateString,
- transactionId,
- transactionHash,
- transactionNetworkId,
- transactionSubmittedTime,
- } = props
- const { history } = this.props
-
- const opts = {
- key: transactionId || transactionHash,
- txParams: transaction.txParams,
- isMsg: Boolean(transaction.msgParams),
- transactionStatus,
- transactionId,
- dateString,
- address,
- transactionAmount,
- transactionHash,
- conversionRate,
- tokenInfoGetter: this.tokenInfoGetter,
- transactionSubmittedTime,
- }
-
- const isUnapproved = transactionStatus === 'unapproved'
-
- if (isUnapproved) {
- opts.onClick = () => {
- this.props.showConfTxPage({ id: transactionId })
- history.push(CONFIRM_TRANSACTION_ROUTE)
- }
- opts.transactionStatus = this.context.t('notStarted')
- } else if (transactionHash) {
- opts.onClick = () => this.view(transactionHash, transactionNetworkId)
- }
-
- opts.className = classnames('.tx-list-item', {
- '.tx-list-pending-item-container': isUnapproved,
- '.tx-list-clickable': Boolean(transactionHash) || isUnapproved,
- })
-
- return h(TxListItem, opts)
-}
-
-TxList.prototype.view = function (txHash, network) {
- const url = etherscanLinkFor(txHash, network)
- if (url) {
- navigateTo(url)
- }
-}
-
-function navigateTo (url) {
- global.platform.openWindow({ url })
-}
-
-function etherscanLinkFor (txHash, network) {
- const prefix = prefixForNetwork(network)
- return `https://${prefix}etherscan.io/tx/${txHash}`
-}
diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js
deleted file mode 100644
index 654090da6..000000000
--- a/ui/app/components/tx-view.js
+++ /dev/null
@@ -1,156 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const actions = require('../actions')
-const selectors = require('../selectors')
-const { SEND_ROUTE } = require('../routes')
-const { checksumAddress: toChecksumAddress } = require('../util')
-
-const BalanceComponent = require('./balance-component')
-const Tooltip = require('./tooltip')
-const TxList = require('./tx-list')
-const SelectedAccount = require('./selected-account')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxView)
-
-TxView.contextTypes = {
- t: PropTypes.func,
-}
-
-function mapStateToProps (state) {
- const sidebarOpen = state.appState.sidebarOpen
- const isMascara = state.appState.isMascara
-
- const identities = state.metamask.identities
- const accounts = state.metamask.accounts
- const network = state.metamask.network
- const selectedTokenAddress = state.metamask.selectedTokenAddress
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- const checksumAddress = toChecksumAddress(selectedAddress)
- const identity = identities[selectedAddress]
-
- return {
- sidebarOpen,
- selectedAddress,
- checksumAddress,
- selectedTokenAddress,
- selectedToken: selectors.getSelectedToken(state),
- identity,
- network,
- isMascara,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showSidebar: () => { dispatch(actions.showSidebar()) },
- hideSidebar: () => { dispatch(actions.hideSidebar()) },
- showModal: (payload) => { dispatch(actions.showModal(payload)) },
- showSendPage: () => { dispatch(actions.showSendPage()) },
- showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
- }
-}
-
-inherits(TxView, Component)
-function TxView () {
- Component.call(this)
-}
-
-TxView.prototype.renderHeroBalance = function () {
- const { selectedToken } = this.props
-
- return h('div.hero-balance', {}, [
-
- h(BalanceComponent, { token: selectedToken }),
-
- this.renderButtons(),
- ])
-}
-
-TxView.prototype.renderButtons = function () {
- const {selectedToken, showModal, history } = this.props
-
- return !selectedToken
- ? (
- h('div.flex-row.flex-center.hero-balance-buttons', [
- h('button.btn-primary.hero-balance-button', {
- onClick: () => showModal({
- name: 'DEPOSIT_ETHER',
- }),
- }, this.context.t('deposit')),
-
- h('button.btn-primary.hero-balance-button', {
- style: {
- marginLeft: '0.8em',
- },
- onClick: () => history.push(SEND_ROUTE),
- }, this.context.t('send')),
- ])
- )
- : (
- h('div.flex-row.flex-center.hero-balance-buttons', [
- h('button.btn-primary.hero-balance-button', {
- onClick: () => history.push(SEND_ROUTE),
- }, this.context.t('send')),
- ])
- )
-}
-
-TxView.prototype.render = function () {
- const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
- const { t } = this.context
-
- return h('div.tx-view.flex-column', {
- style: {},
- }, [
-
- h('div.flex-row.phone-visible', {
- style: {
- justifyContent: 'center',
- alignItems: 'center',
- flex: '0 0 auto',
- marginBottom: '16px',
- padding: '5px',
- borderBottom: '1px solid #e5e5e5',
- },
- }, [
-
- h(Tooltip, {
- title: t('menu'),
- position: 'bottom',
- }, [
- h('div.fa.fa-bars', {
- style: {
- fontSize: '1.3em',
- cursor: 'pointer',
- padding: '10px',
- },
- onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
- }),
- ]),
-
- h(SelectedAccount),
-
- !isMascara && h(Tooltip, {
- title: t('openInTab'),
- position: 'bottom',
- }, [
- h('div.open-in-browser', {
- onClick: () => global.platform.openExtensionInBrowser(),
- }, [h('img', { src: 'images/popout.svg' })]),
- ]),
- ]),
-
- this.renderHeroBalance(),
-
- h(TxList),
-
- ])
-}
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 8e092364c..064a6ab55 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -9,7 +9,7 @@ const classnames = require('classnames')
const { checksumAddress } = require('../util')
const Identicon = require('./identicon')
// const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns
-const Tooltip = require('./tooltip-v2.js')
+const Tooltip = require('./tooltip-v2.js').default
const copyToClipboard = require('copy-to-clipboard')
const actions = require('../actions')
const BalanceComponent = require('./balance-component')
@@ -17,6 +17,8 @@ const TokenList = require('./token-list')
const selectors = require('../selectors')
const { ADD_TOKEN_ROUTE } = require('../routes')
+import Button from './button'
+
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
@@ -26,11 +28,15 @@ WalletView.contextTypes = {
t: PropTypes.func,
}
+WalletView.defaultProps = {
+ responsiveDisplayClassname: '',
+}
+
function mapStateToProps (state) {
return {
network: state.metamask.network,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
tokens: state.metamask.tokens,
@@ -131,8 +137,9 @@ WalletView.prototype.render = function () {
}
}
- return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
+ return h('div.wallet-view.flex-column', {
style: {},
+ className: responsiveDisplayClassname,
}, [
// TODO: Separate component: wallet account details
@@ -194,7 +201,9 @@ WalletView.prototype.render = function () {
h(TokenList),
- h('button.btn-primary.wallet-view__add-token-button', {
+ h(Button, {
+ type: 'primary',
+ className: 'wallet-view__add-token-button',
onClick: () => {
history.push(ADD_TOKEN_ROUTE)
sidebarOpen && hideSidebar()
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 112ea6bca..0784a872e 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -104,7 +104,7 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
if (prevTx && prevTx.status === 'dropped') {
this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
- onHide: () => history.push(DEFAULT_ROUTE),
+ onSubmit: () => history.push(DEFAULT_ROUTE),
}))
return
diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js
new file mode 100644
index 000000000..a20f6cc02
--- /dev/null
+++ b/ui/app/constants/common.js
@@ -0,0 +1,3 @@
+export const ETH = 'ETH'
+export const GWEI = 'GWEI'
+export const WEI = 'WEI'
diff --git a/ui/app/constants/transactions.js b/ui/app/constants/transactions.js
new file mode 100644
index 000000000..2dc061091
--- /dev/null
+++ b/ui/app/constants/transactions.js
@@ -0,0 +1,23 @@
+export const UNAPPROVED_STATUS = 'unapproved'
+export const REJECTED_STATUS = 'rejected'
+export const APPROVED_STATUS = 'approved'
+export const SIGNED_STATUS = 'signed'
+export const SUBMITTED_STATUS = 'submitted'
+export const CONFIRMED_STATUS = 'confirmed'
+export const FAILED_STATUS = 'failed'
+export const DROPPED_STATUS = 'dropped'
+
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
+export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
+
+export const SEND_ETHER_ACTION_KEY = 'sentEther'
+export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
+export const APPROVE_ACTION_KEY = 'approve'
+export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
+export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
+export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
+export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
+export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
+
+export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
index 38f5f1c50..f271b5683 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/conversion-util.js
@@ -35,6 +35,7 @@ BigNumber.config({
// Big Number Constants
const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
+const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1')
// Individual Setters
const convert = R.invoker(1, 'times')
@@ -52,10 +53,12 @@ const toBigNumber = {
const toNormalizedDenomination = {
WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
+ ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER),
}
const toSpecifiedDenomination = {
WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
+ ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9),
}
const baseChange = {
hex: n => n.toString(16),
diff --git a/ui/app/css/itcss/components/account-details-dropdown.scss b/ui/app/css/itcss/components/account-details-dropdown.scss
new file mode 100644
index 000000000..2a3007f83
--- /dev/null
+++ b/ui/app/css/itcss/components/account-details-dropdown.scss
@@ -0,0 +1,7 @@
+.account-details-dropdown {
+ width: 60%;
+ position: absolute;
+ top: 120px;
+ right: 15px;
+ z-index: 2000;
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 34565767f..655188a3e 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -2,10 +2,7 @@
Buttons
*/
-.btn-default,
-.btn-primary,
-.btn-secondary,
-.btn-confirm {
+.button {
height: 44px;
background: $white;
display: flex;
@@ -79,6 +76,16 @@
background-color: $curious-blue;
}
+.btn-raised {
+ color: $curious-blue;
+ background-color: $white;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
+ padding: 6px;
+ height: initial;
+ width: initial;
+ min-width: initial;
+}
+
.btn--large {
height: 54px;
}
diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss
deleted file mode 100644
index eba93ecb4..000000000
--- a/ui/app/css/itcss/components/hero-balance.scss
+++ /dev/null
@@ -1,130 +0,0 @@
-.hero-balance {
-
- @media screen and (max-width: $break-small) {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
- flex: 0 0 auto;
- padding-top: 16px;
- }
-
- @media screen and (min-width: $break-large) {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- margin: 2.3em 2.37em .8em;
- flex: 0 0 auto;
- }
-
- .balance-container {
- display: flex;
- margin: 0;
- justify-content: flex-start;
- align-items: center;
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- flex: 0 0 auto;
- max-width: 100%;
- }
-
- @media screen and (min-width: $break-large) {
- flex-direction: row;
- flex-grow: 3;
- min-width: 0;
- }
- }
-
- .balance-display {
- .token-amount {
- color: $black;
- max-width: 100%;
-
- .token-balance {
- display: flex;
- }
- }
-
- @media screen and (max-width: $break-small) {
- max-width: 100%;
- text-align: center;
-
- .token-amount {
- font-size: 1.75rem;
- margin-top: 1rem;
-
- .token-balance {
- flex-direction: column;
- }
- }
-
- .fiat-amount {
- font-size: 115%;
- margin-top: 8.5%;
- color: #a0a0a0;
- }
- }
-
- @media screen and (min-width: $break-large) {
- margin: 0 .8em;
- justify-content: flex-start;
- align-items: flex-start;
- min-width: 0;
-
- .token-amount {
- font-size: 1.5rem;
- }
-
- .fiat-amount {
- margin-top: .25%;
- font-size: 105%;
- }
- }
-
- @media #{$sub-mid-size-breakpoint-range} {
- margin-left: .4em;
- margin-right: .4em;
- justify-content: flex-start;
- align-items: flex-start;
-
- .token-amount {
- font-size: 1rem;
- }
-
- .fiat-amount {
- margin-top: .25%;
- font-size: 1rem;
- }
- }
- }
-
- .hero-balance-buttons {
-
- @media screen and (max-width: $break-small) {
- width: 100%;
- // height: 100px; // needed a round number to set the heights of the buttons inside
- flex: 0 0 auto;
- padding: 16px 0;
- }
-
- @media screen and (min-width: $break-large) {
- flex-grow: 2;
- justify-content: flex-end;
- }
- }
-}
-
-.hero-balance-button {
- min-width: initial;
- width: 6rem;
-
- @media #{$sub-mid-size-breakpoint-range} {
- padding: .4rem;
- width: 4rem;
- display: flex;
- flex: 1;
- justify-content: center;
- }
-}
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 96ad5fe64..63aa62eb3 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -1,7 +1,5 @@
@import './buttons.scss';
-@import './header.scss';
-
@import './footer.scss';
@import './network.scss';
@@ -21,8 +19,6 @@
@import './loading-overlay.scss';
// Balances
-@import './hero-balance.scss';
-
@import './wallet-balance.scss';
// Tx List and Sections
@@ -40,14 +36,14 @@
@import './gas-slider.scss';
-@import './settings.scss';
-
@import './tab-bar.scss';
@import './simple-dropdown.scss';
@import './request-signature.scss';
+@import './account-details-dropdown.scss';
+
@import './account-dropdown-mini.scss';
@import './editable-label.scss';
diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss
index b07d6af17..b023c8423 100644
--- a/ui/app/css/itcss/components/loading-overlay.scss
+++ b/ui/app/css/itcss/components/loading-overlay.scss
@@ -1,6 +1,6 @@
.loading-overlay {
left: 0;
- z-index: 50;
+ z-index: 51;
position: absolute;
flex-direction: column;
display: flex;
@@ -8,25 +8,9 @@
align-items: center;
flex: 1 1 auto;
width: 100%;
+ height: 100%;
background: rgba(255, 255, 255, .8);
- @media screen and (max-width: 575px) {
- margin-top: 66px;
- height: calc(100% - 66px);
- }
-
- @media screen and (min-width: 576px) {
- margin-top: 75px;
- height: calc(100% - 75px);
- }
-
- &--full-screen {
- position: fixed;
- height: 100vh;
- width: 100vw;
- margin-top: 0;
- }
-
&__container {
position: absolute;
top: 33%;
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
index b23876d01..833a91f12 100644
--- a/ui/app/css/itcss/components/network.scss
+++ b/ui/app/css/itcss/components/network.scss
@@ -59,6 +59,15 @@
font-weight: 500;
}
+.dropdown-menu-item .fa.delete {
+ margin-right: 10px;
+ display: none;
+}
+
+.dropdown-menu-item:hover .fa.delete {
+ display: inherit;
+}
+
.network-droppo {
right: 2px;
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index bbfd85c90..8e963d495 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -6,7 +6,6 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma
*/
// Component Colors
-$tx-view-bg: $white;
$wallet-view-bg: $alabaster;
// Main container
@@ -23,6 +22,12 @@ $wallet-view-bg: $alabaster;
display: none;
}
+.main-container-wrapper {
+ display: flex;
+ width: 100vw;
+ justify-content: center;
+}
+
//Account and transaction details
.account-and-transaction-details {
display: flex;
@@ -30,32 +35,6 @@ $wallet-view-bg: $alabaster;
min-width: 0;
}
-// tx view
-
-.tx-view {
- flex: 1 1 66.5%;
- background: $tx-view-bg;
- min-width: 0;
-
- // No title on mobile
- @media screen and (max-width: 575px) {
- .identicon-wrapper {
- display: none;
- }
-
- .account-name {
- display: none;
- }
- }
-}
-
-.open-in-browser {
- cursor: pointer;
- display: flex;
- justify-content: center;
- padding: 10px;
-}
-
// wallet view and sidebar
.wallet-view {
@@ -175,7 +154,7 @@ $wallet-view-bg: $alabaster;
}
}
-.wallet-view.sidebar {
+.wallet-view.sidebar-right {
flex: 1 0 230px;
background: rgb(250, 250, 250);
z-index: $sidebar-z-index;
@@ -193,20 +172,6 @@ $wallet-view-bg: $alabaster;
height: calc(100% - 56px);
}
-.sidebar-overlay {
- z-index: $sidebar-overlay-z-index;
- position: fixed;
- // top: 41px;
- height: 100%;
- width: 100%;
- left: 0;
- right: 0;
- bottom: 0;
- opacity: 1;
- visibility: visible;
- background-color: rgba(0, 0, 0, .3);
-}
-
// main-container media queries
@media screen and (min-width: 576px) {
@@ -260,6 +225,10 @@ $wallet-view-bg: $alabaster;
overflow-y: auto;
background-color: $white;
}
+
+ .main-container-wrapper {
+ height: 100%;
+ }
}
// wallet view
diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss
index b607aded3..445b9ebf5 100644
--- a/ui/app/css/itcss/components/request-signature.scss
+++ b/ui/app/css/itcss/components/request-signature.scss
@@ -23,6 +23,25 @@
}
}
+ &__typed-container {
+ padding: 17px;
+
+ h1 {
+ font-weight: 900;
+ margin-bottom: 5px;
+ }
+
+ * {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ > div {
+ margin-bottom: 10px;
+ }
+ }
+
&__header {
height: 64px;
width: 100%;
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index 806ac8536..a57653b45 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -622,12 +622,14 @@
position: relative;
&__down-caret {
+ z-index: 1026;
position: absolute;
top: 18px;
right: 12px;
}
&__qr-code {
+ z-index: 1026;
position: absolute;
top: 13px;
right: 33px;
@@ -647,6 +649,8 @@
&__to-autocomplete, &__memo-text-area, &__hex-data {
&__input {
+ z-index: 1025;
+ position: relative;
height: 54px;
width: 100%;
border: 1px solid $alto;
@@ -827,11 +831,17 @@
&__error-message {
display: block;
position: absolute;
- top: 4px;
- right: 4px;
+ top: -18px;
+ right: 0;
font-size: 12px;
line-height: 12px;
color: $red;
+ width: 100%;
+ text-align: center;
+ }
+
+ &__cancel {
+ margin-right: 10px;
}
}
@@ -880,12 +890,21 @@
font-size: 18px;
color: $tundora;
right: 0px;
- padding: 1px 4px;
+ padding: 0;
display: flex;
justify-content: space-around;
align-items: center;
}
+ .gas-tooltip-input-arrow-wrapper {
+ align-items: center;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: center;
+ }
+
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss
deleted file mode 100644
index 0dd61ac5e..000000000
--- a/ui/app/css/itcss/components/settings.scss
+++ /dev/null
@@ -1,214 +0,0 @@
-.settings {
- position: relative;
- background: $white;
- display: flex;
- flex-flow: column nowrap;
-}
-
-.settings__header {
- padding: 25px;
-}
-
-.settings__close-button::after {
- content: '\00D7';
- font-size: 40px;
- color: $dusty-gray;
- position: absolute;
- top: 25px;
- right: 30px;
- cursor: pointer;
-}
-
-.settings__error {
- padding-bottom: 20px;
- text-align: center;
- color: $crimson;
-}
-
-.settings__content {
- padding: 0 25px;
- height: auto;
- overflow: auto;
-}
-
-.settings__content-row {
- display: flex;
- flex-direction: row;
- padding: 10px 0 20px;
-
- @media screen and (max-width: 575px) {
- flex-direction: column;
- padding: 10px 0;
- }
-}
-
-.settings__content-item {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- padding: 0 5px;
- height: 71px;
-
- @media screen and (max-width: 575px) {
- height: initial;
- padding: 5px 0;
- }
-
- &--without-height {
- height: initial;
- }
-}
-
-.settings__content-item-col {
- max-width: 300px;
- display: flex;
- flex-direction: column;
-
- @media screen and (max-width: 575px) {
- max-width: 100%;
- width: 100%;
- }
-}
-
-.settings__content-description {
- font-size: 14px;
- color: $dusty-gray;
- padding-top: 5px;
-}
-
-.settings__input {
- padding-left: 10px;
- font-size: 14px;
- height: 40px;
- border: 1px solid $alto;
-}
-
-.settings__input::-webkit-input-placeholder {
- font-weight: 100;
- color: $dusty-gray;
-}
-
-.settings__input::-moz-placeholder {
- font-weight: 100;
- color: $dusty-gray;
-}
-
-.settings__input:-ms-input-placeholder {
- font-weight: 100;
- color: $dusty-gray;
-}
-
-.settings__input:-moz-placeholder {
- font-weight: 100;
- color: $dusty-gray;
-}
-
-.settings__provider-wrapper {
- font-size: 16px;
- border: 1px solid $alto;
- border-radius: 2px;
- padding: 15px;
- background-color: $white;
- display: flex;
- align-items: center;
- justify-content: flex-start;
-}
-
-.settings__provider-icon {
- height: 10px;
- width: 10px;
- margin-right: 10px;
- border-radius: 10px;
-}
-
-.settings__rpc-save-button {
- align-self: flex-end;
- padding: 5px;
- text-transform: uppercase;
- color: $dusty-gray;
- cursor: pointer;
-}
-
-.settings__button--red {
- border-color: lighten($monzo, 10%);
- color: $monzo;
-
- &:active {
- background: lighten($monzo, 55%);
- border-color: $monzo;
- }
-
- &:hover {
- border-color: $monzo;
- }
-}
-
-.settings__button--orange {
- border-color: lighten($ecstasy, 20%);
- color: $ecstasy;
-
- &:active {
- background: lighten($ecstasy, 40%);
- border-color: $ecstasy;
- }
-
- &:hover {
- border-color: $ecstasy;
- }
-}
-
-.settings__info-logo-wrapper {
- height: 80px;
- margin-bottom: 20px;
-}
-
-.settings__info-logo {
- max-height: 100%;
- max-width: 100%;
-}
-
-.settings__info-item {
- padding: 10px 0;
-}
-
-.settings__info-link-header {
- padding-bottom: 15px;
-
- @media screen and (max-width: 575px) {
- padding-bottom: 5px;
- }
-}
-
-.settings__info-link-item {
- padding: 15px 0;
-
- @media screen and (max-width: 575px) {
- padding: 5px 0;
- }
-}
-
-.settings__info-version-number {
- padding-top: 5px;
- font-size: 13px;
- color: $dusty-gray;
-}
-
-.settings__info-about {
- color: $dusty-gray;
- margin-bottom: 15px;
-}
-
-.settings__info-link {
- color: $curious-blue;
-}
-
-.settings__info-separator {
- margin: 15px 0;
- width: 80px;
- border-color: $alto;
- border: none;
- height: 1px;
- background-color: $alto;
- color: $alto;
-}
diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss
index 1d45ff13b..3435353d9 100644
--- a/ui/app/css/itcss/components/transaction-list.scss
+++ b/ui/app/css/itcss/components/transaction-list.scss
@@ -243,7 +243,7 @@
}
.tx-list-item {
- border-top: 1px solid rgb(231, 231, 231);
+ border-bottom: 1px solid $geyser;
flex: 0 0 auto;
display: flex;
flex-flow: row nowrap;
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
index 1885e12d1..30c32f2bf 100644
--- a/ui/app/ducks/confirm-transaction.duck.js
+++ b/ui/app/ducks/confirm-transaction.duck.js
@@ -5,9 +5,7 @@ import {
} from '../selectors/confirm-transaction'
import {
- getTokenData,
- getMethodData,
- getTransactionAmount,
+ getValueFromWeiHex,
getTransactionFee,
getHexGasTotal,
addFiat,
@@ -16,6 +14,7 @@ import {
hexGreaterThan,
} from '../helpers/confirm-transaction/util'
+import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
@@ -35,8 +34,9 @@ const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
const UPDATE_NONCE = createActionType('UPDATE_NONCE')
-const FETCH_METHOD_DATA_START = createActionType('FETCH_METHOD_DATA_START')
-const FETCH_METHOD_DATA_END = createActionType('FETCH_METHOD_DATA_END')
+const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT')
+const FETCH_DATA_START = createActionType('FETCH_DATA_START')
+const FETCH_DATA_END = createActionType('FETCH_DATA_END')
// Initial state
const initState = {
@@ -55,7 +55,8 @@ const initState = {
ethTransactionTotal: '',
hexGasTotal: '',
nonce: '',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
}
// Reducer
@@ -138,15 +139,20 @@ export default function reducer ({ confirmTransaction: confirmState = initState
...confirmState,
nonce: action.payload,
}
- case FETCH_METHOD_DATA_START:
+ case UPDATE_TO_SMART_CONTRACT:
return {
...confirmState,
- fetchingMethodData: true,
+ toSmartContract: action.payload,
}
- case FETCH_METHOD_DATA_END:
+ case FETCH_DATA_START:
return {
...confirmState,
- fetchingMethodData: false,
+ fetchingData: true,
+ }
+ case FETCH_DATA_END:
+ return {
+ ...confirmState,
+ fetchingData: false,
}
case CLEAR_CONFIRM_TRANSACTION:
return initState
@@ -237,9 +243,16 @@ export function updateNonce (nonce) {
}
}
-export function setFetchingMethodData (isFetching) {
+export function updateToSmartContract (toSmartContract) {
+ return {
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: toSmartContract,
+ }
+}
+
+export function setFetchingData (isFetching) {
return {
- type: isFetching ? FETCH_METHOD_DATA_START : FETCH_METHOD_DATA_END,
+ type: isFetching ? FETCH_DATA_START : FETCH_DATA_END,
}
}
@@ -286,10 +299,10 @@ export function updateTxDataAndCalculate (txData) {
const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
- const fiatTransactionAmount = getTransactionAmount({
+ const fiatTransactionAmount = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
- const ethTransactionAmount = getTransactionAmount({
+ const ethTransactionAmount = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})
@@ -338,19 +351,22 @@ export function setTransactionToConfirm (transactionId) {
dispatch(updateTxDataAndCalculate(txData))
const { txParams } = transaction
+ const { to } = txParams
if (txParams.data) {
const { tokens: existingTokens } = state
const { data, to: tokenAddress } = txParams
try {
- dispatch(setFetchingMethodData(true))
+ dispatch(setFetchingData(true))
const methodData = await getMethodData(data)
dispatch(updateMethodData(methodData))
- dispatch(setFetchingMethodData(false))
+ const toSmartContract = await isSmartContractAddress(to)
+ dispatch(updateToSmartContract(toSmartContract))
+ dispatch(setFetchingData(false))
} catch (error) {
dispatch(updateMethodData({}))
- dispatch(setFetchingMethodData(false))
+ dispatch(setFetchingData(false))
}
const tokenData = getTokenData(data)
diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js
index 111674e33..1bab0add0 100644
--- a/ui/app/ducks/tests/confirm-transaction.duck.test.js
+++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js
@@ -1,6 +1,7 @@
import assert from 'assert'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
+import sinon from 'sinon'
import ConfirmTransactionReducer, * as actions from '../confirm-transaction.duck.js'
@@ -20,7 +21,8 @@ const initialState = {
ethTransactionTotal: '',
hexGasTotal: '',
nonce: '',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
}
const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
@@ -35,8 +37,9 @@ const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTI
const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL'
const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
-const FETCH_METHOD_DATA_START = 'metamask/confirm-transaction/FETCH_METHOD_DATA_START'
-const FETCH_METHOD_DATA_END = 'metamask/confirm-transaction/FETCH_METHOD_DATA_END'
+const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT'
+const FETCH_DATA_START = 'metamask/confirm-transaction/FETCH_DATA_START'
+const FETCH_DATA_END = 'metamask/confirm-transaction/FETCH_DATA_END'
const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION'
describe('Confirm Transaction Duck', () => {
@@ -64,7 +67,8 @@ describe('Confirm Transaction Duck', () => {
ethTransactionTotal: '469.27',
hexGasTotal: '0x1319718a5000',
nonce: '0x0',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
},
}
@@ -271,30 +275,43 @@ describe('Confirm Transaction Duck', () => {
)
})
- it('should set fetchingMethodData to true when receiving a FETCH_METHOD_DATA_START action', () => {
+ it('should update nonce when receiving an UPDATE_TO_SMART_CONTRACT action', () => {
assert.deepEqual(
ConfirmTransactionReducer(mockState, {
- type: FETCH_METHOD_DATA_START,
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: true,
}),
{
...mockState.confirmTransaction,
- fetchingMethodData: true,
+ toSmartContract: true,
}
)
})
- it('should set fetchingMethodData to false when receiving a FETCH_METHOD_DATA_END action', () => {
+ it('should set fetchingData to true when receiving a FETCH_DATA_START action', () => {
assert.deepEqual(
- ConfirmTransactionReducer({ confirmTransaction: { fetchingMethodData: true } }, {
- type: FETCH_METHOD_DATA_END,
+ ConfirmTransactionReducer(mockState, {
+ type: FETCH_DATA_START,
}),
{
- fetchingMethodData: false,
+ ...mockState.confirmTransaction,
+ fetchingData: true,
}
)
})
- it('should clear confirmTransaction when receiving a FETCH_METHOD_DATA_END action', () => {
+ it('should set fetchingData to false when receiving a FETCH_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({ confirmTransaction: { fetchingData: true } }, {
+ type: FETCH_DATA_END,
+ }),
+ {
+ fetchingData: false,
+ }
+ )
+ })
+
+ it('should clear confirmTransaction when receiving a FETCH_DATA_END action', () => {
assert.deepEqual(
ConfirmTransactionReducer(mockState, {
type: CLEAR_CONFIRM_TRANSACTION,
@@ -460,24 +477,24 @@ describe('Confirm Transaction Duck', () => {
)
})
- it('should create an action to set fetchingMethodData to true', () => {
+ it('should create an action to set fetchingData to true', () => {
const expectedAction = {
- type: FETCH_METHOD_DATA_START,
+ type: FETCH_DATA_START,
}
assert.deepEqual(
- actions.setFetchingMethodData(true),
+ actions.setFetchingData(true),
expectedAction
)
})
- it('should create an action to set fetchingMethodData to false', () => {
+ it('should create an action to set fetchingData to false', () => {
const expectedAction = {
- type: FETCH_METHOD_DATA_END,
+ type: FETCH_DATA_END,
}
assert.deepEqual(
- actions.setFetchingMethodData(false),
+ actions.setFetchingData(false),
expectedAction
)
})
@@ -495,6 +512,18 @@ describe('Confirm Transaction Duck', () => {
})
describe('Thunk actions', done => {
+ beforeEach(() => {
+ global.eth = {
+ getCode: sinon.stub().callsFake(
+ address => Promise.resolve(address && address.match(/isContract/) ? 'not-0x' : '0x')
+ ),
+ }
+ })
+
+ afterEach(() => {
+ global.eth.getCode.resetHistory()
+ })
+
it('updates txData and gas on an existing transaction in confirmTransaction', () => {
const mockState = {
metamask: {
@@ -505,7 +534,7 @@ describe('Confirm Transaction Duck', () => {
ethTransactionAmount: '1',
ethTransactionFee: '0.000021',
ethTransactionTotal: '1.000021',
- fetchingMethodData: false,
+ fetchingData: false,
fiatTransactionAmount: '469.26',
fiatTransactionFee: '0.01',
fiatTransactionTotal: '469.27',
@@ -581,7 +610,7 @@ describe('Confirm Transaction Duck', () => {
ethTransactionAmount: '1',
ethTransactionFee: '0.000021',
ethTransactionTotal: '1.000021',
- fetchingMethodData: false,
+ fetchingData: false,
fiatTransactionAmount: '469.26',
fiatTransactionFee: '0.01',
fiatTransactionTotal: '469.27',
@@ -667,6 +696,7 @@ describe('Confirm Transaction Duck', () => {
.then(() => {
const storeActions = store.getActions()
assert.equal(storeActions.length, expectedActions.length)
+
storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
done()
})
diff --git a/ui/app/helpers/common.util.js b/ui/app/helpers/common.util.js
new file mode 100644
index 000000000..0c02481e6
--- /dev/null
+++ b/ui/app/helpers/common.util.js
@@ -0,0 +1,5 @@
+export function camelCaseToCapitalize (str = '') {
+ return str
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, str => str.toUpperCase())
+}
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
index 76e80a8ac..bcac22500 100644
--- a/ui/app/helpers/confirm-transaction/util.js
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -1,15 +1,8 @@
import currencyFormatter from 'currency-formatter'
import currencies from 'currency-formatter/currencies'
-import abi from 'human-standard-token-abi'
-import abiDecoder from 'abi-decoder'
import ethUtil from 'ethereumjs-util'
import BigNumber from 'bignumber.js'
-abiDecoder.addABI(abi)
-
-import MethodRegistry from 'eth-method-registry'
-const registry = new MethodRegistry({ provider: global.ethereumProvider })
-
import {
conversionUtil,
addCurrencies,
@@ -19,22 +12,6 @@ import {
import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
-export function getTokenData (data = {}) {
- return abiDecoder.decodeMethod(data)
-}
-
-export async function getMethodData (data = {}) {
- const prefixedData = ethUtil.addHexPrefix(data)
- const fourBytePrefix = prefixedData.slice(0, 10)
- const sig = await registry.lookup(fourBytePrefix)
- const parsedResult = registry.parse(sig)
-
- return {
- name: parsedResult.name,
- params: parsedResult.args,
- }
-}
-
export function increaseLastGasPrice (lastGasPrice) {
return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
@@ -76,11 +53,12 @@ export function addFiat (...args) {
})
}
-export function getTransactionAmount ({
+export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,
numberOfDecimals,
+ toDenomination,
}) {
return conversionUtil(value, {
fromNumericBase: 'hex',
@@ -89,6 +67,7 @@ export function getTransactionAmount ({
toCurrency,
numberOfDecimals,
fromDenomination: 'WEI',
+ toDenomination,
conversionRate,
})
}
diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js
index a9c8fae34..4c1a3e16b 100644
--- a/ui/app/helpers/confirm-transaction/util.test.js
+++ b/ui/app/helpers/confirm-transaction/util.test.js
@@ -92,9 +92,9 @@ describe('Confirm Transaction utils', () => {
})
})
- describe('getTransactionAmount', () => {
+ describe('getValueFromWeiHex', () => {
it('should get the transaction amount in ETH', () => {
- const ethTransactionAmount = utils.getTransactionAmount({
+ const ethTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
})
@@ -102,7 +102,7 @@ describe('Confirm Transaction utils', () => {
})
it('should get the transaction amount in fiat', () => {
- const fiatTransactionAmount = utils.getTransactionAmount({
+ const fiatTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
})
diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js
new file mode 100644
index 000000000..20ef9e35b
--- /dev/null
+++ b/ui/app/helpers/conversions.util.js
@@ -0,0 +1,63 @@
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil } from '../conversion-util'
+import { ETH, GWEI, WEI } from '../constants/common'
+
+export function bnToHex (inputBn) {
+ return ethUtil.addHexPrefix(inputBn.toString(16))
+}
+
+export function hexToDecimal (hexValue) {
+ return conversionUtil(hexValue, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+}
+
+export function decimalToHex (decimal) {
+ return conversionUtil(decimal, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+}
+
+export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDecimals = 6 }) {
+ const denominations = [ETH, GWEI, WEI]
+
+ let nonZeroDenomination
+
+ for (let i = 0; i < denominations.length; i++) {
+ const convertedValue = getValueFromWeiHex({
+ value,
+ conversionRate,
+ toCurrency: ETH,
+ numberOfDecimals,
+ toDenomination: denominations[i],
+ })
+
+ if (convertedValue !== '0' || i === denominations.length - 1) {
+ nonZeroDenomination = `${convertedValue} ${denominations[i]}`
+ break
+ }
+ }
+
+ return nonZeroDenomination
+}
+
+export function getValueFromWeiHex ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+ toDenomination,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: ETH,
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: WEI,
+ toDenomination,
+ conversionRate,
+ })
+}
diff --git a/ui/app/helpers/tests/common.util.test.js b/ui/app/helpers/tests/common.util.test.js
new file mode 100644
index 000000000..a52b91a10
--- /dev/null
+++ b/ui/app/helpers/tests/common.util.test.js
@@ -0,0 +1,27 @@
+import * as utils from '../common.util'
+import assert from 'assert'
+
+describe('Common utils', () => {
+ describe('camelCaseToCapitalize', () => {
+ it('should return a capitalized string from a camel-cased string', () => {
+ const tests = [
+ {
+ test: undefined,
+ expected: '',
+ },
+ {
+ test: '',
+ expected: '',
+ },
+ {
+ test: 'thisIsATest',
+ expected: 'This Is A Test',
+ },
+ ]
+
+ tests.forEach(({ test, expected }) => {
+ assert.equal(utils.camelCaseToCapitalize(test), expected)
+ })
+ })
+ })
+})
diff --git a/ui/app/helpers/tests/transactions.util.test.js b/ui/app/helpers/tests/transactions.util.test.js
new file mode 100644
index 000000000..103a84a8c
--- /dev/null
+++ b/ui/app/helpers/tests/transactions.util.test.js
@@ -0,0 +1,22 @@
+import * as utils from '../transactions.util'
+import assert from 'assert'
+
+describe('Transactions utils', () => {
+ describe('getTokenData', () => {
+ it('should return token data', () => {
+ const tokenData = utils.getTokenData('0xa9059cbb00000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000004e20')
+ assert.ok(tokenData)
+ const { name, params } = tokenData
+ assert.equal(name, 'transfer')
+ const [to, value] = params
+ assert.equal(to.name, '_to')
+ assert.equal(to.type, 'address')
+ assert.equal(value.name, '_value')
+ assert.equal(value.type, 'uint256')
+ })
+
+ it('should not throw errors when called without arguments', () => {
+ assert.doesNotThrow(() => utils.getTokenData())
+ })
+ })
+})
diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js
new file mode 100644
index 000000000..f7d249e63
--- /dev/null
+++ b/ui/app/helpers/transactions.util.js
@@ -0,0 +1,128 @@
+import ethUtil from 'ethereumjs-util'
+import MethodRegistry from 'eth-method-registry'
+import abi from 'human-standard-token-abi'
+import abiDecoder from 'abi-decoder'
+
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+ SEND_ETHER_ACTION_KEY,
+ DEPLOY_CONTRACT_ACTION_KEY,
+ APPROVE_ACTION_KEY,
+ SEND_TOKEN_ACTION_KEY,
+ TRANSFER_FROM_ACTION_KEY,
+ SIGNATURE_REQUEST_KEY,
+ UNKNOWN_FUNCTION_KEY,
+ CANCEL_ATTEMPT_ACTION_KEY,
+} from '../constants/transactions'
+
+import { addCurrencies } from '../conversion-util'
+
+abiDecoder.addABI(abi)
+
+export function getTokenData (data = '') {
+ return abiDecoder.decodeMethod(data)
+}
+
+const registry = new MethodRegistry({ provider: global.ethereumProvider })
+
+export async function getMethodData (data = '') {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ const sig = await registry.lookup(fourBytePrefix)
+ const parsedResult = registry.parse(sig)
+
+ return {
+ name: parsedResult.name,
+ params: parsedResult.args,
+ }
+}
+
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
+
+/**
+ * Returns the action of a transaction as a key to be passed into the translator.
+ * @param {Object} transaction - txData object
+ * @param {Object} methodData - Data returned from eth-method-registry
+ * @returns {string|undefined}
+ */
+export async function getTransactionActionKey (transaction, methodData) {
+ const { txParams: { data, to } = {}, msgParams, type } = transaction
+
+ if (type === 'cancel') {
+ return CANCEL_ATTEMPT_ACTION_KEY
+ }
+
+ if (msgParams) {
+ return SIGNATURE_REQUEST_KEY
+ }
+
+ if (isConfirmDeployContract(transaction)) {
+ return DEPLOY_CONTRACT_ACTION_KEY
+ }
+
+ if (data) {
+ const toSmartContract = await isSmartContractAddress(to)
+
+ if (!toSmartContract) {
+ return SEND_ETHER_ACTION_KEY
+ }
+
+ const { name } = methodData
+ const methodName = name && name.toLowerCase()
+
+ if (!methodName) {
+ return UNKNOWN_FUNCTION_KEY
+ }
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER:
+ return SEND_TOKEN_ACTION_KEY
+ case TOKEN_METHOD_APPROVE:
+ return APPROVE_ACTION_KEY
+ case TOKEN_METHOD_TRANSFER_FROM:
+ return TRANSFER_FROM_ACTION_KEY
+ default:
+ return undefined
+ }
+ } else {
+ return SEND_ETHER_ACTION_KEY
+ }
+}
+
+export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') {
+ if (!transactions.length) {
+ return {}
+ }
+
+ return transactions.reduce((acc, current) => {
+ const { submittedTime, txParams: { nonce: currentNonce } = {} } = current
+
+ if (currentNonce === nonce) {
+ return acc.submittedTime
+ ? submittedTime > acc.submittedTime ? current : acc
+ : current
+ } else {
+ return acc
+ }
+ }, {})
+}
+
+export async function isSmartContractAddress (address) {
+ const code = await global.eth.getCode(address)
+ return code && code !== '0x'
+}
+
+export function sumHexes (...args) {
+ const total = args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'hex',
+ })
+ })
+
+ return ethUtil.addHexPrefix(total)
+}
diff --git a/ui/app/higher-order-components/with-method-data/index.js b/ui/app/higher-order-components/with-method-data/index.js
new file mode 100644
index 000000000..f511e1ae7
--- /dev/null
+++ b/ui/app/higher-order-components/with-method-data/index.js
@@ -0,0 +1 @@
+export { default } from './with-method-data.component'
diff --git a/ui/app/higher-order-components/with-method-data/with-method-data.component.js b/ui/app/higher-order-components/with-method-data/with-method-data.component.js
new file mode 100644
index 000000000..fed7d9865
--- /dev/null
+++ b/ui/app/higher-order-components/with-method-data/with-method-data.component.js
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { getMethodData } from '../../helpers/transactions.util'
+
+export default function withMethodData (WrappedComponent) {
+ return class MethodDataWrappedComponent extends PureComponent {
+ static propTypes = {
+ transaction: PropTypes.object,
+ }
+
+ static defaultProps = {
+ transaction: {},
+ }
+
+ state = {
+ methodData: {},
+ done: false,
+ error: null,
+ }
+
+ componentDidMount () {
+ this.fetchMethodData()
+ }
+
+ async fetchMethodData () {
+ const { transaction } = this.props
+ const { txParams: { data = '' } = {} } = transaction
+
+ if (data) {
+ try {
+ const methodData = await getMethodData(data)
+ this.setState({ methodData, done: true })
+ } catch (error) {
+ this.setState({ done: true, error })
+ }
+ } else {
+ this.setState({ done: true })
+ }
+ }
+
+ render () {
+ const { methodData, done, error } = this.state
+
+ return (
+ <WrappedComponent
+ { ...this.props }
+ methodData={{ data: methodData, done, error }}
+ />
+ )
+ }
+ }
+}
diff --git a/ui/app/higher-order-components/with-modal-props/index.js b/ui/app/higher-order-components/with-modal-props/index.js
new file mode 100644
index 000000000..e476b51d2
--- /dev/null
+++ b/ui/app/higher-order-components/with-modal-props/index.js
@@ -0,0 +1 @@
+export { default } from './with-modal-props'
diff --git a/ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js
new file mode 100644
index 000000000..654e7062a
--- /dev/null
+++ b/ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js
@@ -0,0 +1,43 @@
+
+import assert from 'assert'
+import configureMockStore from 'redux-mock-store'
+import { mount } from 'enzyme'
+import React from 'react'
+import withModalProps from '../with-modal-props'
+
+const mockState = {
+ appState: {
+ modal: {
+ modalState: {
+ props: {
+ prop1: 'prop1',
+ prop2: 2,
+ prop3: true,
+ },
+ },
+ },
+ },
+}
+
+describe('withModalProps', () => {
+ it('should return a component wrapped with modal state props', () => {
+ const TestComponent = props => (
+ <div className="test">Testing</div>
+ )
+ const WrappedComponent = withModalProps(TestComponent)
+ const store = configureMockStore()(mockState)
+ const wrapper = mount(
+ <WrappedComponent store={store} />
+ )
+
+ assert.ok(wrapper)
+ const testComponent = wrapper.find(TestComponent).at(0)
+ assert.equal(testComponent.length, 1)
+ assert.equal(testComponent.find('.test').text(), 'Testing')
+ const testComponentProps = testComponent.props()
+ assert.equal(testComponentProps.prop1, 'prop1')
+ assert.equal(testComponentProps.prop2, 2)
+ assert.equal(testComponentProps.prop3, true)
+ assert.equal(typeof testComponentProps.hideModal, 'function')
+ })
+})
diff --git a/ui/app/higher-order-components/with-modal-props/with-modal-props.js b/ui/app/higher-order-components/with-modal-props/with-modal-props.js
new file mode 100644
index 000000000..02f3855af
--- /dev/null
+++ b/ui/app/higher-order-components/with-modal-props/with-modal-props.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import { hideModal } from '../../actions'
+
+const mapStateToProps = state => {
+ const { appState } = state
+ const { props: modalProps } = appState.modal.modalState
+
+ return {
+ ...modalProps,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ }
+}
+
+export default function withModalProps (Component) {
+ return connect(mapStateToProps, mapDispatchToProps)(Component)
+}
diff --git a/ui/app/higher-order-components/with-token-tracker/index.js b/ui/app/higher-order-components/with-token-tracker/index.js
new file mode 100644
index 000000000..d401e81f1
--- /dev/null
+++ b/ui/app/higher-order-components/with-token-tracker/index.js
@@ -0,0 +1 @@
+export { default } from './with-token-tracker.component'
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js
index 8608b15f4..36f6a6efd 100644
--- a/ui/app/helpers/with-token-tracker.js
+++ b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from 'eth-token-tracker'
-const withTokenTracker = WrappedComponent => {
+export default function withTokenTracker (WrappedComponent) {
return class TokenTrackerWrappedComponent extends Component {
static propTypes = {
userAddress: PropTypes.string.isRequired,
@@ -104,5 +104,3 @@ const withTokenTracker = WrappedComponent => {
}
}
}
-
-module.exports = withTokenTracker
diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js
index d46911f7c..3419474c4 100644
--- a/ui/app/i18n-provider.js
+++ b/ui/app/i18n-provider.js
@@ -6,6 +6,11 @@ const { compose } = require('recompose')
const t = require('../i18n-helper').getMessage
class I18nProvider extends Component {
+ tOrDefault = (key, defaultValue, ...args) => {
+ const { localeMessages: { current, en } = {} } = this.props
+ return t(current, key, ...args) || t(en, key, ...args) || defaultValue
+ }
+
getChildContext () {
const { localeMessages } = this.props
const { current, en } = localeMessages
@@ -13,6 +18,10 @@ class I18nProvider extends Component {
t (key, ...args) {
return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
},
+ tOrDefault: this.tOrDefault,
+ tOrKey (key, ...args) {
+ return this.tOrDefault(key, key, ...args)
+ },
}
}
@@ -28,6 +37,8 @@ I18nProvider.propTypes = {
I18nProvider.childContextTypes = {
t: PropTypes.func,
+ tOrDefault: PropTypes.func,
+ tOrKey: PropTypes.func,
}
const mapStateToProps = state => {
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
deleted file mode 100644
index 8a0708025..000000000
--- a/ui/app/main-container.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const AccountAndTransactionDetails = require('./account-and-transaction-details')
-const Settings = require('./components/pages/settings')
-const log = require('loglevel')
-
-import UnlockScreen from './components/pages/unlock-page'
-
-module.exports = MainContainer
-
-inherits(MainContainer, Component)
-function MainContainer () {
- Component.call(this)
-}
-
-MainContainer.prototype.render = function () {
- // 3. summarize:
- // switch statement goes inside MainContainer,
- // or a method in renderPrimary
- // - pass resulting h() to MainContainer
- // - error checking in separate func
- // - router in separate func
- const contents = {
- component: AccountAndTransactionDetails,
- key: 'account-detail',
- style: {},
- }
-
- if (this.props.isUnlocked === false) {
- switch (this.props.currentViewName) {
- case 'config':
- log.debug('rendering config screen from unlock screen.')
- return h(Settings, {key: 'config'})
- default:
- log.debug('rendering locked screen')
- return h('.unlock-screen-container', {}, h(UnlockScreen, { key: 'locked' }))
- }
- }
-
- return h('div.main-container', {
- style: contents.style,
- }, [
- h(contents.component, {
- key: contents.key,
- }, []),
- ])
-}
-
diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js
deleted file mode 100644
index cc9633166..000000000
--- a/ui/app/new-keychain.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-
-module.exports = connect(mapStateToProps)(NewKeychain)
-
-function mapStateToProps (state) {
- return {}
-}
-
-inherits(NewKeychain, Component)
-function NewKeychain () {
- Component.call(this)
-}
-
-NewKeychain.prototype.render = function () {
- // const props = this.props
-
- return (
- h('div', {
- style: {
- background: 'blue',
- },
- }, [
- h('h1', `Here's a list!!!!`),
- ])
- )
-}
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index c246e7904..5c86d397d 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -48,7 +48,11 @@ function reduceApp (state, action) {
name: null,
},
},
- sidebarOpen: false,
+ sidebar: {
+ isOpen: false,
+ transitionName: '',
+ type: '',
+ },
alertOpen: false,
alertMessage: null,
qrCodeData: null,
@@ -88,12 +92,18 @@ function reduceApp (state, action) {
// sidebar methods
case actions.SIDEBAR_OPEN:
return extend(appState, {
- sidebarOpen: true,
+ sidebar: {
+ ...action.value,
+ isOpen: true,
+ },
})
case actions.SIDEBAR_CLOSE:
return extend(appState, {
- sidebarOpen: false,
+ sidebar: {
+ ...appState.sidebar,
+ isOpen: false,
+ },
})
// alert methods
@@ -209,6 +219,15 @@ function reduceApp (state, action) {
transForward: action.value,
})
+ case actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'add-suggested-token',
+ context: appState.currentView.context,
+ },
+ transForward: action.value,
+ })
+
case actions.SHOW_IMPORT_PAGE:
return extend(appState, {
currentView: {
diff --git a/ui/app/routes.js b/ui/app/routes.js
index f6b2a7a55..76afed5db 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -7,6 +7,7 @@ const CONFIRM_SEED_ROUTE = '/confirm-seed'
const RESTORE_VAULT_ROUTE = '/restore-vault'
const ADD_TOKEN_ROUTE = '/add-token'
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
+const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
@@ -41,6 +42,7 @@ module.exports = {
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index d86462275..fb4517628 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -1,6 +1,9 @@
-const valuesFor = require('./util').valuesFor
const abi = require('human-standard-token-abi')
+import {
+ transactionsSelector,
+} from './selectors/transactions'
+
const {
multiplyCurrencies,
} = require('./conversion-util')
@@ -11,6 +14,8 @@ const selectors = {
getSelectedAccount,
getSelectedToken,
getSelectedTokenExchangeRate,
+ getSelectedTokenAssetImage,
+ getAssetImages,
getTokenExchangeRate,
conversionRateSelector,
transactionsSelector,
@@ -68,6 +73,18 @@ function getSelectedTokenExchangeRate (state) {
return contractExchangeRates[address] || 0
}
+function getSelectedTokenAssetImage (state) {
+ const assetImages = state.metamask.assetImages || {}
+ const selectedToken = getSelectedToken(state) || {}
+ const { address } = selectedToken
+ return assetImages[address]
+}
+
+function getAssetImages (state) {
+ const assetImages = state.metamask.assetImages || {}
+ return assetImages
+}
+
function getTokenExchangeRate (state, address) {
const contractExchangeRates = state.metamask.contractExchangeRates
return contractExchangeRates[address] || 0
@@ -101,22 +118,6 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
-function transactionsSelector (state) {
- const { network, selectedTokenAddress } = state.metamask
- const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
- const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
- const transactions = state.metamask.selectedAddressTxList || []
- const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
-
- // console.log({txsToRender, selectedTokenAddress})
- return selectedTokenAddress
- ? txsToRender
- .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
- .sort((a, b) => b.time - a.time)
- : txsToRender
- .sort((a, b) => b.time - a.time)
-}
-
function getGasIsLoading (state) {
return state.appState.gasIsLoading
}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index 6e760c429..86b10bac3 100644
--- a/ui/app/selectors/confirm-transaction.js
+++ b/ui/app/selectors/confirm-transaction.js
@@ -126,7 +126,8 @@ const TOKEN_PARAM_VALUE = '_value'
export const tokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
- params => {
+ tokenDecimalsSelector,
+ (params, tokenDecimals) => {
let toAddress = ''
let tokenAmount = 0
@@ -136,6 +137,10 @@ export const tokenAmountAndToAddressSelector = createSelector(
toAddress = toParam ? toParam.value : params[0].value
const value = valueParam ? Number(valueParam.value) : Number(params[1].value)
tokenAmount = roundExponential(value)
+
+ if (tokenDecimals) {
+ tokenAmount = calcTokenAmount(value, tokenDecimals)
+ }
}
return {
diff --git a/ui/app/selectors/tokens.js b/ui/app/selectors/tokens.js
new file mode 100644
index 000000000..47b6e0192
--- /dev/null
+++ b/ui/app/selectors/tokens.js
@@ -0,0 +1,11 @@
+import { createSelector } from 'reselect'
+
+export const selectedTokenAddressSelector = state => state.metamask.selectedTokenAddress
+export const tokenSelector = state => state.metamask.tokens
+export const selectedTokenSelector = createSelector(
+ tokenSelector,
+ selectedTokenAddressSelector,
+ (tokens = [], selectedTokenAddress = '') => {
+ return tokens.find(({ address }) => address === selectedTokenAddress)
+ }
+)
diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js
new file mode 100644
index 000000000..479002794
--- /dev/null
+++ b/ui/app/selectors/transactions.js
@@ -0,0 +1,58 @@
+import { createSelector } from 'reselect'
+import { valuesFor } from '../util'
+import {
+ UNAPPROVED_STATUS,
+ APPROVED_STATUS,
+ SUBMITTED_STATUS,
+} from '../constants/transactions'
+
+import { selectedTokenAddressSelector } from './tokens'
+
+export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
+export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
+export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
+
+const pendingStatusHash = {
+ [UNAPPROVED_STATUS]: true,
+ [APPROVED_STATUS]: true,
+ [SUBMITTED_STATUS]: true,
+}
+
+export const transactionsSelector = createSelector(
+ selectedTokenAddressSelector,
+ unapprovedMsgsSelector,
+ shapeShiftTxListSelector,
+ selectedAddressTxListSelector,
+ (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
+ const unapprovedMsgsList = valuesFor(unapprovedMsgs)
+ const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
+
+ return selectedTokenAddress
+ ? txsToRender
+ .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
+ .sort((a, b) => b.time - a.time)
+ : txsToRender
+ .sort((a, b) => b.time - a.time)
+ }
+)
+
+export const pendingTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => transaction.status in pendingStatusHash).reverse()
+ )
+)
+
+export const submittedPendingTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
+ )
+)
+
+export const completedTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => !(transaction.status in pendingStatusHash))
+ )
+)
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index 8798ed266..6e4992763 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -1,55 +1,118 @@
const log = require('loglevel')
const util = require('./util')
const BigNumber = require('bignumber.js')
+import contractMap from 'eth-contract-metadata'
-function tokenInfoGetter () {
- const tokens = {}
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
- return async (address) => {
- if (tokens[address]) {
- return tokens[address]
+const DEFAULT_SYMBOL = ''
+const DEFAULT_DECIMALS = '0'
+
+async function getSymbolFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.symbol()
+ return result[0]
+ } catch (error) {
+ log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+async function getDecimalsFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.decimals()
+ const decimalsBN = result[0]
+ return decimalsBN && decimalsBN.toString()
+ } catch (error) {
+ log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+function getContractMetadata (tokenAddress) {
+ return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]
+}
+
+async function getSymbol (tokenAddress) {
+ let symbol = await getSymbolFromContract(tokenAddress)
+
+ if (!symbol) {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ symbol = contractMetadataInfo.symbol
}
+ }
- tokens[address] = await getSymbolAndDecimals(address)
+ return symbol
+}
- return tokens[address]
+async function getDecimals (tokenAddress) {
+ let decimals = await getDecimalsFromContract(tokenAddress)
+
+ if (!decimals || decimals === '0') {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ decimals = contractMetadataInfo.decimals
+ }
}
+
+ return decimals
}
-async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
+export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
const existingToken = existingTokens.find(({ address }) => tokenAddress === address)
+
if (existingToken) {
- return existingToken
+ return {
+ symbol: existingToken.symbol,
+ decimals: existingToken.decimals,
+ }
}
- let result = []
+ let symbol, decimals
+
try {
- const token = util.getContractAtAddress(tokenAddress)
+ symbol = await getSymbol(tokenAddress)
+ decimals = await getDecimals(tokenAddress)
+ } catch (error) {
+ log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error)
+ }
- result = await Promise.all([
- token.symbol(),
- token.decimals(),
- ])
- } catch (err) {
- log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err)
+ return {
+ symbol: symbol || DEFAULT_SYMBOL,
+ decimals: decimals || DEFAULT_DECIMALS,
}
+}
- const [ symbol = [], decimals = [] ] = result
+export function tokenInfoGetter () {
+ const tokens = {}
- return {
- symbol: symbol[0] || null,
- decimals: decimals[0] && decimals[0].toString() || null,
+ return async (address) => {
+ if (tokens[address]) {
+ return tokens[address]
+ }
+
+ tokens[address] = await getSymbolAndDecimals(address)
+
+ return tokens[address]
}
}
-function calcTokenAmount (value, decimals) {
+export function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
return new BigNumber(String(value)).div(multiplier).toNumber()
}
-
-module.exports = {
- tokenInfoGetter,
- calcTokenAmount,
- getSymbolAndDecimals,
+export function getTokenValue (tokenParams = []) {
+ const valueData = tokenParams.find(param => param.name === '_value')
+ return valueData && valueData.value
}
diff --git a/ui/app/util.js b/ui/app/util.js
index ade4fec8a..37c0fb698 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -9,7 +9,7 @@ const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
// formatData :: ( date: <Unix Timestamp> ) -> String
function formatDate (date) {
- return vreme.format(new Date(date), 'March 16 2014 14:30')
+ return vreme.format(new Date(date), '3/16/2014 at 14:30')
}
var valueTable = {