aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkumavis <aaron@kumavis.me>2019-03-29 11:54:12 +0800
committerkumavis <aaron@kumavis.me>2019-03-29 11:54:12 +0800
commit781a39c039500fcfe8fce3c3d75081ff781c4cf1 (patch)
tree5f7ba6db12b471ddd82e6be1d6e7dde0560d2cfd
parent07c974525859eba81471ec7591821ea010addf5a (diff)
parent649a1d483a574dcff902708f95b37329a02709a8 (diff)
downloadtangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar.gz
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar.bz2
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar.lz
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar.xz
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.tar.zst
tangerine-wallet-browser-781a39c039500fcfe8fce3c3d75081ff781c4cf1.zip
Merge branch 'develop' into clearNotices
-rw-r--r--CHANGELOG.md15
-rw-r--r--app/_locales/en/messages.json45
-rw-r--r--app/images/caret-left-black.svg18
-rw-r--r--app/images/wyre.svg9
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/controllers/preferences.js4
-rw-r--r--app/scripts/controllers/transactions/tx-gas-utils.js2
-rw-r--r--app/scripts/inpage.js4
-rw-r--r--app/scripts/lib/auto-reload.js4
-rw-r--r--app/scripts/lib/buy-eth-url.js2
-rw-r--r--development/mock-dev.js6
-rw-r--r--development/ui-dev.js2
-rw-r--r--development/uiStore.js2
-rw-r--r--docs/adding-new-networks.md2
-rw-r--r--docs/secret-preferences.md2
-rw-r--r--mascara/src/app/buy-ether-widget/index.js2
-rw-r--r--mascara/src/app/shapeshift-form/index.js4
-rw-r--r--package-lock.json14
-rw-r--r--test/e2e/beta/metamask-beta-ui.spec.js18
-rw-r--r--test/unit/actions/config_test.js4
-rw-r--r--test/unit/actions/set_account_label_test.js4
-rw-r--r--test/unit/actions/set_selected_account_test.js4
-rw-r--r--test/unit/actions/tx_test.js2
-rw-r--r--test/unit/actions/view_info_test.js4
-rw-r--r--test/unit/actions/warning_test.js4
-rw-r--r--test/unit/app/buy-eth-url.spec.js11
-rw-r--r--test/unit/balance-formatter-test.js2
-rw-r--r--test/unit/reducers/unlock_vault_test.js4
-rw-r--r--test/unit/responsive/components/dropdown-test.js2
-rw-r--r--test/unit/ui/app/actions.spec.js2
-rw-r--r--test/unit/ui/app/components/token-cell.spec.js4
-rw-r--r--test/unit/ui/app/reducers/app.spec.js4
-rw-r--r--test/unit/ui/app/reducers/metamask.spec.js4
-rw-r--r--test/unit/ui/app/selectors.spec.js2
-rw-r--r--test/unit/util_test.js2
-rw-r--r--ui/.gitignore66
-rw-r--r--ui/app/accounts/new-account/index.js87
-rw-r--r--ui/app/actions.js2774
-rw-r--r--ui/app/app.js441
-rw-r--r--ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js84
-rw-r--r--ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js107
-rw-r--r--ui/app/components/account-dropdowns.js338
-rw-r--r--ui/app/components/account-menu/account-menu.component.js340
-rw-r--r--ui/app/components/account-menu/account-menu.container.js62
-rw-r--r--ui/app/components/account-panel.js86
-rw-r--r--ui/app/components/app-header/app-header.component.js127
-rw-r--r--ui/app/components/app-header/app-header.container.js40
-rw-r--r--ui/app/components/app/account-dropdowns.js338
-rw-r--r--ui/app/components/app/account-menu/account-menu.component.js340
-rw-r--r--ui/app/components/app/account-menu/account-menu.container.js62
-rw-r--r--ui/app/components/app/account-menu/index.js (renamed from ui/app/components/account-menu/index.js)0
-rw-r--r--ui/app/components/app/account-menu/index.scss (renamed from ui/app/components/account-menu/index.scss)0
-rw-r--r--ui/app/components/app/account-panel.js86
-rw-r--r--ui/app/components/app/add-token-button/add-token-button.component.js (renamed from ui/app/components/add-token-button/add-token-button.component.js)0
-rw-r--r--ui/app/components/app/add-token-button/index.js (renamed from ui/app/components/add-token-button/index.js)0
-rw-r--r--ui/app/components/app/add-token-button/index.scss (renamed from ui/app/components/add-token-button/index.scss)0
-rw-r--r--ui/app/components/app/app-header/app-header.component.js127
-rw-r--r--ui/app/components/app/app-header/app-header.container.js40
-rw-r--r--ui/app/components/app/app-header/index.js (renamed from ui/app/components/app-header/index.js)0
-rw-r--r--ui/app/components/app/app-header/index.scss (renamed from ui/app/components/app-header/index.scss)0
-rw-r--r--ui/app/components/app/bn-as-decimal-input.js (renamed from ui/app/components/bn-as-decimal-input.js)0
-rw-r--r--ui/app/components/app/coinbase-form.js69
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js84
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-detail-row/index.js (renamed from ui/app/components/confirm-page-container/confirm-detail-row/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss (renamed from ui/app/components/confirm-page-container/confirm-detail-row/index.scss)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js (renamed from ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js110
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js71
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-content/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss68
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js63
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-header/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss (renamed from ui/app/components/confirm-page-container/confirm-page-container-header/index.scss)0
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js)0
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js (renamed from ui/app/components/confirm-page-container/confirm-page-container-navigation/index.js)0
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss (renamed from ui/app/components/confirm-page-container/confirm-page-container-navigation/index.scss)0
-rw-r--r--ui/app/components/app/confirm-page-container/confirm-page-container.component.js170
-rw-r--r--ui/app/components/app/confirm-page-container/index.js (renamed from ui/app/components/confirm-page-container/index.js)0
-rw-r--r--ui/app/components/app/confirm-page-container/index.scss7
-rw-r--r--ui/app/components/app/copyable.js53
-rw-r--r--ui/app/components/app/customize-gas-modal/gas-modal-card.js (renamed from ui/app/components/customize-gas-modal/gas-modal-card.js)0
-rw-r--r--ui/app/components/app/customize-gas-modal/gas-slider.js (renamed from ui/app/components/customize-gas-modal/gas-slider.js)0
-rw-r--r--ui/app/components/app/customize-gas-modal/index.js396
-rw-r--r--ui/app/components/app/dropdowns/account-details-dropdown.js131
-rw-r--r--ui/app/components/app/dropdowns/components/account-dropdowns.js473
-rw-r--r--ui/app/components/app/dropdowns/components/dropdown.js (renamed from ui/app/components/dropdowns/components/dropdown.js)0
-rw-r--r--ui/app/components/app/dropdowns/components/menu.js (renamed from ui/app/components/dropdowns/components/menu.js)0
-rw-r--r--ui/app/components/app/dropdowns/components/network-dropdown-icon.js (renamed from ui/app/components/dropdowns/components/network-dropdown-icon.js)0
-rw-r--r--ui/app/components/app/dropdowns/index.js (renamed from ui/app/components/dropdowns/index.js)0
-rw-r--r--ui/app/components/app/dropdowns/network-dropdown.js378
-rw-r--r--ui/app/components/app/dropdowns/simple-dropdown.js (renamed from ui/app/components/dropdowns/simple-dropdown.js)0
-rw-r--r--ui/app/components/app/dropdowns/tests/dropdown.test.js (renamed from ui/app/components/dropdowns/tests/dropdown.test.js)0
-rw-r--r--ui/app/components/app/dropdowns/tests/menu.test.js (renamed from ui/app/components/dropdowns/tests/menu.test.js)0
-rw-r--r--ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js (renamed from ui/app/components/dropdowns/tests/network-dropdown-icon.test.js)0
-rw-r--r--ui/app/components/app/dropdowns/tests/network-dropdown.test.js97
-rw-r--r--ui/app/components/app/dropdowns/token-menu-dropdown.js68
-rw-r--r--ui/app/components/app/ens-input.js181
-rw-r--r--ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js (renamed from ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js)0
-rw-r--r--ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js38
-rw-r--r--ui/app/components/app/gas-customization/advanced-gas-inputs/index.js (renamed from ui/app/components/gas-customization/advanced-gas-inputs/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss (renamed from ui/app/components/gas-customization/advanced-gas-inputs/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js226
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss207
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js365
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss (renamed from ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js30
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js35
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss (renamed from ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js82
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js186
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js295
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/index.js (renamed from ui/app/components/gas-customization/gas-modal-page-container/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/index.scss (renamed from ui/app/components/gas-customization/gas-modal-page-container/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js274
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js431
-rw-r--r--ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js89
-rw-r--r--ui/app/components/app/gas-customization/gas-price-button-group/index.js (renamed from ui/app/components/gas-customization/gas-price-button-group/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-button-group/index.scss (renamed from ui/app/components/gas-customization/gas-price-button-group/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js233
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js (renamed from ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js (renamed from ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/index.js (renamed from ui/app/components/gas-customization/gas-price-chart/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/index.scss (renamed from ui/app/components/gas-customization/gas-price-chart/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js218
-rw-r--r--ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js (renamed from ui/app/components/gas-customization/gas-slider/gas-slider.component.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-slider/index.js (renamed from ui/app/components/gas-customization/gas-slider/index.js)0
-rw-r--r--ui/app/components/app/gas-customization/gas-slider/index.scss (renamed from ui/app/components/gas-customization/gas-slider/index.scss)0
-rw-r--r--ui/app/components/app/gas-customization/gas.selectors.js (renamed from ui/app/components/gas-customization/gas.selectors.js)0
-rw-r--r--ui/app/components/app/gas-customization/index.scss (renamed from ui/app/components/gas-customization/index.scss)0
-rw-r--r--ui/app/components/app/index.scss81
-rw-r--r--ui/app/components/app/info-box/index.js (renamed from ui/app/components/info-box/index.js)0
-rw-r--r--ui/app/components/app/info-box/index.scss (renamed from ui/app/components/info-box/index.scss)0
-rw-r--r--ui/app/components/app/info-box/info-box.component.js (renamed from ui/app/components/info-box/info-box.component.js)0
-rw-r--r--ui/app/components/app/input-number.js81
-rw-r--r--ui/app/components/app/loading-network-screen/index.js (renamed from ui/app/components/loading-network-screen/index.js)0
-rw-r--r--ui/app/components/app/loading-network-screen/loading-network-screen.component.js138
-rw-r--r--ui/app/components/app/loading-network-screen/loading-network-screen.container.js41
-rw-r--r--ui/app/components/app/menu-bar/index.js (renamed from ui/app/components/menu-bar/index.js)0
-rw-r--r--ui/app/components/app/menu-bar/index.scss (renamed from ui/app/components/menu-bar/index.scss)0
-rw-r--r--ui/app/components/app/menu-bar/menu-bar.component.js79
-rw-r--r--ui/app/components/app/menu-bar/menu-bar.container.js26
-rw-r--r--ui/app/components/app/menu-droppo.js (renamed from ui/app/components/menu-droppo.js)0
-rw-r--r--ui/app/components/app/modal/index.js (renamed from ui/app/components/modal/index.js)0
-rw-r--r--ui/app/components/app/modal/index.scss62
-rw-r--r--ui/app/components/app/modal/modal-content/index.js (renamed from ui/app/components/modal/modal-content/index.js)0
-rw-r--r--ui/app/components/app/modal/modal-content/index.scss (renamed from ui/app/components/modal/modal-content/index.scss)0
-rw-r--r--ui/app/components/app/modal/modal-content/modal-content.component.js (renamed from ui/app/components/modal/modal-content/modal-content.component.js)0
-rw-r--r--ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js (renamed from ui/app/components/modal/modal-content/tests/modal-content.component.test.js)0
-rw-r--r--ui/app/components/app/modal/modal.component.js83
-rw-r--r--ui/app/components/app/modal/tests/modal.component.test.js133
-rw-r--r--ui/app/components/app/modals/account-details-modal.js101
-rw-r--r--ui/app/components/app/modals/account-modal-container.js80
-rw-r--r--ui/app/components/app/modals/buy-options-modal.js101
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js29
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js (renamed from ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js)0
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss (renamed from ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss)0
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js (renamed from ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js)0
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js76
-rw-r--r--ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js60
-rw-r--r--ui/app/components/app/modals/cancel-transaction/index.js (renamed from ui/app/components/modals/cancel-transaction/index.js)0
-rw-r--r--ui/app/components/app/modals/cancel-transaction/index.scss18
-rw-r--r--ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js (renamed from ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js)0
-rw-r--r--ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js (renamed from ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js)0
-rw-r--r--ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js16
-rw-r--r--ui/app/components/app/modals/clear-approved-origins/index.js (renamed from ui/app/components/modals/clear-approved-origins/index.js)0
-rw-r--r--ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js89
-rw-r--r--ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js22
-rw-r--r--ui/app/components/app/modals/confirm-remove-account/index.js (renamed from ui/app/components/modals/confirm-remove-account/index.js)0
-rw-r--r--ui/app/components/app/modals/confirm-remove-account/index.scss (renamed from ui/app/components/modals/confirm-remove-account/index.scss)0
-rw-r--r--ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js (renamed from ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js)0
-rw-r--r--ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js16
-rw-r--r--ui/app/components/app/modals/confirm-reset-account/index.js (renamed from ui/app/components/modals/confirm-reset-account/index.js)0
-rw-r--r--ui/app/components/app/modals/customize-gas/customize-gas.component.js162
-rw-r--r--ui/app/components/app/modals/customize-gas/customize-gas.container.js22
-rw-r--r--ui/app/components/app/modals/customize-gas/customize-gas.util.js34
-rw-r--r--ui/app/components/app/modals/customize-gas/index.js (renamed from ui/app/components/modals/customize-gas/index.js)0
-rw-r--r--ui/app/components/app/modals/customize-gas/index.scss (renamed from ui/app/components/modals/customize-gas/index.scss)0
-rw-r--r--ui/app/components/app/modals/deposit-ether-modal.js220
-rw-r--r--ui/app/components/app/modals/edit-account-name-modal.js83
-rw-r--r--ui/app/components/app/modals/export-private-key-modal.js177
-rw-r--r--ui/app/components/app/modals/hide-token-confirmation-modal.js83
-rw-r--r--ui/app/components/app/modals/index.js (renamed from ui/app/components/modals/index.js)0
-rw-r--r--ui/app/components/app/modals/index.scss11
-rw-r--r--ui/app/components/app/modals/loading-network-error/index.js (renamed from ui/app/components/modals/loading-network-error/index.js)0
-rw-r--r--ui/app/components/app/modals/loading-network-error/loading-network-error.component.js (renamed from ui/app/components/modals/loading-network-error/loading-network-error.component.js)0
-rw-r--r--ui/app/components/app/modals/loading-network-error/loading-network-error.container.js4
-rw-r--r--ui/app/components/app/modals/metametrics-opt-in-modal/index.js (renamed from ui/app/components/modals/metametrics-opt-in-modal/index.js)0
-rw-r--r--ui/app/components/app/modals/metametrics-opt-in-modal/index.scss (renamed from ui/app/components/modals/metametrics-opt-in-modal/index.scss)0
-rw-r--r--ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js141
-rw-r--r--ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js24
-rw-r--r--ui/app/components/app/modals/modal.js511
-rw-r--r--ui/app/components/app/modals/new-account-modal.js112
-rw-r--r--ui/app/components/app/modals/notification-modal.js81
-rw-r--r--ui/app/components/app/modals/qr-scanner/index.js (renamed from ui/app/components/modals/qr-scanner/index.js)0
-rw-r--r--ui/app/components/app/modals/qr-scanner/index.scss (renamed from ui/app/components/modals/qr-scanner/index.scss)0
-rw-r--r--ui/app/components/app/modals/qr-scanner/qr-scanner.component.js216
-rw-r--r--ui/app/components/app/modals/qr-scanner/qr-scanner.container.js24
-rw-r--r--ui/app/components/app/modals/reject-transactions/index.js (renamed from ui/app/components/modals/reject-transactions/index.js)0
-rw-r--r--ui/app/components/app/modals/reject-transactions/index.scss (renamed from ui/app/components/modals/reject-transactions/index.scss)0
-rw-r--r--ui/app/components/app/modals/reject-transactions/reject-transactions.component.js (renamed from ui/app/components/modals/reject-transactions/reject-transactions.component.js)0
-rw-r--r--ui/app/components/app/modals/reject-transactions/reject-transactions.container.js17
-rw-r--r--ui/app/components/app/modals/shapeshift-deposit-tx-modal.js40
-rw-r--r--ui/app/components/app/modals/transaction-confirmed/index.js (renamed from ui/app/components/modals/transaction-confirmed/index.js)0
-rw-r--r--ui/app/components/app/modals/transaction-confirmed/index.scss (renamed from ui/app/components/modals/transaction-confirmed/index.scss)0
-rw-r--r--ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js (renamed from ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js)0
-rw-r--r--ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js4
-rw-r--r--ui/app/components/app/network-display/index.js (renamed from ui/app/components/network-display/index.js)0
-rw-r--r--ui/app/components/app/network-display/index.scss (renamed from ui/app/components/network-display/index.scss)0
-rw-r--r--ui/app/components/app/network-display/network-display.component.js76
-rw-r--r--ui/app/components/app/network-display/network-display.container.js (renamed from ui/app/components/network-display/network-display.container.js)0
-rw-r--r--ui/app/components/app/network.js (renamed from ui/app/components/network.js)0
-rw-r--r--ui/app/components/app/notice.js (renamed from ui/app/components/notice.js)0
-rw-r--r--ui/app/components/app/provider-page-container/index.js (renamed from ui/app/components/provider-page-container/index.js)0
-rw-r--r--ui/app/components/app/provider-page-container/index.scss (renamed from ui/app/components/provider-page-container/index.scss)0
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container-content/index.js (renamed from ui/app/components/provider-page-container/provider-page-container-content/index.js)0
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js77
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js11
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container-header/index.js (renamed from ui/app/components/provider-page-container/provider-page-container-header/index.js)0
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js (renamed from ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js)0
-rw-r--r--ui/app/components/app/provider-page-container/provider-page-container.component.js76
-rw-r--r--ui/app/components/app/selected-account/index.js (renamed from ui/app/components/selected-account/index.js)0
-rw-r--r--ui/app/components/app/selected-account/index.scss (renamed from ui/app/components/selected-account/index.scss)0
-rw-r--r--ui/app/components/app/selected-account/selected-account.component.js55
-rw-r--r--ui/app/components/app/selected-account/selected-account.container.js14
-rw-r--r--ui/app/components/app/selected-account/tests/selected-account-component.test.js (renamed from ui/app/components/selected-account/tests/selected-account-component.test.js)0
-rw-r--r--ui/app/components/app/send/README.md (renamed from ui/app/components/send/README.md)0
-rw-r--r--ui/app/components/app/send/account-list-item/account-list-item-README.md (renamed from ui/app/components/send/account-list-item/account-list-item-README.md)0
-rw-r--r--ui/app/components/app/send/account-list-item/account-list-item.component.js108
-rw-r--r--ui/app/components/app/send/account-list-item/account-list-item.container.js27
-rw-r--r--ui/app/components/app/send/account-list-item/index.js (renamed from ui/app/components/send/account-list-item/index.js)0
-rw-r--r--ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js148
-rw-r--r--ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js73
-rw-r--r--ui/app/components/app/send/index.js (renamed from ui/app/components/send/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/index.js (renamed from ui/app/components/send/send-content/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/README.md (renamed from ui/app/components/send/send-content/send-amount-row/README.md)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js40
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js29
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js91
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js (renamed from ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/index.js (renamed from ui/app/components/send/send-content/send-amount-row/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js119
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js54
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss (renamed from ui/app/components/send/send-content/send-amount-row/send-amount-row.scss)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js (renamed from ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js (renamed from ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js125
-rw-r--r--ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js (renamed from ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-content.component.js41
-rw-r--r--ui/app/components/app/send/send-content/send-dropdown-list/index.js (renamed from ui/app/components/send/send-content/send-dropdown-list/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js52
-rw-r--r--ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js (renamed from ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/index.js (renamed from ui/app/components/send/send-content/send-from-row/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js27
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js (renamed from ui/app/components/send/send-content/send-from-row/send-from-row.container.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js (renamed from ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js (renamed from ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js (renamed from ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js (renamed from ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/README.md (renamed from ui/app/components/send/send-content/send-gas-row/README.md)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js57
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js (renamed from ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js (renamed from ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/index.js (renamed from ui/app/components/send/send-content/send-gas-row/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js131
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js118
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss (renamed from ui/app/components/send/send-content/send-gas-row/send-gas-row.scss)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js (renamed from ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js (renamed from ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js200
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js (renamed from ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-hex-data-row/index.js (renamed from ui/app/components/send/send-content/send-hex-data-row/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js (renamed from ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js21
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/index.js (renamed from ui/app/components/send/send-content/send-row-wrapper/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js48
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss (renamed from ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss)0
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js (renamed from ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/index.js (renamed from ui/app/components/send/send-content/send-to-row/index.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md (renamed from ui/app/components/send/send-content/send-to-row/send-to-row-README.md)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js91
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js54
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js (renamed from ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js36
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js (renamed from ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js134
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js (renamed from ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js107
-rw-r--r--ui/app/components/app/send/send-content/tests/send-content-component.test.js50
-rw-r--r--ui/app/components/app/send/send-footer/README.md (renamed from ui/app/components/send/send-footer/README.md)0
-rw-r--r--ui/app/components/app/send/send-footer/index.js (renamed from ui/app/components/send/send-footer/index.js)0
-rw-r--r--ui/app/components/app/send/send-footer/send-footer.component.js137
-rw-r--r--ui/app/components/app/send/send-footer/send-footer.container.js107
-rw-r--r--ui/app/components/app/send/send-footer/send-footer.scss (renamed from ui/app/components/send/send-footer/send-footer.scss)0
-rw-r--r--ui/app/components/app/send/send-footer/send-footer.selectors.js (renamed from ui/app/components/send/send-footer/send-footer.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-footer/send-footer.utils.js (renamed from ui/app/components/send/send-footer/send-footer.utils.js)0
-rw-r--r--ui/app/components/app/send/send-footer/tests/send-footer-component.test.js233
-rw-r--r--ui/app/components/app/send/send-footer/tests/send-footer-container.test.js200
-rw-r--r--ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js (renamed from ui/app/components/send/send-footer/tests/send-footer-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js (renamed from ui/app/components/send/send-footer/tests/send-footer-utils.test.js)0
-rw-r--r--ui/app/components/app/send/send-header/README.md (renamed from ui/app/components/send/send-header/README.md)0
-rw-r--r--ui/app/components/app/send/send-header/index.js (renamed from ui/app/components/send/send-header/index.js)0
-rw-r--r--ui/app/components/app/send/send-header/send-header.component.js34
-rw-r--r--ui/app/components/app/send/send-header/send-header.container.js19
-rw-r--r--ui/app/components/app/send/send-header/send-header.selectors.js (renamed from ui/app/components/send/send-header/send-header.selectors.js)0
-rw-r--r--ui/app/components/app/send/send-header/tests/send-header-component.test.js70
-rw-r--r--ui/app/components/app/send/send-header/tests/send-header-container.test.js59
-rw-r--r--ui/app/components/app/send/send-header/tests/send-header-selectors.test.js (renamed from ui/app/components/send/send-header/tests/send-header-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/send.component.js218
-rw-r--r--ui/app/components/app/send/send.constants.js61
-rw-r--r--ui/app/components/app/send/send.container.js112
-rw-r--r--ui/app/components/app/send/send.scss (renamed from ui/app/components/send/send.scss)0
-rw-r--r--ui/app/components/app/send/send.selectors.js291
-rw-r--r--ui/app/components/app/send/send.utils.js332
-rw-r--r--ui/app/components/app/send/tests/send-component.test.js354
-rw-r--r--ui/app/components/app/send/tests/send-container.test.js174
-rw-r--r--ui/app/components/app/send/tests/send-selectors-test-data.js (renamed from ui/app/components/send/tests/send-selectors-test-data.js)0
-rw-r--r--ui/app/components/app/send/tests/send-selectors.test.js (renamed from ui/app/components/send/tests/send-selectors.test.js)0
-rw-r--r--ui/app/components/app/send/tests/send-utils.test.js527
-rw-r--r--ui/app/components/app/send/to-autocomplete.component.js141
-rw-r--r--ui/app/components/app/send/to-autocomplete/index.js (renamed from ui/app/components/send/to-autocomplete/index.js)0
-rw-r--r--ui/app/components/app/send/to-autocomplete/to-autocomplete.js129
-rw-r--r--ui/app/components/app/shapeshift-form.js256
-rw-r--r--ui/app/components/app/shift-list-item.js204
-rw-r--r--ui/app/components/app/sidebars/index.js (renamed from ui/app/components/sidebars/index.js)0
-rw-r--r--ui/app/components/app/sidebars/index.scss81
-rw-r--r--ui/app/components/app/sidebars/sidebar-content.scss (renamed from ui/app/components/sidebars/sidebar-content.scss)0
-rw-r--r--ui/app/components/app/sidebars/sidebar.component.js (renamed from ui/app/components/sidebars/sidebar.component.js)0
-rw-r--r--ui/app/components/app/sidebars/sidebar.constants.js (renamed from ui/app/components/sidebars/sidebar.constants.js)0
-rw-r--r--ui/app/components/app/sidebars/tests/sidebars-component.test.js (renamed from ui/app/components/sidebars/tests/sidebars-component.test.js)0
-rw-r--r--ui/app/components/app/signature-request.js349
-rw-r--r--ui/app/components/app/tab-bar.js37
-rw-r--r--ui/app/components/app/token-cell.js177
-rw-r--r--ui/app/components/app/token-list.js188
-rw-r--r--ui/app/components/app/transaction-action/index.js (renamed from ui/app/components/transaction-action/index.js)0
-rw-r--r--ui/app/components/app/transaction-action/tests/transaction-action.component.test.js (renamed from ui/app/components/transaction-action/tests/transaction-action.component.test.js)0
-rw-r--r--ui/app/components/app/transaction-action/transaction-action.component.js58
-rw-r--r--ui/app/components/app/transaction-activity-log/index.js (renamed from ui/app/components/transaction-activity-log/index.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/index.scss (renamed from ui/app/components/transaction-activity-log/index.scss)0
-rw-r--r--ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js (renamed from ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js (renamed from ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js (renamed from ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js (renamed from ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js (renamed from ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js131
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js (renamed from ui/app/components/transaction-activity-log/transaction-activity-log.constants.js)0
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js44
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js233
-rw-r--r--ui/app/components/app/transaction-breakdown/index.js (renamed from ui/app/components/transaction-breakdown/index.js)0
-rw-r--r--ui/app/components/app/transaction-breakdown/index.scss24
-rw-r--r--ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js (renamed from ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js)0
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js (renamed from ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js)0
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss (renamed from ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss)0
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js39
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js (renamed from ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js)0
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js106
-rw-r--r--ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js29
-rw-r--r--ui/app/components/app/transaction-list-item-details/index.js (renamed from ui/app/components/transaction-list-item-details/index.js)0
-rw-r--r--ui/app/components/app/transaction-list-item-details/index.scss (renamed from ui/app/components/transaction-list-item-details/index.scss)0
-rw-r--r--ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js81
-rw-r--r--ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js215
-rw-r--r--ui/app/components/app/transaction-list-item/index.js (renamed from ui/app/components/transaction-list-item/index.js)0
-rw-r--r--ui/app/components/app/transaction-list-item/index.scss (renamed from ui/app/components/transaction-list-item/index.scss)0
-rw-r--r--ui/app/components/app/transaction-list-item/transaction-list-item.component.js227
-rw-r--r--ui/app/components/app/transaction-list-item/transaction-list-item.container.js98
-rw-r--r--ui/app/components/app/transaction-list/index.js (renamed from ui/app/components/transaction-list/index.js)0
-rw-r--r--ui/app/components/app/transaction-list/index.scss (renamed from ui/app/components/transaction-list/index.scss)0
-rw-r--r--ui/app/components/app/transaction-list/transaction-list.component.js126
-rw-r--r--ui/app/components/app/transaction-list/transaction-list.container.js44
-rw-r--r--ui/app/components/app/transaction-status/index.js (renamed from ui/app/components/transaction-status/index.js)0
-rw-r--r--ui/app/components/app/transaction-status/index.scss (renamed from ui/app/components/transaction-status/index.scss)0
-rw-r--r--ui/app/components/app/transaction-status/tests/transaction-status.component.test.js33
-rw-r--r--ui/app/components/app/transaction-status/transaction-status.component.js63
-rw-r--r--ui/app/components/app/transaction-view-balance/index.js (renamed from ui/app/components/transaction-view-balance/index.js)0
-rw-r--r--ui/app/components/app/transaction-view-balance/index.scss (renamed from ui/app/components/transaction-view-balance/index.scss)0
-rw-r--r--ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js72
-rw-r--r--ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js145
-rw-r--r--ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js46
-rw-r--r--ui/app/components/app/transaction-view/index.js (renamed from ui/app/components/transaction-view/index.js)0
-rw-r--r--ui/app/components/app/transaction-view/index.scss (renamed from ui/app/components/transaction-view/index.scss)0
-rw-r--r--ui/app/components/app/transaction-view/transaction-view.component.js (renamed from ui/app/components/transaction-view/transaction-view.component.js)0
-rw-r--r--ui/app/components/app/ui-migration-annoucement/index.js (renamed from ui/app/components/ui-migration-annoucement/index.js)0
-rw-r--r--ui/app/components/app/ui-migration-annoucement/index.scss (renamed from ui/app/components/ui-migration-annoucement/index.scss)0
-rw-r--r--ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js (renamed from ui/app/components/ui-migration-annoucement/ui-migration-annoucement.component.js)0
-rw-r--r--ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js21
-rw-r--r--ui/app/components/app/user-preferenced-currency-display/index.js (renamed from ui/app/components/user-preferenced-currency-display/index.js)0
-rw-r--r--ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js34
-rw-r--r--ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js (renamed from ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js)0
-rw-r--r--ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js47
-rw-r--r--ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js67
-rw-r--r--ui/app/components/app/user-preferenced-currency-input/index.js (renamed from ui/app/components/user-preferenced-currency-input/index.js)0
-rw-r--r--ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js32
-rw-r--r--ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js (renamed from ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js)0
-rw-r--r--ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js20
-rw-r--r--ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js13
-rw-r--r--ui/app/components/app/user-preferenced-token-input/index.js (renamed from ui/app/components/user-preferenced-token-input/index.js)0
-rw-r--r--ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js32
-rw-r--r--ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js (renamed from ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js)0
-rw-r--r--ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js20
-rw-r--r--ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js13
-rw-r--r--ui/app/components/app/wallet-view.js246
-rw-r--r--ui/app/components/balance/balance.component.js92
-rw-r--r--ui/app/components/balance/balance.container.js32
-rw-r--r--ui/app/components/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/button/button.stories.js58
-rw-r--r--ui/app/components/coinbase-form.js69
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js84
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js110
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js71
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.scss68
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js63
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js170
-rw-r--r--ui/app/components/confirm-page-container/index.scss7
-rw-r--r--ui/app/components/copyable.js53
-rw-r--r--ui/app/components/currency-display/currency-display.component.js46
-rw-r--r--ui/app/components/currency-display/currency-display.container.js51
-rw-r--r--ui/app/components/currency-input/currency-input.component.js160
-rw-r--r--ui/app/components/currency-input/currency-input.container.js31
-rw-r--r--ui/app/components/customize-gas-modal/index.js396
-rw-r--r--ui/app/components/dropdowns/account-details-dropdown.js131
-rw-r--r--ui/app/components/dropdowns/components/account-dropdowns.js473
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js378
-rw-r--r--ui/app/components/dropdowns/tests/network-dropdown.test.js97
-rw-r--r--ui/app/components/dropdowns/token-menu-dropdown.js68
-rw-r--r--ui/app/components/ens-input.js181
-rw-r--r--ui/app/components/eth-balance.js102
-rw-r--r--ui/app/components/export-text-container/export-text-container.component.js45
-rw-r--r--ui/app/components/fiat-value.js66
-rw-r--r--ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js38
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js219
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss203
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js364
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js30
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js35
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js82
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js186
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js291
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js274
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js425
-rw-r--r--ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js89
-rw-r--r--ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js233
-rw-r--r--ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js218
-rw-r--r--ui/app/components/hex-to-decimal/hex-to-decimal.component.js21
-rw-r--r--ui/app/components/identicon/identicon.component.js99
-rw-r--r--ui/app/components/index.scss81
-rw-r--r--ui/app/components/input-number.js81
-rw-r--r--ui/app/components/jazzicon/jazzicon.component.js69
-rw-r--r--ui/app/components/loading-network-screen/loading-network-screen.component.js138
-rw-r--r--ui/app/components/loading-network-screen/loading-network-screen.container.js41
-rw-r--r--ui/app/components/menu-bar/menu-bar.component.js79
-rw-r--r--ui/app/components/menu-bar/menu-bar.container.js26
-rw-r--r--ui/app/components/modal/index.scss62
-rw-r--r--ui/app/components/modal/modal.component.js83
-rw-r--r--ui/app/components/modal/tests/modal.component.test.js133
-rw-r--r--ui/app/components/modals/account-details-modal.js101
-rw-r--r--ui/app/components/modals/account-modal-container.js80
-rw-r--r--ui/app/components/modals/buy-options-modal.js101
-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.component.js76
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction.container.js60
-rw-r--r--ui/app/components/modals/cancel-transaction/index.scss18
-rw-r--r--ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js16
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js89
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js22
-rw-r--r--ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js16
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js162
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.container.js22
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.util.js34
-rw-r--r--ui/app/components/modals/deposit-ether-modal.js220
-rw-r--r--ui/app/components/modals/edit-account-name-modal.js83
-rw-r--r--ui/app/components/modals/export-private-key-modal.js177
-rw-r--r--ui/app/components/modals/hide-token-confirmation-modal.js83
-rw-r--r--ui/app/components/modals/index.scss11
-rw-r--r--ui/app/components/modals/loading-network-error/loading-network-error.container.js4
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js141
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js24
-rw-r--r--ui/app/components/modals/modal.js511
-rw-r--r--ui/app/components/modals/new-account-modal.js112
-rw-r--r--ui/app/components/modals/notification-modal.js81
-rw-r--r--ui/app/components/modals/qr-scanner/qr-scanner.component.js216
-rw-r--r--ui/app/components/modals/qr-scanner/qr-scanner.container.js24
-rw-r--r--ui/app/components/modals/reject-transactions/reject-transactions.container.js17
-rw-r--r--ui/app/components/modals/shapeshift-deposit-tx-modal.js40
-rw-r--r--ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js4
-rw-r--r--ui/app/components/network-display/network-display.component.js76
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js335
-rw-r--r--ui/app/components/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/components/pages/add-token/index.scss45
-rw-r--r--ui/app/components/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/components/pages/add-token/token-search/token-search.component.js85
-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-token/confirm-add-token.component.js117
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.container.js15
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js39
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js45
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.component.js29
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.container.js52
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js119
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js34
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js574
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js242
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js92
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.component.js160
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.container.js37
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js205
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js197
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js293
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js170
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js128
-rw-r--r--ui/app/components/pages/create-account/index.js113
-rw-r--r--ui/app/components/pages/create-account/new-account.js130
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/create-password.component.js71
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js256
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js225
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js55
-rw-r--r--ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js93
-rw-r--r--ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js25
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js57
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.component.js152
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.container.js31
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.selectors.js26
-rw-r--r--ui/app/components/pages/first-time-flow/index.scss159
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js169
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js27
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js155
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/index.scss40
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js143
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js70
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/select-action.component.js112
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/select-action.container.js23
-rw-r--r--ui/app/components/pages/first-time-flow/welcome/welcome.component.js69
-rw-r--r--ui/app/components/pages/first-time-flow/welcome/welcome.container.js26
-rw-r--r--ui/app/components/pages/home/home.component.js77
-rw-r--r--ui/app/components/pages/home/home.container.js32
-rw-r--r--ui/app/components/pages/index.scss11
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js197
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js177
-rw-r--r--ui/app/components/pages/lock/lock.component.js26
-rw-r--r--ui/app/components/pages/lock/lock.container.js24
-rw-r--r--ui/app/components/pages/mobile-sync/index.js387
-rw-r--r--ui/app/components/pages/notice.js203
-rw-r--r--ui/app/components/pages/provider-approval/provider-approval.component.js29
-rw-r--r--ui/app/components/pages/provider-approval/provider-approval.container.js12
-rw-r--r--ui/app/components/pages/settings/index.scss80
-rw-r--r--ui/app/components/pages/settings/info-tab/info-tab.component.js136
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.component.js679
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.container.js83
-rw-r--r--ui/app/components/pages/settings/settings.component.js54
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js191
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.container.js64
-rw-r--r--ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js77
-rw-r--r--ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js11
-rw-r--r--ui/app/components/provider-page-container/provider-page-container.component.js76
-rw-r--r--ui/app/components/qr-code.js63
-rw-r--r--ui/app/components/selected-account/selected-account.component.js55
-rw-r--r--ui/app/components/selected-account/selected-account.container.js14
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.component.js108
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.container.js27
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-component.test.js148
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-container.test.js73
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js40
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js29
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js91
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js119
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js54
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js125
-rw-r--r--ui/app/components/send/send-content/send-content.component.js41
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js52
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.component.js27
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js57
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js131
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js118
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js200
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js21
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js48
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js91
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.container.js54
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.utils.js36
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js134
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js107
-rw-r--r--ui/app/components/send/send-content/tests/send-content-component.test.js50
-rw-r--r--ui/app/components/send/send-footer/send-footer.component.js137
-rw-r--r--ui/app/components/send/send-footer/send-footer.container.js107
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-component.test.js233
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js200
-rw-r--r--ui/app/components/send/send-header/send-header.component.js34
-rw-r--r--ui/app/components/send/send-header/send-header.container.js19
-rw-r--r--ui/app/components/send/send-header/tests/send-header-component.test.js70
-rw-r--r--ui/app/components/send/send-header/tests/send-header-container.test.js59
-rw-r--r--ui/app/components/send/send.component.js218
-rw-r--r--ui/app/components/send/send.constants.js61
-rw-r--r--ui/app/components/send/send.container.js112
-rw-r--r--ui/app/components/send/send.selectors.js291
-rw-r--r--ui/app/components/send/send.utils.js332
-rw-r--r--ui/app/components/send/tests/send-component.test.js354
-rw-r--r--ui/app/components/send/tests/send-container.test.js174
-rw-r--r--ui/app/components/send/tests/send-utils.test.js527
-rw-r--r--ui/app/components/send/to-autocomplete.component.js141
-rw-r--r--ui/app/components/send/to-autocomplete/to-autocomplete.js129
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js186
-rw-r--r--ui/app/components/shapeshift-form.js256
-rw-r--r--ui/app/components/shift-list-item.js204
-rw-r--r--ui/app/components/sidebars/index.scss81
-rw-r--r--ui/app/components/signature-request.js316
-rw-r--r--ui/app/components/tab-bar.js33
-rw-r--r--ui/app/components/tabs/index.scss11
-rw-r--r--ui/app/components/text-field/text-field.stories.js53
-rw-r--r--ui/app/components/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/token-cell.js177
-rw-r--r--ui/app/components/token-currency-display/token-currency-display.component.js57
-rw-r--r--ui/app/components/token-input/token-input.component.js145
-rw-r--r--ui/app/components/token-input/token-input.container.js30
-rw-r--r--ui/app/components/token-list.js188
-rw-r--r--ui/app/components/transaction-action/transaction-action.component.js58
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.component.js131
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.container.js44
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.util.js224
-rw-r--r--ui/app/components/transaction-breakdown/index.scss24
-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.component.js106
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown.container.js29
-rw-r--r--ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js81
-rw-r--r--ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js181
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js224
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js82
-rw-r--r--ui/app/components/transaction-list/transaction-list.component.js126
-rw-r--r--ui/app/components/transaction-list/transaction-list.container.js44
-rw-r--r--ui/app/components/transaction-status/tests/transaction-status.component.test.js33
-rw-r--r--ui/app/components/transaction-status/transaction-status.component.js63
-rw-r--r--ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js72
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.component.js145
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.container.js46
-rw-r--r--ui/app/components/ui-migration-annoucement/ui-migration-announcement.container.js21
-rw-r--r--ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js84
-rw-r--r--ui/app/components/ui/account-dropdown-mini/index.js (renamed from ui/app/components/account-dropdown-mini/index.js)0
-rw-r--r--ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js107
-rw-r--r--ui/app/components/ui/alert/index.js (renamed from ui/app/components/alert/index.js)0
-rw-r--r--ui/app/components/ui/balance/balance.component.js92
-rw-r--r--ui/app/components/ui/balance/balance.container.js32
-rw-r--r--ui/app/components/ui/balance/index.js (renamed from ui/app/components/balance/index.js)0
-rw-r--r--ui/app/components/ui/breadcrumbs/breadcrumbs.component.js (renamed from ui/app/components/breadcrumbs/breadcrumbs.component.js)0
-rw-r--r--ui/app/components/ui/breadcrumbs/index.js (renamed from ui/app/components/breadcrumbs/index.js)0
-rw-r--r--ui/app/components/ui/breadcrumbs/index.scss (renamed from ui/app/components/breadcrumbs/index.scss)0
-rw-r--r--ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js (renamed from ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js)0
-rw-r--r--ui/app/components/ui/button-group/button-group.component.js (renamed from ui/app/components/button-group/button-group.component.js)0
-rw-r--r--ui/app/components/ui/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/ui/button-group/index.js (renamed from ui/app/components/button-group/index.js)0
-rw-r--r--ui/app/components/ui/button-group/index.scss (renamed from ui/app/components/button-group/index.scss)0
-rw-r--r--ui/app/components/ui/button-group/tests/button-group-component.test.js (renamed from ui/app/components/button-group/tests/button-group-component.test.js)0
-rw-r--r--ui/app/components/ui/button/button.component.js (renamed from ui/app/components/button/button.component.js)0
-rw-r--r--ui/app/components/ui/button/button.stories.js58
-rw-r--r--ui/app/components/ui/button/index.js (renamed from ui/app/components/button/index.js)0
-rw-r--r--ui/app/components/ui/card/card.component.js (renamed from ui/app/components/card/card.component.js)0
-rw-r--r--ui/app/components/ui/card/index.js (renamed from ui/app/components/card/index.js)0
-rw-r--r--ui/app/components/ui/card/index.scss (renamed from ui/app/components/card/index.scss)0
-rw-r--r--ui/app/components/ui/card/tests/card.component.test.js (renamed from ui/app/components/card/tests/card.component.test.js)0
-rw-r--r--ui/app/components/ui/copyButton.js (renamed from ui/app/components/copyButton.js)0
-rw-r--r--ui/app/components/ui/currency-display/currency-display.component.js46
-rw-r--r--ui/app/components/ui/currency-display/currency-display.container.js51
-rw-r--r--ui/app/components/ui/currency-display/index.js (renamed from ui/app/components/currency-display/index.js)0
-rw-r--r--ui/app/components/ui/currency-display/index.scss (renamed from ui/app/components/currency-display/index.scss)0
-rw-r--r--ui/app/components/ui/currency-display/tests/currency-display.component.test.js (renamed from ui/app/components/currency-display/tests/currency-display.component.test.js)0
-rw-r--r--ui/app/components/ui/currency-display/tests/currency-display.container.test.js (renamed from ui/app/components/currency-display/tests/currency-display.container.test.js)0
-rw-r--r--ui/app/components/ui/currency-input/currency-input.component.js160
-rw-r--r--ui/app/components/ui/currency-input/currency-input.container.js31
-rw-r--r--ui/app/components/ui/currency-input/index.js (renamed from ui/app/components/currency-input/index.js)0
-rw-r--r--ui/app/components/ui/currency-input/index.scss (renamed from ui/app/components/currency-input/index.scss)0
-rw-r--r--ui/app/components/ui/currency-input/tests/currency-input.component.test.js (renamed from ui/app/components/currency-input/tests/currency-input.component.test.js)0
-rw-r--r--ui/app/components/ui/currency-input/tests/currency-input.container.test.js (renamed from ui/app/components/currency-input/tests/currency-input.container.test.js)0
-rw-r--r--ui/app/components/ui/editable-label.js (renamed from ui/app/components/editable-label.js)0
-rw-r--r--ui/app/components/ui/error-message/error-message.component.js (renamed from ui/app/components/error-message/error-message.component.js)0
-rw-r--r--ui/app/components/ui/error-message/index.js (renamed from ui/app/components/error-message/index.js)0
-rw-r--r--ui/app/components/ui/error-message/index.scss (renamed from ui/app/components/error-message/index.scss)0
-rw-r--r--ui/app/components/ui/error-message/tests/error-message.component.test.js (renamed from ui/app/components/error-message/tests/error-message.component.test.js)0
-rw-r--r--ui/app/components/ui/eth-balance.js102
-rw-r--r--ui/app/components/ui/export-text-container/export-text-container.component.js45
-rw-r--r--ui/app/components/ui/export-text-container/index.js (renamed from ui/app/components/export-text-container/index.js)0
-rw-r--r--ui/app/components/ui/export-text-container/index.scss (renamed from ui/app/components/export-text-container/index.scss)0
-rw-r--r--ui/app/components/ui/fiat-value.js66
-rw-r--r--ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js21
-rw-r--r--ui/app/components/ui/hex-to-decimal/index.js (renamed from ui/app/components/hex-to-decimal/index.js)0
-rw-r--r--ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js (renamed from ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js)0
-rw-r--r--ui/app/components/ui/identicon/identicon.component.js99
-rw-r--r--ui/app/components/ui/identicon/identicon.container.js (renamed from ui/app/components/identicon/identicon.container.js)0
-rw-r--r--ui/app/components/ui/identicon/index.js (renamed from ui/app/components/identicon/index.js)0
-rw-r--r--ui/app/components/ui/identicon/index.scss (renamed from ui/app/components/identicon/index.scss)0
-rw-r--r--ui/app/components/ui/identicon/tests/identicon.component.test.js (renamed from ui/app/components/identicon/tests/identicon.component.test.js)0
-rw-r--r--ui/app/components/ui/jazzicon/index.js (renamed from ui/app/components/jazzicon/index.js)0
-rw-r--r--ui/app/components/ui/jazzicon/jazzicon.component.js69
-rw-r--r--ui/app/components/ui/loading-screen/index.js (renamed from ui/app/components/loading-screen/index.js)0
-rw-r--r--ui/app/components/ui/loading-screen/loading-screen.component.js (renamed from ui/app/components/loading-screen/loading-screen.component.js)0
-rw-r--r--ui/app/components/ui/lock-icon/index.js (renamed from ui/app/components/lock-icon/index.js)0
-rw-r--r--ui/app/components/ui/lock-icon/lock-icon.component.js (renamed from ui/app/components/lock-icon/lock-icon.component.js)0
-rw-r--r--ui/app/components/ui/mascot.js (renamed from ui/app/components/mascot.js)0
-rw-r--r--ui/app/components/ui/page-container/index.js (renamed from ui/app/components/page-container/index.js)0
-rw-r--r--ui/app/components/ui/page-container/index.scss (renamed from ui/app/components/page-container/index.scss)0
-rw-r--r--ui/app/components/ui/page-container/page-container-content.component.js (renamed from ui/app/components/page-container/page-container-content.component.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-footer/index.js (renamed from ui/app/components/page-container/page-container-footer/index.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js (renamed from ui/app/components/page-container/page-container-footer/page-container-footer.component.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js (renamed from ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-header/index.js (renamed from ui/app/components/page-container/page-container-header/index.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-header/page-container-header.component.js (renamed from ui/app/components/page-container/page-container-header/page-container-header.component.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js (renamed from ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js)0
-rw-r--r--ui/app/components/ui/page-container/page-container.component.js (renamed from ui/app/components/page-container/page-container.component.js)0
-rw-r--r--ui/app/components/ui/page-container/tests/page-container.component.test.js (renamed from ui/app/components/page-container/tests/page-container.component.test.js)0
-rw-r--r--ui/app/components/ui/qr-code.js63
-rw-r--r--ui/app/components/ui/readonly-input.js (renamed from ui/app/components/readonly-input.js)0
-rw-r--r--ui/app/components/ui/sender-to-recipient/index.js (renamed from ui/app/components/sender-to-recipient/index.js)0
-rw-r--r--ui/app/components/ui/sender-to-recipient/index.scss (renamed from ui/app/components/sender-to-recipient/index.scss)0
-rw-r--r--ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js186
-rw-r--r--ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js (renamed from ui/app/components/sender-to-recipient/sender-to-recipient.constants.js)0
-rw-r--r--ui/app/components/ui/spinner/index.js (renamed from ui/app/components/spinner/index.js)0
-rw-r--r--ui/app/components/ui/spinner/spinner.component.js (renamed from ui/app/components/spinner/spinner.component.js)0
-rw-r--r--ui/app/components/ui/tabs/index.js (renamed from ui/app/components/tabs/index.js)0
-rw-r--r--ui/app/components/ui/tabs/index.scss11
-rw-r--r--ui/app/components/ui/tabs/tab/index.js (renamed from ui/app/components/tabs/tab/index.js)0
-rw-r--r--ui/app/components/ui/tabs/tab/index.scss (renamed from ui/app/components/tabs/tab/index.scss)0
-rw-r--r--ui/app/components/ui/tabs/tab/tab.component.js (renamed from ui/app/components/tabs/tab/tab.component.js)0
-rw-r--r--ui/app/components/ui/tabs/tabs.component.js (renamed from ui/app/components/tabs/tabs.component.js)0
-rw-r--r--ui/app/components/ui/text-field/index.js (renamed from ui/app/components/text-field/index.js)0
-rw-r--r--ui/app/components/ui/text-field/text-field.component.js (renamed from ui/app/components/text-field/text-field.component.js)0
-rw-r--r--ui/app/components/ui/text-field/text-field.stories.js53
-rw-r--r--ui/app/components/ui/token-balance/index.js (renamed from ui/app/components/token-balance/index.js)0
-rw-r--r--ui/app/components/ui/token-balance/index.scss (renamed from ui/app/components/token-balance/index.scss)0
-rw-r--r--ui/app/components/ui/token-balance/token-balance.component.js (renamed from ui/app/components/token-balance/token-balance.component.js)0
-rw-r--r--ui/app/components/ui/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/ui/token-currency-display/index.js (renamed from ui/app/components/token-currency-display/index.js)0
-rw-r--r--ui/app/components/ui/token-currency-display/token-currency-display.component.js57
-rw-r--r--ui/app/components/ui/token-input/index.js (renamed from ui/app/components/token-input/index.js)0
-rw-r--r--ui/app/components/ui/token-input/tests/token-input.component.test.js (renamed from ui/app/components/token-input/tests/token-input.component.test.js)0
-rw-r--r--ui/app/components/ui/token-input/tests/token-input.container.test.js (renamed from ui/app/components/token-input/tests/token-input.container.test.js)0
-rw-r--r--ui/app/components/ui/token-input/token-input.component.js145
-rw-r--r--ui/app/components/ui/token-input/token-input.container.js30
-rw-r--r--ui/app/components/ui/tooltip-v2.js (renamed from ui/app/components/tooltip-v2.js)0
-rw-r--r--ui/app/components/ui/tooltip.js (renamed from ui/app/components/tooltip.js)0
-rw-r--r--ui/app/components/ui/unit-input/index.js (renamed from ui/app/components/unit-input/index.js)0
-rw-r--r--ui/app/components/ui/unit-input/index.scss (renamed from ui/app/components/unit-input/index.scss)0
-rw-r--r--ui/app/components/ui/unit-input/tests/unit-input.component.test.js (renamed from ui/app/components/unit-input/tests/unit-input.component.test.js)0
-rw-r--r--ui/app/components/ui/unit-input/unit-input.component.js108
-rw-r--r--ui/app/components/unit-input/unit-input.component.js108
-rw-r--r--ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js34
-rw-r--r--ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js47
-rw-r--r--ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js67
-rw-r--r--ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js32
-rw-r--r--ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js20
-rw-r--r--ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js13
-rw-r--r--ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js32
-rw-r--r--ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js20
-rw-r--r--ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js13
-rw-r--r--ui/app/components/wallet-view.js246
-rw-r--r--ui/app/conf-tx.js225
-rw-r--r--ui/app/constants/common.js10
-rw-r--r--ui/app/css/itcss/components/index.scss2
-rw-r--r--ui/app/css/itcss/components/tab-bar.scss68
-rw-r--r--ui/app/ducks/app/app.js788
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js420
-rw-r--r--ui/app/ducks/confirm-transaction/confirm-transaction.duck.js420
-rw-r--r--ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js685
-rw-r--r--ui/app/ducks/gas.duck.js517
-rw-r--r--ui/app/ducks/gas/gas-duck.test.js606
-rw-r--r--ui/app/ducks/gas/gas.duck.js526
-rw-r--r--ui/app/ducks/index.js95
-rw-r--r--ui/app/ducks/locale/locale.js17
-rw-r--r--ui/app/ducks/metamask/metamask.js419
-rw-r--r--ui/app/ducks/mock-gas-estimate-data.js3
-rw-r--r--ui/app/ducks/send/send-duck.test.js186
-rw-r--r--ui/app/ducks/send/send.duck.js (renamed from ui/app/ducks/send.duck.js)0
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js685
-rw-r--r--ui/app/ducks/tests/gas-duck.test.js600
-rw-r--r--ui/app/ducks/tests/send-duck.test.js186
-rw-r--r--ui/app/helpers/confirm-transaction/util.js131
-rw-r--r--ui/app/helpers/confirm-transaction/util.test.js137
-rw-r--r--ui/app/helpers/constants/common.js13
-rw-r--r--ui/app/helpers/constants/error-keys.js (renamed from ui/app/constants/error-keys.js)0
-rw-r--r--ui/app/helpers/constants/infura-conversion.json (renamed from ui/app/infura-conversion.json)0
-rw-r--r--ui/app/helpers/constants/routes.js93
-rw-r--r--ui/app/helpers/constants/transactions.js (renamed from ui/app/constants/transactions.js)0
-rw-r--r--ui/app/helpers/conversions.util.js122
-rw-r--r--ui/app/helpers/higher-order-components/authenticated/authenticated.component.js22
-rw-r--r--ui/app/helpers/higher-order-components/authenticated/authenticated.container.js (renamed from ui/app/higher-order-components/authenticated/authenticated.container.js)0
-rw-r--r--ui/app/helpers/higher-order-components/authenticated/index.js (renamed from ui/app/higher-order-components/authenticated/index.js)0
-rw-r--r--ui/app/helpers/higher-order-components/i18n-provider.js55
-rw-r--r--ui/app/helpers/higher-order-components/initialized/index.js (renamed from ui/app/higher-order-components/initialized/index.js)0
-rw-r--r--ui/app/helpers/higher-order-components/initialized/initialized.component.js14
-rw-r--r--ui/app/helpers/higher-order-components/initialized/initialized.container.js (renamed from ui/app/higher-order-components/initialized/initialized.container.js)0
-rw-r--r--ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js106
-rw-r--r--ui/app/helpers/higher-order-components/with-method-data/index.js (renamed from ui/app/higher-order-components/with-method-data/index.js)0
-rw-r--r--ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js65
-rw-r--r--ui/app/helpers/higher-order-components/with-modal-props/index.js (renamed from ui/app/higher-order-components/with-modal-props/index.js)0
-rw-r--r--ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js (renamed from ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js)0
-rw-r--r--ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js21
-rw-r--r--ui/app/helpers/higher-order-components/with-token-tracker/index.js (renamed from ui/app/higher-order-components/with-token-tracker/index.js)0
-rw-r--r--ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js (renamed from ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js)0
-rw-r--r--ui/app/helpers/tests/common.util.test.js27
-rw-r--r--ui/app/helpers/tests/transactions.util.test.js57
-rw-r--r--ui/app/helpers/transactions.util.js179
-rw-r--r--ui/app/helpers/utils/common.util.js (renamed from ui/app/helpers/common.util.js)0
-rw-r--r--ui/app/helpers/utils/common.util.test.js27
-rw-r--r--ui/app/helpers/utils/confirm-tx.util.js131
-rw-r--r--ui/app/helpers/utils/confirm-tx.util.test.js137
-rw-r--r--ui/app/helpers/utils/conversion-util.js (renamed from ui/app/conversion-util.js)0
-rw-r--r--ui/app/helpers/utils/conversion-util.test.js (renamed from ui/app/conversion-util.test.js)0
-rw-r--r--ui/app/helpers/utils/conversions.util.js122
-rw-r--r--ui/app/helpers/utils/formatters.js (renamed from ui/app/helpers/formatters.js)0
-rw-r--r--ui/app/helpers/utils/i18n-helper.js (renamed from ui/i18n-helper.js)0
-rw-r--r--ui/app/helpers/utils/metametrics.util.js (renamed from ui/app/metametrics/metametrics.util.js)0
-rw-r--r--ui/app/helpers/utils/token-util.js (renamed from ui/app/token-util.js)0
-rw-r--r--ui/app/helpers/utils/transactions.util.js179
-rw-r--r--ui/app/helpers/utils/transactions.util.test.js57
-rw-r--r--ui/app/helpers/utils/util.js326
-rw-r--r--ui/app/higher-order-components/authenticated/authenticated.component.js22
-rw-r--r--ui/app/higher-order-components/initialized/initialized.component.js14
-rw-r--r--ui/app/higher-order-components/with-method-data/with-method-data.component.js65
-rw-r--r--ui/app/higher-order-components/with-modal-props/with-modal-props.js21
-rw-r--r--ui/app/i18n-provider.js55
-rw-r--r--ui/app/img/identicon-tardigrade.pngbin141119 -> 0 bytes
-rw-r--r--ui/app/img/identicon-walrus.pngbin388973 -> 0 bytes
-rw-r--r--ui/app/keychains/hd/create-vault-complete.js91
-rw-r--r--ui/app/keychains/hd/restore-vault.js181
-rw-r--r--ui/app/metametrics/metametrics.provider.js106
-rw-r--r--ui/app/pages/add-token/add-token.component.js335
-rw-r--r--ui/app/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/pages/add-token/index.js (renamed from ui/app/components/pages/add-token/index.js)0
-rw-r--r--ui/app/pages/add-token/index.scss45
-rw-r--r--ui/app/pages/add-token/token-list/index.js (renamed from ui/app/components/pages/add-token/token-list/index.js)0
-rw-r--r--ui/app/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/index.js (renamed from ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js)0
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/index.scss (renamed from ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss)0
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js (renamed from ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js)0
-rw-r--r--ui/app/pages/add-token/token-list/token-list.component.js (renamed from ui/app/components/pages/add-token/token-list/token-list.component.js)0
-rw-r--r--ui/app/pages/add-token/token-list/token-list.container.js (renamed from ui/app/components/pages/add-token/token-list/token-list.container.js)0
-rw-r--r--ui/app/pages/add-token/token-search/index.js (renamed from ui/app/components/pages/add-token/token-search/index.js)0
-rw-r--r--ui/app/pages/add-token/token-search/token-search.component.js85
-rw-r--r--ui/app/pages/add-token/util.js (renamed from ui/app/components/pages/add-token/util.js)0
-rw-r--r--ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js122
-rw-r--r--ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js29
-rw-r--r--ui/app/pages/confirm-add-suggested-token/index.js (renamed from ui/app/components/pages/confirm-add-suggested-token/index.js)0
-rw-r--r--ui/app/pages/confirm-add-token/confirm-add-token.component.js117
-rw-r--r--ui/app/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/pages/confirm-add-token/index.js (renamed from ui/app/components/pages/confirm-add-token/index.js)0
-rw-r--r--ui/app/pages/confirm-add-token/index.scss (renamed from ui/app/components/pages/confirm-add-token/index.scss)0
-rw-r--r--ui/app/pages/confirm-approve/confirm-approve.component.js (renamed from ui/app/components/pages/confirm-approve/confirm-approve.component.js)0
-rw-r--r--ui/app/pages/confirm-approve/confirm-approve.container.js15
-rw-r--r--ui/app/pages/confirm-approve/index.js (renamed from ui/app/components/pages/confirm-approve/index.js)0
-rw-r--r--ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js (renamed from ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js)0
-rw-r--r--ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js (renamed from ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js)0
-rw-r--r--ui/app/pages/confirm-deploy-contract/index.js (renamed from ui/app/components/pages/confirm-deploy-contract/index.js)0
-rw-r--r--ui/app/pages/confirm-send-ether/confirm-send-ether.component.js39
-rw-r--r--ui/app/pages/confirm-send-ether/confirm-send-ether.container.js45
-rw-r--r--ui/app/pages/confirm-send-ether/index.js (renamed from ui/app/components/pages/confirm-send-ether/index.js)0
-rw-r--r--ui/app/pages/confirm-send-token/confirm-send-token.component.js29
-rw-r--r--ui/app/pages/confirm-send-token/confirm-send-token.container.js52
-rw-r--r--ui/app/pages/confirm-send-token/index.js (renamed from ui/app/components/pages/confirm-send-token/index.js)0
-rw-r--r--ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js119
-rw-r--r--ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js34
-rw-r--r--ui/app/pages/confirm-token-transaction-base/index.js (renamed from ui/app/components/pages/confirm-token-transaction-base/index.js)0
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js592
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js242
-rw-r--r--ui/app/pages/confirm-transaction-base/index.js (renamed from ui/app/components/pages/confirm-transaction-base/index.js)0
-rw-r--r--ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js (renamed from ui/app/components/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js)0
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js92
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js (renamed from ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js)0
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js (renamed from ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js)0
-rw-r--r--ui/app/pages/confirm-transaction-switch/index.js (renamed from ui/app/components/pages/confirm-transaction-switch/index.js)0
-rw-r--r--ui/app/pages/confirm-transaction/conf-tx.js225
-rw-r--r--ui/app/pages/confirm-transaction/confirm-transaction.component.js160
-rw-r--r--ui/app/pages/confirm-transaction/confirm-transaction.container.js37
-rw-r--r--ui/app/pages/confirm-transaction/index.js (renamed from ui/app/components/pages/confirm-transaction/index.js)0
-rw-r--r--ui/app/pages/create-account/connect-hardware/account-list.js205
-rw-r--r--ui/app/pages/create-account/connect-hardware/connect-screen.js197
-rw-r--r--ui/app/pages/create-account/connect-hardware/index.js293
-rw-r--r--ui/app/pages/create-account/import-account/index.js (renamed from ui/app/components/pages/create-account/import-account/index.js)0
-rw-r--r--ui/app/pages/create-account/import-account/json.js170
-rw-r--r--ui/app/pages/create-account/import-account/private-key.js128
-rw-r--r--ui/app/pages/create-account/import-account/seed.js (renamed from ui/app/components/pages/create-account/import-account/seed.js)0
-rw-r--r--ui/app/pages/create-account/index.js113
-rw-r--r--ui/app/pages/create-account/new-account.js130
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.component.js71
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.container.js (renamed from ui/app/components/pages/first-time-flow/create-password/create-password.container.js)0
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js256
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js (renamed from ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/create-password/index.js (renamed from ui/app/components/pages/first-time-flow/create-password/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/index.js (renamed from ui/app/components/pages/first-time-flow/create-password/new-account/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js225
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/index.js (renamed from ui/app/components/pages/first-time-flow/create-password/unique-image/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js55
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js (renamed from ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js)0
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js93
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js25
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.js (renamed from ui/app/components/pages/first-time-flow/end-of-flow/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.scss (renamed from ui/app/components/pages/first-time-flow/end-of-flow/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js57
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js (renamed from ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js)0
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/index.js (renamed from ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.component.js152
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.container.js31
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.selectors.js26
-rw-r--r--ui/app/pages/first-time-flow/index.js (renamed from ui/app/components/pages/first-time-flow/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/index.scss159
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.js (renamed from ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.scss (renamed from ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js169
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js27
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js155
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js (renamed from ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js (renamed from ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss (renamed from ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.js (renamed from ui/app/components/pages/first-time-flow/seed-phrase/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.scss40
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js (renamed from ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss (renamed from ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js143
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js70
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.js (renamed from ui/app/components/pages/first-time-flow/select-action/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.scss (renamed from ui/app/components/pages/first-time-flow/select-action/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.component.js112
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.container.js23
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.js (renamed from ui/app/components/pages/first-time-flow/welcome/index.js)0
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.scss (renamed from ui/app/components/pages/first-time-flow/welcome/index.scss)0
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.component.js69
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.container.js26
-rw-r--r--ui/app/pages/home/home.component.js77
-rw-r--r--ui/app/pages/home/home.container.js32
-rw-r--r--ui/app/pages/home/index.js (renamed from ui/app/components/pages/home/index.js)0
-rw-r--r--ui/app/pages/index.js31
-rw-r--r--ui/app/pages/index.scss11
-rw-r--r--ui/app/pages/keychains/index.scss (renamed from ui/app/components/pages/keychains/index.scss)0
-rw-r--r--ui/app/pages/keychains/restore-vault.js197
-rw-r--r--ui/app/pages/keychains/reveal-seed.js177
-rw-r--r--ui/app/pages/lock/index.js (renamed from ui/app/components/pages/lock/index.js)0
-rw-r--r--ui/app/pages/lock/lock.component.js26
-rw-r--r--ui/app/pages/lock/lock.container.js24
-rw-r--r--ui/app/pages/mobile-sync/index.js387
-rw-r--r--ui/app/pages/notice/notice.js203
-rw-r--r--ui/app/pages/provider-approval/index.js (renamed from ui/app/components/pages/provider-approval/index.js)0
-rw-r--r--ui/app/pages/provider-approval/provider-approval.component.js29
-rw-r--r--ui/app/pages/provider-approval/provider-approval.container.js12
-rw-r--r--ui/app/pages/routes/index.js441
-rw-r--r--ui/app/pages/settings/advanced-tab/advanced-tab.component.js378
-rw-r--r--ui/app/pages/settings/advanced-tab/advanced-tab.container.js48
-rw-r--r--ui/app/pages/settings/advanced-tab/index.js1
-rw-r--r--ui/app/pages/settings/index.js (renamed from ui/app/components/pages/settings/index.js)0
-rw-r--r--ui/app/pages/settings/index.scss143
-rw-r--r--ui/app/pages/settings/info-tab/index.js (renamed from ui/app/components/pages/settings/info-tab/index.js)0
-rw-r--r--ui/app/pages/settings/info-tab/index.scss (renamed from ui/app/components/pages/settings/info-tab/index.scss)0
-rw-r--r--ui/app/pages/settings/info-tab/info-tab.component.js140
-rw-r--r--ui/app/pages/settings/security-tab/index.js1
-rw-r--r--ui/app/pages/settings/security-tab/security-tab.component.js195
-rw-r--r--ui/app/pages/settings/security-tab/security-tab.container.js42
-rw-r--r--ui/app/pages/settings/settings-tab/index.js (renamed from ui/app/components/pages/settings/settings-tab/index.js)0
-rw-r--r--ui/app/pages/settings/settings-tab/index.scss (renamed from ui/app/components/pages/settings/settings-tab/index.scss)0
-rw-r--r--ui/app/pages/settings/settings-tab/settings-tab.component.js200
-rw-r--r--ui/app/pages/settings/settings-tab/settings-tab.container.js53
-rw-r--r--ui/app/pages/settings/settings.component.js137
-rw-r--r--ui/app/pages/unlock-page/index.js (renamed from ui/app/components/pages/unlock-page/index.js)0
-rw-r--r--ui/app/pages/unlock-page/index.scss (renamed from ui/app/components/pages/unlock-page/index.scss)0
-rw-r--r--ui/app/pages/unlock-page/unlock-page.component.js191
-rw-r--r--ui/app/pages/unlock-page/unlock-page.container.js64
-rw-r--r--ui/app/reducers.js95
-rw-r--r--ui/app/reducers/app.js788
-rw-r--r--ui/app/reducers/locale.js17
-rw-r--r--ui/app/reducers/metamask.js419
-rw-r--r--ui/app/root.js34
-rw-r--r--ui/app/routes.js83
-rw-r--r--ui/app/selectors.js301
-rw-r--r--ui/app/selectors/confirm-transaction.js4
-rw-r--r--ui/app/selectors/custom-gas.js12
-rw-r--r--ui/app/selectors/custom-gas.test.js595
-rw-r--r--ui/app/selectors/selectors.js313
-rw-r--r--ui/app/selectors/tests/custom-gas.test.js595
-rw-r--r--ui/app/selectors/transactions.js4
-rw-r--r--ui/app/store.js21
-rw-r--r--ui/app/store/actions.js2778
-rw-r--r--ui/app/store/store.js21
-rw-r--r--ui/app/util.js326
-rw-r--r--ui/index.js8
-rw-r--r--ui/lib/icon-factory.js2
-rw-r--r--ui/lib/lost-accounts-notice.js2
-rw-r--r--ui/lib/tx-helper.js2
1009 files changed, 38369 insertions, 38167 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abd1b4dc4..bada9b8b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
## Current Develop Branch
+## 6.3.0 Mon Mar 25 2019
+
+- [#6300](https://github.com/MetaMask/metamask-extension/pull/6300): Gas chart hidden on custom networks
+- [#6301](https://github.com/MetaMask/metamask-extension/pull/6301): Fix gas fee in the submitted step of the transaction details activity log
+- [#6302](https://github.com/MetaMask/metamask-extension/pull/6302): Replaces the coinbase link in the deposit modal with one for wyre
+- [#6307](https://github.com/MetaMask/metamask-extension/pull/6307): Centre the notification in the current window
+- [#6312](https://github.com/MetaMask/metamask-extension/pull/6312): Fixes popups not showing when screen size is odd
+- [#6326](https://github.com/MetaMask/metamask-extension/pull/6326): Fix oversized loading overlay on gas customization modal.
+- [#6330](https://github.com/MetaMask/metamask-extension/pull/6330): Stop reloading dapps on network change allowing dapps to decide if it should refresh or not
+- [#6332](https://github.com/MetaMask/metamask-extension/pull/6332): Enable mobile sync
+- [#6333](https://github.com/MetaMask/metamask-extension/pull/6333): Redesign of the settings screen
+- [#6340](https://github.com/MetaMask/metamask-extension/pull/6340): Cancel transactions and signature requests on the closing of notification windows
+- [#6341](https://github.com/MetaMask/metamask-extension/pull/6341): Disable transaction "Cancel" button when balance is insufficient
+- [#6347](https://github.com/MetaMask/metamask-extension/pull/6347): Enable privacy mode by default for first time users
+
## 6.2.2 Tue Mar 12 2019
- [#6271](https://github.com/MetaMask/metamask-extension/pull/6271): Centre all notification popups
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 08e6014f8..eab243b8a 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -11,6 +11,9 @@
"exposeDescription": {
"message": "Expose accounts to the current website. Useful for legacy dapps."
},
+ "chartOnlyAvailableEth": {
+ "message": "Chart only available on Ethereum networks."
+ },
"confirmExpose": {
"message": "Are you sure you want to expose your accounts to the current website?"
},
@@ -41,6 +44,9 @@
"providerRequestInfo": {
"message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with."
},
+ "aboutUs": {
+ "message": "About Us"
+ },
"accept": {
"message": "Accept"
},
@@ -71,6 +77,12 @@
"address": {
"message": "Address"
},
+ "advanced": {
+ "message": "Advanced"
+ },
+ "advancedSettingsDescription": {
+ "message": "Access developer features, download State Logs, Reset Account, setup testnets and custom RPC."
+ },
"advancedOptions": {
"message": "Advanced Options"
},
@@ -89,9 +101,6 @@
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
- "advanced": {
- "message": "Advanced"
- },
"agreeTermsOfService": {
"message": "I agree to the Terms of Service"
},
@@ -188,6 +197,12 @@
"buyCoinbaseExplainer": {
"message": "Coinbase is the world’s most popular way to buy and sell Bitcoin, Ethereum, and Litecoin."
},
+ "buyWithWyre": {
+ "message": "Buy ETH with Wyre"
+ },
+ "buyWithWyreDescription": {
+ "message": "Wyre lets you use aa credit card to deposit ETH right in to your MetaMask account. From the Airswap website, click \"Use Fiat\" in the top-right corner. You can also use Airswap to get started with ERC 20 tokens!"
+ },
"bytes": {
"message": "Bytes"
},
@@ -227,6 +242,9 @@
"chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
},
+ "company": {
+ "message": "Company"
+ },
"confirm": {
"message": "Confirm"
},
@@ -293,6 +311,9 @@
"continueToCoinbase": {
"message": "Continue to Coinbase"
},
+ "continueToWyre": {
+ "message": "Continue to Wyre"
+ },
"contractDeployment": {
"message": "Contract Deployment"
},
@@ -607,6 +628,12 @@
"gasPriceRequired": {
"message": "Gas Price Required"
},
+ "general": {
+ "message": "General"
+ },
+ "generalSettingsDescription": {
+ "message": "Currency conversion, primary currency, language, blockies identicon"
+ },
"generatingTransaction": {
"message": "Generating transaction"
},
@@ -778,6 +805,9 @@
"ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one."
},
+ "legal": {
+ "message": "Legal"
+ },
"lessThanMax": {
"message": "must be less than or equal to $1.",
"description": "helper for inputting hex as decimal input"
@@ -959,6 +989,9 @@
"noTransactions": {
"message": "You have no transactions"
},
+ "notEnoughGas": {
+ "message": "Not Enough Gas"
+ },
"notFound": {
"message": "Not Found"
},
@@ -1228,6 +1261,12 @@
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
+ "securityAndPrivacy": {
+ "message": "Security & Privacy"
+ },
+ "securitySettingsDescription": {
+ "message": "Privacy settings and wallet seed phrase"
+ },
"secondsShorthand": {
"message": "Sec"
},
diff --git a/app/images/caret-left-black.svg b/app/images/caret-left-black.svg
new file mode 100644
index 000000000..872135ece
--- /dev/null
+++ b/app/images/caret-left-black.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="9px" height="15px" viewBox="0 0 9 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>8439120D-5704-4273-B416-FEE134322584</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Approve---insufficient-amount" transform="translate(-75.000000, -69.000000)" stroke="#000000" stroke-width="2">
+ <g id="Group-7" transform="translate(53.000000, 51.000000)">
+ <g id="cancel" transform="translate(24.000000, 14.000000)">
+ <g id="Group">
+ <polyline id="Path-8" points="6.1263881 18.0633906 0 11.6306831 6.31493631 5"></polyline>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/app/images/wyre.svg b/app/images/wyre.svg
new file mode 100644
index 000000000..a5209329d
--- /dev/null
+++ b/app/images/wyre.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="72" height="24" viewBox="0 0 72 24">
+ <g fill="#000000" fill-rule="evenodd">
+ <path d="M40.2451269,20.4956914 C42.4518577,20.8032343 43.7956269,20.4027771 44.8365115,19.3728343 C45.49005,18.7265486 45.9867808,17.96952 46.4253577,17.1740914 C48.2011269,13.9553486 52.4737038,6.17900571 52.4737038,6.17900571 L49.3957038,6.17900571 L46.1955115,12.5492914 L45.1968577,6.17900571 L41.9257038,6.17900571 L41.9257038,6.32712 C41.9257038,6.32712 43.03305,12.15912 43.6014346,15.1478057 C43.6862423,15.5931771 43.7738192,16.7393486 42.9603577,17.4806057 C42.5051654,17.89512 41.8478192,18.2396914 40.71555,18.0562629 L40.2451269,20.4956914 Z"/>
+ <polygon points="37.793 6.179 34.817 12.868 34.737 12.861 34.334 6.345 31.093 6.345 27.934 12.869 27.853 12.842 27.718 6.179 24.617 6.179 25.234 16.864 28.59 16.864 31.842 9.892 31.918 9.905 32.434 16.864 35.812 16.864 40.698 6.179"/>
+ <path d="M55.7303192 16.8643543L52.4737038 16.8643543C53.1992423 13.2612686 53.8922423 9.77921143 54.6174346 6.17886857L57.1422808 6.17886857 57.1422808 8.14481143C57.4801269 7.33258286 57.9412038 6.57624 58.7235115 6.24435429 59.4909346 5.91864 60.6772038 5.93578286 61.8866654 6.30366857 61.4103577 7.21738286 61.0472423 8.03029714 60.5314731 9.01944 60.0662423 8.92824 59.6477423 8.81166857 59.2216269 8.76984 58.2645115 8.67555429 57.5694346 9.05372571 57.2426654 9.95269714 57.01455 10.5804686 56.8726269 11.2452686 56.7348577 11.9011543 56.3893962 13.5444686 56.0685115 15.1918971 55.7303192 16.8643543M66.1421769 8.72009143C66.6077538 8.15952 67.4461385 7.84957714 68.0685231 8.04260571 68.8276385 8.27814857 69.0768692 9.11163429 68.5375615 9.69346286 68.3544462 9.89094857 68.0989846 10.0544914 67.8445615 10.14912 67.0612154 10.4405486 66.2505231 10.4316343 65.3976 10.2862629 65.5419462 9.66672 65.7704077 9.16752 66.1421769 8.72009143L66.1421769 8.72009143zM70.3102154 14.2082057C70.3102154 14.2082057 70.2828692 14.2078629 70.2011769 14.2078629 68.9543308 14.6816914 67.5707538 14.8058057 66.6171 14.5380343 65.5634077 14.2421486 64.9555615 13.33392 65.1469846 12.3080914 68.2872923 12.5696914 69.6203308 11.9775771 70.3358308 11.5538057 72.2742923 10.4055771 72.7672154 7.33220571 70.2378692 6.42226286 68.0155615 5.62272 64.9199077 6.00294857 63.4574077 7.75769143 62.1247154 9.35677714 61.7335615 11.1152914 61.9377923 13.0726629 62.0911385 14.5452343 63.1105615 16.6562057 66.4422923 16.9856914 68.4565615 17.1848914 69.7459846 16.67232 69.8397923 16.63392L70.3102154 14.2082057zM4.97682692 23.9956457L12.3824423 23.9956457C13.8428654 23.9956457 15.15825 23.1210171 15.7114038 21.7825029L19.7060192 12.11736 15.2226346 17.7649029C14.5431346 18.6614743 13.5147115 19.2443314 12.30525 19.2443314L6.94055769 19.2443314 4.97682692 23.9956457z"/>
+ <path d="M2.52183462,18.0546514 L9.92745,18.0546514 C11.3878731,18.0546514 12.7032577,17.1800229 13.2564115,15.8415086 L17.2513731,6.17670857 L12.7676423,11.8239086 C12.0881423,12.72048 11.0597192,13.3033371 9.85025769,13.3033371 L4.48556538,13.3033371 L2.52183462,18.0546514 Z"/>
+ <path d="M7.39111154,0.22872 L14.7967269,0.22872 L10.8014192,9.89489143 C10.2482654,11.2334057 8.93288077,12.1080343 7.47245769,12.1080343 L0.0668423077,12.1080343 L4.06215,2.44186286 C4.61530385,1.10334857 5.93068846,0.22872 7.39111154,0.22872"/>
+ </g>
+</svg>
diff --git a/app/manifest.json b/app/manifest.json
index df5b67bb9..6837adf61 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
- "version": "6.2.2",
+ "version": "6.3.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index f92341353..737411890 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -40,7 +40,9 @@ class PreferencesController {
// Feature flag toggling is available in the global namespace
// for convenient testing of pre-release features, and should never
// perform sensitive operations.
- featureFlags: {},
+ featureFlags: {
+ privacyMode: true,
+ },
knownMethodData: {},
participateInMetaMetrics: null,
firstTimeFlowType: null,
diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js
index b296dc5eb..765551167 100644
--- a/app/scripts/controllers/transactions/tx-gas-utils.js
+++ b/app/scripts/controllers/transactions/tx-gas-utils.js
@@ -7,7 +7,7 @@ const {
const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
-import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/constants/error-keys'
+import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'
/**
tx-gas-utils are gas utility methods for Transaction manager
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index c7f0c5669..68394d1ae 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -91,6 +91,10 @@ inpageProvider.enable = function ({ force } = {}) {
})
}
+// give the dapps control of a refresh they can toggle this off on the window.ethereum
+// this will be default true so it does not break any old apps.
+inpageProvider.autoRefreshOnNetworkChange = true
+
// add metamask-specific convenience methods
inpageProvider._metamask = new Proxy({
/**
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index 558391a06..44fbe847c 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -20,6 +20,10 @@ function setupDappAutoReload (web3, observable) {
})
observable.subscribe(function (state) {
+ // if the auto refresh on network change is false do not
+ // do anything
+ if (!window.ethereum.autoRefreshOnNetworkChange) return
+
// if reload in progress, no need to check reload logic
if (reloadInProgress) return
diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js
index 4e2d0bc79..040b5695b 100644
--- a/app/scripts/lib/buy-eth-url.js
+++ b/app/scripts/lib/buy-eth-url.js
@@ -15,7 +15,7 @@ function getBuyEthUrl ({ network, amount, address }) {
let url
switch (network) {
case '1':
- url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
+ url = `https://dash.sendwyre.com/sign-up`
break
case '3':
diff --git a/development/mock-dev.js b/development/mock-dev.js
index 1af10a131..4a3217a06 100644
--- a/development/mock-dev.js
+++ b/development/mock-dev.js
@@ -14,9 +14,9 @@
const render = require('react-dom').render
const h = require('react-hyperscript')
-const Root = require('../ui/app/root')
-const configureStore = require('../ui/app/store')
-const actions = require('../ui/app/actions')
+const Root = require('../ui/app/pages')
+const configureStore = require('../ui/app/store/store')
+const actions = require('../ui/app/store/actions')
const states = require('./states')
const backGroundConnectionModifiers = require('./backGroundConnectionModifiers')
const Selector = require('./selector')
diff --git a/development/ui-dev.js b/development/ui-dev.js
index bae0ce50e..70f513972 100644
--- a/development/ui-dev.js
+++ b/development/ui-dev.js
@@ -17,7 +17,7 @@
const render = require('react-dom').render
const h = require('react-hyperscript')
-const Root = require('../ui/app/root')
+const Root = require('../ui/app/pages')
const configureStore = require('./uiStore')
const states = require('./states')
const Selector = require('./selector')
diff --git a/development/uiStore.js b/development/uiStore.js
index c71d66d3b..bfec8f5e4 100644
--- a/development/uiStore.js
+++ b/development/uiStore.js
@@ -2,7 +2,7 @@ const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk').default
const createLogger = require('redux-logger').createLogger
-const rootReducer = require('../ui/app/reducers')
+const rootReducer = require('../ui/app/ducks')
module.exports = configureStore
diff --git a/docs/adding-new-networks.md b/docs/adding-new-networks.md
index b74233fa6..40925f135 100644
--- a/docs/adding-new-networks.md
+++ b/docs/adding-new-networks.md
@@ -5,7 +5,7 @@ To add another network to our dropdown menu, make sure the following files are a
```
app/scripts/config.js
app/scripts/lib/buy-eth-url.js
-ui/app/app.js
+ui/app/index.js
ui/app/components/buy-button-subview.js
ui/app/components/drop-menu-item.js
ui/app/components/network.js
diff --git a/docs/secret-preferences.md b/docs/secret-preferences.md
index f9d01a503..58a4554c4 100644
--- a/docs/secret-preferences.md
+++ b/docs/secret-preferences.md
@@ -6,5 +6,5 @@ One example is our "sync with mobile" feature, which didn't make sense to roll o
To enable features like this, first open the background console, and then you can use the global method `global.setPreference(key, value)`.
-For example, if the feature flag was a booelan was called `mobileSync`, you might type `setPreference('mobileSync', true)`.
+For example, if the feature flag was a boolean was called `useNativeCurrencyAsPrimaryCurrency`, you might type `setPreference('useNativeCurrencyAsPrimaryCurrency', true)`.
diff --git a/mascara/src/app/buy-ether-widget/index.js b/mascara/src/app/buy-ether-widget/index.js
index c8530ba4c..d0d6ff343 100644
--- a/mascara/src/app/buy-ether-widget/index.js
+++ b/mascara/src/app/buy-ether-widget/index.js
@@ -5,7 +5,7 @@ import {connect} from 'react-redux'
import {qrcode} from 'qrcode-generator'
import copyToClipboard from 'copy-to-clipboard'
import ShapeShiftForm from '../shapeshift-form'
-import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
+import {buyEth, showAccountDetail} from '../../../../ui/app/store/actions'
const OPTION_VALUES = {
COINBASE: 'coinbase',
diff --git a/mascara/src/app/shapeshift-form/index.js b/mascara/src/app/shapeshift-form/index.js
index fe7f7ffcb..c044f9ecc 100644
--- a/mascara/src/app/shapeshift-form/index.js
+++ b/mascara/src/app/shapeshift-form/index.js
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import qrcode from 'qrcode-generator'
import {connect} from 'react-redux'
-import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/actions'
-import {isValidAddress} from '../../../../ui/app/util'
+import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/store/actions'
+import {isValidAddress} from '../../../../ui/app/helpers/utils/util'
export class ShapeShiftForm extends Component {
static propTypes = {
diff --git a/package-lock.json b/package-lock.json
index 1d2b3b724..d753a1e9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9849,7 +9849,7 @@
"dependencies": {
"babelify": {
"version": "7.3.0",
- "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"requires": {
"babel-core": "^6.0.14",
@@ -9901,7 +9901,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.11.8",
@@ -10189,7 +10189,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.11.8",
@@ -10484,7 +10484,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.11.8",
@@ -24076,7 +24076,7 @@
"dependencies": {
"babelify": {
"version": "7.3.0",
- "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"requires": {
"babel-core": "^6.0.14",
@@ -24115,7 +24115,7 @@
},
"babelify": {
"version": "7.3.0",
- "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"requires": {
"babel-core": "^6.0.14",
@@ -26541,7 +26541,7 @@
"dependencies": {
"babelify": {
"version": "7.3.0",
- "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"requires": {
"babel-core": "^6.0.14",
diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js
index 06f67ad95..2700d1656 100644
--- a/test/e2e/beta/metamask-beta-ui.spec.js
+++ b/test/e2e/beta/metamask-beta-ui.spec.js
@@ -233,7 +233,11 @@ describe('MetaMask', function () {
await customRpcButton.click()
await delay(regularDelayMs)
- const privacyToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(10) .settings-page__content-item-col > div'))
+ const securityTab = await findElement(driver, By.xpath(`//div[contains(text(), 'Security & Privacy')]`))
+ await securityTab.click()
+ await delay(regularDelayMs)
+
+ const privacyToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(1) .settings-page__content-item-col > div'))
await privacyToggle.click()
await delay(largeDelayMs * 2)
})
@@ -472,15 +476,19 @@ describe('MetaMask', function () {
const settingsButton = await findElement(driver, By.xpath(`//div[contains(text(), 'Settings')]`))
settingsButton.click()
- await findElement(driver, By.css('.tab-bar'))
+ // await findElement(driver, By.css('.tab-bar'))
+
+ const advancedTab = await findElement(driver, By.xpath(`//div[contains(text(), 'Advanced')]`))
+ await advancedTab.click()
+ await delay(regularDelayMs)
- const showConversionToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(3) .settings-page__content-item-col > div'))
+ const showConversionToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(7) .settings-page__content-item-col > div'))
await showConversionToggle.click()
const advancedGasTitle = await findElement(driver, By.xpath(`//span[contains(text(), 'Advanced gas controls')]`))
await driver.executeScript('arguments[0].scrollIntoView(true)', advancedGasTitle)
- const advancedGasToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(12) .settings-page__content-item-col > div'))
+ const advancedGasToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(5) .settings-page__content-item-col > div'))
await advancedGasToggle.click()
windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
@@ -1053,7 +1061,6 @@ describe('MetaMask', function () {
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
- await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs)
await driver.switchTo().window(dapp)
@@ -1062,7 +1069,6 @@ describe('MetaMask', function () {
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`))
await transferTokens.click()
- await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
await delay(largeDelayMs)
diff --git a/test/unit/actions/config_test.js b/test/unit/actions/config_test.js
index 648f456fb..9127474a8 100644
--- a/test/unit/actions/config_test.js
+++ b/test/unit/actions/config_test.js
@@ -3,8 +3,8 @@ var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('config view actions', function () {
var initialState = {
diff --git a/test/unit/actions/set_account_label_test.js b/test/unit/actions/set_account_label_test.js
index 53ea1d130..1601d6383 100644
--- a/test/unit/actions/set_account_label_test.js
+++ b/test/unit/actions/set_account_label_test.js
@@ -2,8 +2,8 @@ const assert = require('assert')
const freeze = require('deep-freeze-strict')
const path = require('path')
-const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('SET_ACCOUNT_LABEL', function () {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () {
diff --git a/test/unit/actions/set_selected_account_test.js b/test/unit/actions/set_selected_account_test.js
index 28b47d09d..36d312d7b 100644
--- a/test/unit/actions/set_selected_account_test.js
+++ b/test/unit/actions/set_selected_account_test.js
@@ -3,8 +3,8 @@ var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('SET_SELECTED_ACCOUNT', function () {
it('sets the state.appState.activeAddress property of the state to the action.value', function () {
diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js
index 160cd4552..8c64d844f 100644
--- a/test/unit/actions/tx_test.js
+++ b/test/unit/actions/tx_test.js
@@ -4,7 +4,7 @@ var path = require('path')
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
-const actions = require(path.join(__dirname, '../../../ui/app/actions.js'))
+const actions = require(path.join(__dirname, '../../../ui/app/store/actions.js'))
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
diff --git a/test/unit/actions/view_info_test.js b/test/unit/actions/view_info_test.js
index 69895d801..5785a368c 100644
--- a/test/unit/actions/view_info_test.js
+++ b/test/unit/actions/view_info_test.js
@@ -3,8 +3,8 @@ var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('SHOW_INFO_PAGE', function () {
it('sets the state.appState.currentView.name property to info', function () {
diff --git a/test/unit/actions/warning_test.js b/test/unit/actions/warning_test.js
index 28b565499..e57374cda 100644
--- a/test/unit/actions/warning_test.js
+++ b/test/unit/actions/warning_test.js
@@ -3,8 +3,8 @@ var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('action DISPLAY_WARNING', function () {
it('sets appState.warning to provided value', function () {
diff --git a/test/unit/app/buy-eth-url.spec.js b/test/unit/app/buy-eth-url.spec.js
index 36646fa68..6cf3e7d75 100644
--- a/test/unit/app/buy-eth-url.spec.js
+++ b/test/unit/app/buy-eth-url.spec.js
@@ -18,14 +18,9 @@ describe('', function () {
}
it('returns coinbase url with amount and address for network 1', function () {
- const coinbaseUrl = getBuyEthUrl(mainnet)
- const coinbase = coinbaseUrl.match(/(https:\/\/buy.coinbase.com)/)
- const amount = coinbaseUrl.match(/(amount)\D\d/)
- const address = coinbaseUrl.match(/(address)(.*)(?=&)/)
-
- assert.equal(coinbase[0], 'https://buy.coinbase.com')
- assert.equal(amount[0], 'amount=5')
- assert.equal(address[0], 'address=0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
+ const wyreUrl = getBuyEthUrl(mainnet)
+
+ assert.equal(wyreUrl, 'https://dash.sendwyre.com/sign-up')
})
diff --git a/test/unit/balance-formatter-test.js b/test/unit/balance-formatter-test.js
index ab6daa19c..bd0eb5008 100644
--- a/test/unit/balance-formatter-test.js
+++ b/test/unit/balance-formatter-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
const currencyFormatter = require('currency-formatter')
-const infuraConversion = require('../../ui/app/infura-conversion.json')
+const infuraConversion = require('../../ui/app/helpers/constants/infura-conversion.json')
describe('currencyFormatting', function () {
it('be able to format any infura currency', function (done) {
diff --git a/test/unit/reducers/unlock_vault_test.js b/test/unit/reducers/unlock_vault_test.js
index d66e8edbb..d66891a63 100644
--- a/test/unit/reducers/unlock_vault_test.js
+++ b/test/unit/reducers/unlock_vault_test.js
@@ -4,8 +4,8 @@ var assert = require('assert')
var path = require('path')
var sinon = require('sinon')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js'))
describe('#unlockMetamask(selectedAccount)', function () {
beforeEach(function () {
diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js
index f3f236d90..1fadbfb60 100644
--- a/test/unit/responsive/components/dropdown-test.js
+++ b/test/unit/responsive/components/dropdown-test.js
@@ -3,7 +3,7 @@ const assert = require('assert')
const h = require('react-hyperscript')
const sinon = require('sinon')
const path = require('path')
-const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown
+const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'app', 'dropdowns', 'index.js')).Dropdown
const { createMockStore } = require('redux-test-utils')
const { mountWithStore } = require('../../../lib/render-helpers')
diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js
index bfc6459b1..d395c9adb 100644
--- a/test/unit/ui/app/actions.spec.js
+++ b/test/unit/ui/app/actions.spec.js
@@ -16,7 +16,7 @@ const { createTestProviderTools } = require('../../../stub/provider')
const provider = createTestProviderTools({ scaffold: {}}).provider
const enLocale = require('../../../../app/_locales/en/messages.json')
-const actions = require('../../../../ui/app/actions')
+const actions = require('../../../../ui/app/store/actions')
const MetaMaskController = require('../../../../app/scripts/metamask-controller')
const firstTimeState = require('../../../unit/localhostState')
diff --git a/test/unit/ui/app/components/token-cell.spec.js b/test/unit/ui/app/components/token-cell.spec.js
index 6145c6924..23e76c418 100644
--- a/test/unit/ui/app/components/token-cell.spec.js
+++ b/test/unit/ui/app/components/token-cell.spec.js
@@ -5,8 +5,8 @@ import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import { mount } from 'enzyme'
-import TokenCell from '../../../../../ui/app/components/token-cell'
-import Identicon from '../../../../../ui/app/components/identicon'
+import TokenCell from '../../../../../ui/app/components/app/token-cell'
+import Identicon from '../../../../../ui/app/components/ui/identicon'
describe('Token Cell', () => {
let wrapper
diff --git a/test/unit/ui/app/reducers/app.spec.js b/test/unit/ui/app/reducers/app.spec.js
index bee4963e5..6c77e0ef9 100644
--- a/test/unit/ui/app/reducers/app.spec.js
+++ b/test/unit/ui/app/reducers/app.spec.js
@@ -1,6 +1,6 @@
import assert from 'assert'
-import reduceApp from '../../../../../ui/app/reducers/app'
-import * as actions from '../../../../../ui/app/actions'
+import reduceApp from '../../../../../ui/app/ducks/app/app'
+import * as actions from '../../../../../ui/app/store/actions'
describe('App State', () => {
diff --git a/test/unit/ui/app/reducers/metamask.spec.js b/test/unit/ui/app/reducers/metamask.spec.js
index 8cdb780fe..388c67c76 100644
--- a/test/unit/ui/app/reducers/metamask.spec.js
+++ b/test/unit/ui/app/reducers/metamask.spec.js
@@ -1,6 +1,6 @@
import assert from 'assert'
-import reduceMetamask from '../../../../../ui/app/reducers/metamask'
-import * as actions from '../../../../../ui/app/actions'
+import reduceMetamask from '../../../../../ui/app/ducks/metamask/metamask'
+import * as actions from '../../../../../ui/app/store/actions'
describe('MetaMask Reducers', () => {
diff --git a/test/unit/ui/app/selectors.spec.js b/test/unit/ui/app/selectors.spec.js
index 070de0bcd..b4aa8e272 100644
--- a/test/unit/ui/app/selectors.spec.js
+++ b/test/unit/ui/app/selectors.spec.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const selectors = require('../../../../ui/app/selectors')
+const selectors = require('../../../../ui/app/selectors/selectors')
const mockState = require('../../../data/mock-state.json')
const Eth = require('ethjs')
diff --git a/test/unit/util_test.js b/test/unit/util_test.js
index 39473854f..87f57b218 100644
--- a/test/unit/util_test.js
+++ b/test/unit/util_test.js
@@ -3,7 +3,7 @@ var sinon = require('sinon')
const ethUtil = require('ethereumjs-util')
var path = require('path')
-var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'util.js'))
+var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'helpers', 'utils', 'util.js'))
describe('util', function () {
var ethInWei = '1'
diff --git a/ui/.gitignore b/ui/.gitignore
deleted file mode 100644
index c6b1254b5..000000000
--- a/ui/.gitignore
+++ /dev/null
@@ -1,66 +0,0 @@
-
-# Created by https://www.gitignore.io/api/osx,node
-
-### OSX ###
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
-
-
-### Node ###
-# Logs
-logs
-*.log
-npm-debug.log*
-
-# Runtime data
-pids
-*.pid
-*.seed
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-
-# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (http://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directory
-# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
-node_modules
-
-# Optional npm cache directory
-.npm
-
-# Optional REPL history
-.node_repl_history
-
diff --git a/ui/app/accounts/new-account/index.js b/ui/app/accounts/new-account/index.js
deleted file mode 100644
index 795bd7ce6..000000000
--- a/ui/app/accounts/new-account/index.js
+++ /dev/null
@@ -1,87 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const actions = require('../../actions')
-const { getCurrentViewContext } = require('../../selectors')
-const classnames = require('classnames')
-
-const NewAccountCreateForm = require('./create-form')
-const NewAccountImportForm = require('../import')
-
-function mapStateToProps (state) {
- return {
- displayedForm: getCurrentViewContext(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- displayForm: form => dispatch(actions.setNewAccountForm(form)),
- showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
- showExportPrivateKeyModal: () => {
- dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
- },
- hideModal: () => dispatch(actions.hideModal()),
- setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
- }
-}
-
-inherits(AccountDetailsModal, Component)
-function AccountDetailsModal (props) {
- Component.call(this)
-
- this.state = {
- displayedForm: props.displayedForm,
- }
-}
-
-AccountDetailsModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal)
-
-
-AccountDetailsModal.prototype.render = function () {
- const { displayedForm, displayForm } = this.props
-
- return h('div.new-account', {}, [
-
- h('div.new-account__header', [
-
- h('div.new-account__title', this.context.t('newAccount')),
-
- h('div.new-account__tabs', [
-
- h('div.new-account__tabs__tab', {
- className: classnames('new-account__tabs__tab', {
- 'new-account__tabs__selected': displayedForm === 'CREATE',
- 'new-account__tabs__unselected cursor-pointer': displayedForm !== 'CREATE',
- }),
- onClick: () => displayForm('CREATE'),
- }, this.context.t('createDen')),
-
- h('div.new-account__tabs__tab', {
- className: classnames('new-account__tabs__tab', {
- 'new-account__tabs__selected': displayedForm === 'IMPORT',
- 'new-account__tabs__unselected cursor-pointer': displayedForm !== 'IMPORT',
- }),
- onClick: () => displayForm('IMPORT'),
- }, this.context.t('import')),
-
- ]),
-
- ]),
-
- h('div.new-account__form', [
-
- displayedForm === 'CREATE'
- ? h(NewAccountCreateForm)
- : h(NewAccountImportForm),
-
- ]),
-
- ])
-}
diff --git a/ui/app/actions.js b/ui/app/actions.js
deleted file mode 100644
index 65070fc8c..000000000
--- a/ui/app/actions.js
+++ /dev/null
@@ -1,2774 +0,0 @@
-const abi = require('human-standard-token-abi')
-const pify = require('pify')
-const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
-const { getTokenAddressFromTokenObject } = require('./util')
-const {
- calcTokenBalance,
- estimateGas,
-} = require('./components/send/send.utils')
-const ethUtil = require('ethereumjs-util')
-const { fetchLocale } = require('../i18n-helper')
-const log = require('loglevel')
-const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums')
-const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
-const gasDuck = require('./ducks/gas.duck')
-const WebcamUtils = require('../lib/webcam-utils')
-
-var actions = {
- _setBackgroundConnection: _setBackgroundConnection,
-
- GO_HOME: 'GO_HOME',
- goHome: goHome,
- // modal state
- MODAL_OPEN: 'UI_MODAL_OPEN',
- MODAL_CLOSE: 'UI_MODAL_CLOSE',
- showModal: showModal,
- hideModal: hideModal,
- // sidebar state
- SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN',
- SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE',
- showSidebar: showSidebar,
- hideSidebar: hideSidebar,
- // sidebar state
- ALERT_OPEN: 'UI_ALERT_OPEN',
- ALERT_CLOSE: 'UI_ALERT_CLOSE',
- showAlert: showAlert,
- hideAlert: hideAlert,
- QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED',
- qrCodeDetected,
- // network dropdown open
- NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
- NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
- showNetworkDropdown: showNetworkDropdown,
- hideNetworkDropdown: hideNetworkDropdown,
- // menu state/
- getNetworkStatus: 'getNetworkStatus',
- // transition state
- TRANSITION_FORWARD: 'TRANSITION_FORWARD',
- TRANSITION_BACKWARD: 'TRANSITION_BACKWARD',
- transitionForward,
- transitionBackward,
- // remote state
- UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
- updateMetamaskState: updateMetamaskState,
- // notices
- MARK_NOTICE_READ: 'MARK_NOTICE_READ',
- markNoticeRead: markNoticeRead,
- SHOW_NOTICE: 'SHOW_NOTICE',
- showNotice: showNotice,
- CLEAR_NOTICES: 'CLEAR_NOTICES',
- clearNotices: clearNotices,
- markAccountsFound,
- // intialize screen
- CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
- SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
- SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
- fetchInfoToSync,
- FORGOT_PASSWORD: 'FORGOT_PASSWORD',
- forgotPassword: forgotPassword,
- markPasswordForgotten,
- unMarkPasswordForgotten,
- SHOW_INIT_MENU: 'SHOW_INIT_MENU',
- SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
- SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
- SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE',
- SHOW_NEW_ACCOUNT_PAGE: 'SHOW_NEW_ACCOUNT_PAGE',
- SET_NEW_ACCOUNT_FORM: 'SET_NEW_ACCOUNT_FORM',
- unlockMetamask: unlockMetamask,
- unlockFailed: unlockFailed,
- unlockSucceeded,
- showCreateVault: showCreateVault,
- showRestoreVault: showRestoreVault,
- showInitializeMenu: showInitializeMenu,
- showImportPage,
- showNewAccountPage,
- setNewAccountForm,
- createNewVaultAndKeychain: createNewVaultAndKeychain,
- createNewVaultAndRestore: createNewVaultAndRestore,
- createNewVaultInProgress: createNewVaultInProgress,
- createNewVaultAndGetSeedPhrase,
- unlockAndGetSeedPhrase,
- addNewKeyring,
- importNewAccount,
- addNewAccount,
- connectHardware,
- checkHardwareStatus,
- forgetDevice,
- unlockHardwareWalletAccount,
- NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
- navigateToNewAccountScreen,
- resetAccount,
- removeAccount,
- showNewVaultSeed: showNewVaultSeed,
- showInfoPage: showInfoPage,
- CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN',
- closeWelcomeScreen,
- // seed recovery actions
- REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
- revealSeedConfirmation: revealSeedConfirmation,
- requestRevealSeed: requestRevealSeed,
- requestRevealSeedWords,
- // unlock screen
- UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
- UNLOCK_FAILED: 'UNLOCK_FAILED',
- UNLOCK_SUCCEEDED: 'UNLOCK_SUCCEEDED',
- UNLOCK_METAMASK: 'UNLOCK_METAMASK',
- LOCK_METAMASK: 'LOCK_METAMASK',
- tryUnlockMetamask: tryUnlockMetamask,
- lockMetamask: lockMetamask,
- unlockInProgress: unlockInProgress,
- // error handling
- displayWarning: displayWarning,
- DISPLAY_WARNING: 'DISPLAY_WARNING',
- HIDE_WARNING: 'HIDE_WARNING',
- hideWarning: hideWarning,
- // accounts screen
- SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT',
- SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN',
- setSelectedToken,
- SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
- SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
- SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
- SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
- SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
- showQrScanner,
- setCurrentCurrency,
- setCurrentAccountTab,
- // account detail screen
- SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
- showSendPage: showSendPage,
- SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE',
- showSendTokenPage,
- ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
- addToAddressBook: addToAddressBook,
- REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
- requestExportAccount: requestExportAccount,
- EXPORT_ACCOUNT: 'EXPORT_ACCOUNT',
- exportAccount: exportAccount,
- SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
- showPrivateKey: showPrivateKey,
- exportAccountComplete,
- SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
- setAccountLabel,
- updateNetworkNonce,
- SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
- // tx conf screen
- COMPLETED_TX: 'COMPLETED_TX',
- TRANSACTION_ERROR: 'TRANSACTION_ERROR',
- NEXT_TX: 'NEXT_TX',
- PREVIOUS_TX: 'PREV_TX',
- EDIT_TX: 'EDIT_TX',
- signMsg: signMsg,
- cancelMsg: cancelMsg,
- signPersonalMsg,
- cancelPersonalMsg,
- signTypedMsg,
- cancelTypedMsg,
- sendTx: sendTx,
- signTx: signTx,
- signTokenTx: signTokenTx,
- updateTransaction,
- updateAndApproveTx,
- cancelTx: cancelTx,
- cancelTxs,
- completedTx: completedTx,
- txError: txError,
- nextTx: nextTx,
- editTx,
- previousTx: previousTx,
- cancelAllTx: cancelAllTx,
- viewPendingTx: viewPendingTx,
- VIEW_PENDING_TX: 'VIEW_PENDING_TX',
- updateTransactionParams,
- UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
- // send screen
- UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
- UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
- UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
- UPDATE_SEND_FROM: 'UPDATE_SEND_FROM',
- UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA',
- UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE',
- UPDATE_SEND_TO: 'UPDATE_SEND_TO',
- UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
- UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO',
- UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
- UPDATE_SEND_WARNINGS: 'UPDATE_SEND_WARNINGS',
- UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
- UPDATE_SEND: 'UPDATE_SEND',
- CLEAR_SEND: 'CLEAR_SEND',
- OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
- CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
- GAS_LOADING_STARTED: 'GAS_LOADING_STARTED',
- GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED',
- setGasLimit,
- setGasPrice,
- updateGasData,
- setGasTotal,
- setSendTokenBalance,
- updateSendTokenBalance,
- updateSendHexData,
- updateSendTo,
- updateSendAmount,
- updateSendMemo,
- setMaxModeTo,
- updateSend,
- updateSendErrors,
- updateSendWarnings,
- clearSend,
- setSelectedAddress,
- gasLoadingStarted,
- gasLoadingFinished,
- // app messages
- confirmSeedWords: confirmSeedWords,
- showAccountDetail: showAccountDetail,
- BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL',
- backToAccountDetail: backToAccountDetail,
- showAccountsPage: showAccountsPage,
- showConfTxPage: showConfTxPage,
- // config screen
- SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE',
- SET_RPC_TARGET: 'SET_RPC_TARGET',
- SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET',
- SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
- SET_PREVIOUS_PROVIDER: 'SET_PREVIOUS_PROVIDER',
- 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,
- addKnownMethodData,
- UPDATE_TOKENS: 'UPDATE_TOKENS',
- updateAndSetCustomRpc: updateAndSetCustomRpc,
- setRpcTarget: setRpcTarget,
- delRpcTarget: delRpcTarget,
- setProviderType: setProviderType,
- SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
- setHardwareWalletDefaultHdPath,
- updateProviderType,
- // loading overlay
- SHOW_LOADING: 'SHOW_LOADING_INDICATION',
- HIDE_LOADING: 'HIDE_LOADING_INDICATION',
- showLoadingIndication: showLoadingIndication,
- hideLoadingIndication: hideLoadingIndication,
- // buy Eth with coinbase
- onboardingBuyEthView,
- ONBOARDING_BUY_ETH_VIEW: 'ONBOARDING_BUY_ETH_VIEW',
- BUY_ETH: 'BUY_ETH',
- buyEth: buyEth,
- buyEthView: buyEthView,
- buyWithShapeShift,
- BUY_ETH_VIEW: 'BUY_ETH_VIEW',
- COINBASE_SUBVIEW: 'COINBASE_SUBVIEW',
- coinBaseSubview: coinBaseSubview,
- SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
- shapeShiftSubview: shapeShiftSubview,
- PAIR_UPDATE: 'PAIR_UPDATE',
- pairUpdate: pairUpdate,
- coinShiftRquest: coinShiftRquest,
- SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION',
- showSubLoadingIndication: showSubLoadingIndication,
- HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION',
- hideSubLoadingIndication: hideSubLoadingIndication,
-// QR STUFF:
- SHOW_QR: 'SHOW_QR',
- showQrView: showQrView,
- reshowQrCode: reshowQrCode,
- SHOW_QR_VIEW: 'SHOW_QR_VIEW',
-// FORGOT PASSWORD:
- BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU',
- goBackToInitView: goBackToInitView,
- RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
- BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
- backToUnlockView: backToUnlockView,
- // SHOWING KEYCHAIN
- SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN',
- showNewKeychain: showNewKeychain,
-
- callBackgroundThenUpdate,
- forceUpdateMetamaskState,
-
- TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU',
- toggleAccountMenu,
-
- useEtherscanProvider,
-
- SET_USE_BLOCKIE: 'SET_USE_BLOCKIE',
- setUseBlockie,
-
- SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS',
- SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT',
- setParticipateInMetaMetrics,
- setMetaMetricsSendCount,
-
- // locale
- SET_CURRENT_LOCALE: 'SET_CURRENT_LOCALE',
- SET_LOCALE_MESSAGES: 'SET_LOCALE_MESSAGES',
- setCurrentLocale,
- updateCurrentLocale,
- setLocaleMessages,
- //
- // Feature Flags
- setFeatureFlag,
- updateFeatureFlags,
- UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS',
-
- // Preferences
- setPreference,
- updatePreferences,
- UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
- setUseNativeCurrencyAsPrimaryCurrencyPreference,
- setShowFiatConversionOnTestnetsPreference,
-
- // Migration of users to new UI
- setCompletedUiMigration,
- completeUiMigration,
- COMPLETE_UI_MIGRATION: 'COMPLETE_UI_MIGRATION',
-
- // Onboarding
- setCompletedOnboarding,
- completeOnboarding,
- COMPLETE_ONBOARDING: 'COMPLETE_ONBOARDING',
-
- setMouseUserState,
- SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
-
- // Network
- updateNetworkEndpointType,
- UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
-
- retryTransaction,
- SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
- CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
- setPendingTokens,
- clearPendingTokens,
-
- createCancelTransaction,
- createSpeedUpTransaction,
-
- approveProviderRequest,
- rejectProviderRequest,
- clearApprovedOrigins,
-
- setFirstTimeFlowType,
- SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE',
-}
-
-module.exports = actions
-
-var background = null
-function _setBackgroundConnection (backgroundConnection) {
- background = backgroundConnection
-}
-
-function goHome () {
- return {
- type: actions.GO_HOME,
- }
-}
-
-// async actions
-
-function tryUnlockMetamask (password) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- dispatch(actions.unlockInProgress())
- log.debug(`background.submitPassword`)
-
- return new Promise((resolve, reject) => {
- background.submitPassword(password, error => {
- if (error) {
- return reject(error)
- }
-
- resolve()
- })
- })
- .then(() => {
- dispatch(actions.unlockSucceeded())
- return forceUpdateMetamaskState(dispatch)
- })
- .then(() => {
- return new Promise((resolve, reject) => {
- background.verifySeedPhrase(err => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- resolve()
- })
- })
- })
- .then(() => {
- dispatch(actions.transitionForward())
- dispatch(actions.hideLoadingIndication())
- })
- .catch(err => {
- dispatch(actions.unlockFailed(err.message))
- dispatch(actions.hideLoadingIndication())
- return Promise.reject(err)
- })
- }
-}
-
-function transitionForward () {
- return {
- type: this.TRANSITION_FORWARD,
- }
-}
-
-function transitionBackward () {
- return {
- type: this.TRANSITION_BACKWARD,
- }
-}
-
-function confirmSeedWords () {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.clearSeedWordCache`)
- return new Promise((resolve, reject) => {
- background.clearSeedWordCache((err, account) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- log.info('Seed word cache cleared. ' + account)
- dispatch(actions.showAccountsPage())
- resolve(account)
- })
- })
- }
-}
-
-function createNewVaultAndRestore (password, seed) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.createNewVaultAndRestore`)
-
- return new Promise((resolve, reject) => {
- background.clearSeedWordCache((err) => {
- if (err) {
- return reject(err)
- }
-
- background.createNewVaultAndRestore(password, seed, (err) => {
- if (err) {
- return reject(err)
- }
-
- resolve()
- })
- })
- })
- .then(() => dispatch(actions.unMarkPasswordForgotten()))
- .then(() => {
- dispatch(actions.showAccountsPage())
- dispatch(actions.hideLoadingIndication())
- })
- .catch(err => {
- dispatch(actions.displayWarning(err.message))
- dispatch(actions.hideLoadingIndication())
- return Promise.reject(err)
- })
- }
-}
-
-function createNewVaultAndKeychain (password) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.createNewVaultAndKeychain`)
-
- return new Promise((resolve, reject) => {
- background.createNewVaultAndKeychain(password, err => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- log.debug(`background.placeSeedWords`)
-
- background.placeSeedWords((err) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- resolve()
- })
- })
- })
- .then(() => forceUpdateMetamaskState(dispatch))
- .then(() => dispatch(actions.hideLoadingIndication()))
- .catch(() => dispatch(actions.hideLoadingIndication()))
- }
-}
-
-function createNewVaultAndGetSeedPhrase (password) {
- return async dispatch => {
- dispatch(actions.showLoadingIndication())
-
- try {
- await createNewVault(password)
- const seedWords = await verifySeedPhrase()
- dispatch(actions.hideLoadingIndication())
- return seedWords
- } catch (error) {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.displayWarning(error.message))
- throw new Error(error.message)
- }
- }
-}
-
-function unlockAndGetSeedPhrase (password) {
- return async dispatch => {
- dispatch(actions.showLoadingIndication())
-
- try {
- await submitPassword(password)
- const seedWords = await verifySeedPhrase()
- await forceUpdateMetamaskState(dispatch)
- dispatch(actions.hideLoadingIndication())
- return seedWords
- } catch (error) {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.displayWarning(error.message))
- throw new Error(error.message)
- }
- }
-}
-
-function revealSeedConfirmation () {
- return {
- type: this.REVEAL_SEED_CONFIRMATION,
- }
-}
-
-function submitPassword (password) {
- return new Promise((resolve, reject) => {
- background.submitPassword(password, error => {
- if (error) {
- return reject(error)
- }
-
- resolve()
- })
- })
-}
-
-function createNewVault (password) {
- return new Promise((resolve, reject) => {
- background.createNewVaultAndKeychain(password, error => {
- if (error) {
- return reject(error)
- }
-
- resolve(true)
- })
- })
-}
-
-function verifyPassword (password) {
- return new Promise((resolve, reject) => {
- background.submitPassword(password, error => {
- if (error) {
- return reject(error)
- }
-
- resolve(true)
- })
- })
-}
-
-function verifySeedPhrase () {
- return new Promise((resolve, reject) => {
- background.verifySeedPhrase((error, seedWords) => {
- if (error) {
- return reject(error)
- }
-
- resolve(seedWords)
- })
- })
-}
-
-function requestRevealSeed (password) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.submitPassword`)
- return new Promise((resolve, reject) => {
- background.submitPassword(password, err => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- log.debug(`background.placeSeedWords`)
- background.placeSeedWords((err, result) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.showNewVaultSeed(result))
- dispatch(actions.hideLoadingIndication())
- resolve()
- })
- })
- })
- }
-}
-
-function requestRevealSeedWords (password) {
- return async dispatch => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.submitPassword`)
-
- try {
- await verifyPassword(password)
- const seedWords = await verifySeedPhrase()
- dispatch(actions.hideLoadingIndication())
- return seedWords
- } catch (error) {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.displayWarning(error.message))
- throw new Error(error.message)
- }
- }
-}
-
-function fetchInfoToSync () {
- return dispatch => {
- log.debug(`background.fetchInfoToSync`)
- return new Promise((resolve, reject) => {
- background.fetchInfoToSync((err, result) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
- resolve(result)
- })
- })
- }
-}
-
-function resetAccount () {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- background.resetAccount((err, account) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- log.info('Transaction history reset for ' + account)
- dispatch(actions.showAccountsPage())
- resolve(account)
- })
- })
- }
-}
-
-function removeAccount (address) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- background.removeAccount(address, (err, account) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- log.info('Account removed: ' + account)
- dispatch(actions.showAccountsPage())
- resolve()
- })
- })
- }
-}
-
-function addNewKeyring (type, opts) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.addNewKeyring`)
- background.addNewKeyring(type, opts, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) return dispatch(actions.displayWarning(err.message))
- dispatch(actions.showAccountsPage())
- })
- }
-}
-
-function importNewAccount (strategy, args) {
- return async (dispatch) => {
- let newState
- dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
- try {
- log.debug(`background.importAccountWithStrategy`)
- await pify(background.importAccountWithStrategy).call(background, strategy, args)
- log.debug(`background.getState`)
- newState = await pify(background.getState).call(background)
- } catch (err) {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.displayWarning(err.message))
- throw err
- }
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.updateMetamaskState(newState))
- if (newState.selectedAddress) {
- dispatch({
- type: actions.SHOW_ACCOUNT_DETAIL,
- value: newState.selectedAddress,
- })
- }
- return newState
- }
-}
-
-function navigateToNewAccountScreen () {
- return {
- type: this.NEW_ACCOUNT_SCREEN,
- }
-}
-
-function addNewAccount () {
- log.debug(`background.addNewAccount`)
- return (dispatch, getState) => {
- const oldIdentities = getState().metamask.identities
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.addNewAccount((err, { identities: newIdentities}) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
- const newAccountAddress = Object.keys(newIdentities).find(address => !oldIdentities[address])
-
- dispatch(actions.hideLoadingIndication())
-
- forceUpdateMetamaskState(dispatch)
- return resolve(newAccountAddress)
- })
- })
- }
-}
-
-function checkHardwareStatus (deviceName, hdPath) {
- log.debug(`background.checkHardwareStatus`, deviceName, hdPath)
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => {
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.hideLoadingIndication())
-
- forceUpdateMetamaskState(dispatch)
- return resolve(unlocked)
- })
- })
- }
-}
-
-function forgetDevice (deviceName) {
- log.debug(`background.forgetDevice`, deviceName)
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.forgetDevice(deviceName, (err, response) => {
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.hideLoadingIndication())
-
- forceUpdateMetamaskState(dispatch)
- return resolve()
- })
- })
- }
-}
-
-function connectHardware (deviceName, page, hdPath) {
- log.debug(`background.connectHardware`, deviceName, page, hdPath)
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.connectHardware(deviceName, page, hdPath, (err, accounts) => {
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.hideLoadingIndication())
-
- forceUpdateMetamaskState(dispatch)
- return resolve(accounts)
- })
- })
- }
-}
-
-function unlockHardwareWalletAccount (index, deviceName, hdPath) {
- log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => {
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.hideLoadingIndication())
- return resolve()
- })
- })
- }
-}
-
-function showInfoPage () {
- return {
- type: actions.SHOW_INFO_PAGE,
- }
-}
-
-function showQrScanner (ROUTE) {
- return (dispatch, getState) => {
- return WebcamUtils.checkStatus()
- .then(status => {
- if (!status.environmentReady) {
- // We need to switch to fullscreen mode to ask for permission
- global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`)
- } else {
- dispatch(actions.showModal({
- name: 'QR_SCANNER',
- }))
- }
- }).catch(e => {
- dispatch(actions.showModal({
- name: 'QR_SCANNER',
- error: true,
- errorType: e.type,
- }))
- })
- }
-}
-
-function setCurrentCurrency (currencyCode) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.setCurrentCurrency`)
- background.setCurrentCurrency(currencyCode, (err, data) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- log.error(err.stack)
- return dispatch(actions.displayWarning(err.message))
- }
- dispatch({
- type: actions.SET_CURRENT_FIAT,
- value: {
- currentCurrency: data.currentCurrency,
- conversionRate: data.conversionRate,
- conversionDate: data.conversionDate,
- },
- })
- })
- }
-}
-
-function signMsg (msgData) {
- log.debug('action - signMsg')
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- log.debug(`actions calling background.signMessage`)
- background.signMessage(msgData, (err, newState) => {
- log.debug('signMessage called back')
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.completedTx(msgData.metamaskId))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function signPersonalMsg (msgData) {
- log.debug('action - signPersonalMsg')
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- log.debug(`actions calling background.signPersonalMessage`)
- background.signPersonalMessage(msgData, (err, newState) => {
- log.debug('signPersonalMessage called back')
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.completedTx(msgData.metamaskId))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function signTypedMsg (msgData) {
- log.debug('action - signTypedMsg')
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- log.debug(`actions calling background.signTypedMessage`)
- background.signTypedMessage(msgData, (err, newState) => {
- log.debug('signTypedMessage called back')
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- log.error(err)
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.completedTx(msgData.metamaskId))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function signTx (txData) {
- return (dispatch) => {
- global.ethQuery.sendTransaction(txData, (err, data) => {
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- })
- dispatch(actions.showConfTxPage({}))
- }
-}
-
-function setGasLimit (gasLimit) {
- return {
- type: actions.UPDATE_GAS_LIMIT,
- value: gasLimit,
- }
-}
-
-function setGasPrice (gasPrice) {
- return {
- type: actions.UPDATE_GAS_PRICE,
- value: gasPrice,
- }
-}
-
-function setGasTotal (gasTotal) {
- return {
- type: actions.UPDATE_GAS_TOTAL,
- value: gasTotal,
- }
-}
-
-function updateGasData ({
- gasPrice,
- blockGasLimit,
- recentBlocks,
- selectedAddress,
- selectedToken,
- to,
- value,
- data,
-}) {
- return (dispatch) => {
- dispatch(actions.gasLoadingStarted())
- return estimateGas({
- estimateGasMethod: background.estimateGas,
- blockGasLimit,
- selectedAddress,
- selectedToken,
- to,
- value,
- estimateGasPrice: gasPrice,
- data,
- })
- .then(gas => {
- dispatch(actions.setGasLimit(gas))
- dispatch(gasDuck.setCustomGasLimit(gas))
- dispatch(updateSendErrors({ gasLoadingError: null }))
- dispatch(actions.gasLoadingFinished())
- })
- .catch(err => {
- log.error(err)
- dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
- dispatch(actions.gasLoadingFinished())
- })
- }
-}
-
-function gasLoadingStarted () {
- return {
- type: actions.GAS_LOADING_STARTED,
- }
-}
-
-function gasLoadingFinished () {
- return {
- type: actions.GAS_LOADING_FINISHED,
- }
-}
-
-function updateSendTokenBalance ({
- selectedToken,
- tokenContract,
- address,
-}) {
- return (dispatch) => {
- const tokenBalancePromise = tokenContract
- ? tokenContract.balanceOf(address)
- : Promise.resolve()
- return tokenBalancePromise
- .then(usersToken => {
- if (usersToken) {
- const newTokenBalance = calcTokenBalance({ selectedToken, usersToken })
- dispatch(setSendTokenBalance(newTokenBalance))
- }
- })
- .catch(err => {
- log.error(err)
- updateSendErrors({ tokenBalance: 'tokenBalanceError' })
- })
- }
-}
-
-function updateSendErrors (errorObject) {
- return {
- type: actions.UPDATE_SEND_ERRORS,
- value: errorObject,
- }
-}
-
-function updateSendWarnings (warningObject) {
- return {
- type: actions.UPDATE_SEND_WARNINGS,
- value: warningObject,
- }
-}
-
-function setSendTokenBalance (tokenBalance) {
- return {
- type: actions.UPDATE_SEND_TOKEN_BALANCE,
- value: tokenBalance,
- }
-}
-
-function updateSendHexData (value) {
- return {
- type: actions.UPDATE_SEND_HEX_DATA,
- value,
- }
-}
-
-function updateSendTo (to, nickname = '') {
- return {
- type: actions.UPDATE_SEND_TO,
- value: { to, nickname },
- }
-}
-
-function updateSendAmount (amount) {
- return {
- type: actions.UPDATE_SEND_AMOUNT,
- value: amount,
- }
-}
-
-function updateSendMemo (memo) {
- return {
- type: actions.UPDATE_SEND_MEMO,
- value: memo,
- }
-}
-
-function setMaxModeTo (bool) {
- return {
- type: actions.UPDATE_MAX_MODE,
- value: bool,
- }
-}
-
-function updateSend (newSend) {
- return {
- type: actions.UPDATE_SEND,
- value: newSend,
- }
-}
-
-function clearSend () {
- return {
- type: actions.CLEAR_SEND,
- }
-}
-
-
-function sendTx (txData) {
- log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
- return (dispatch, getState) => {
- log.debug(`actions calling background.approveTransaction`)
- background.approveTransaction(txData.id, (err) => {
- if (err) {
- dispatch(actions.txError(err))
- return log.error(err.message)
- }
- dispatch(actions.completedTx(txData.id))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
- })
- }
-}
-
-function signTokenTx (tokenAddress, toAddress, amount, txData) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- const token = global.eth.contract(abi).at(tokenAddress)
- token.transfer(toAddress, ethUtil.addHexPrefix(amount), txData)
- .catch(err => {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.displayWarning(err.message))
- })
- dispatch(actions.showConfTxPage({}))
- }
-}
-
-function updateTransaction (txData) {
- log.info('actions: updateTx: ' + JSON.stringify(txData))
- return dispatch => {
- log.debug(`actions calling background.updateTx`)
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- background.updateTransaction(txData, (err) => {
- dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
- if (err) {
- dispatch(actions.txError(err))
- dispatch(actions.goHome())
- log.error(err.message)
- return reject(err)
- }
-
- resolve(txData)
- })
- })
- .then(() => updateMetamaskStateFromBackground())
- .then(newState => dispatch(actions.updateMetamaskState(newState)))
- .then(() => {
- dispatch(actions.showConfTxPage({ id: txData.id }))
- dispatch(actions.hideLoadingIndication())
- return txData
- })
- }
-}
-
-function updateAndApproveTx (txData) {
- log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
- return (dispatch, getState) => {
- log.debug(`actions calling background.updateAndApproveTx`)
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- background.updateAndApproveTransaction(txData, err => {
- dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
- dispatch(actions.clearSend())
-
- if (err) {
- dispatch(actions.txError(err))
- dispatch(actions.goHome())
- log.error(err.message)
- reject(err)
- }
-
- resolve(txData)
- })
- })
- .then(() => updateMetamaskStateFromBackground())
- .then(newState => dispatch(actions.updateMetamaskState(newState)))
- .then(() => {
- dispatch(actions.clearSend())
- dispatch(actions.completedTx(txData.id))
- dispatch(actions.hideLoadingIndication())
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return txData
- })
- .catch((err) => {
- dispatch(actions.hideLoadingIndication())
- return Promise.reject(err)
- })
- }
-}
-
-function completedTx (id) {
- return {
- type: actions.COMPLETED_TX,
- value: id,
- }
-}
-
-function updateTransactionParams (id, txParams) {
- return {
- type: actions.UPDATE_TRANSACTION_PARAMS,
- id,
- value: txParams,
- }
-}
-
-function txError (err) {
- return {
- type: actions.TRANSACTION_ERROR,
- message: err.message,
- }
-}
-
-function cancelMsg (msgData) {
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- log.debug(`background.cancelMessage`)
- background.cancelMessage(msgData.id, (err, newState) => {
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- return reject(err)
- }
-
- dispatch(actions.completedTx(msgData.id))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function cancelPersonalMsg (msgData) {
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- const id = msgData.id
- background.cancelPersonalMessage(id, (err, newState) => {
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- return reject(err)
- }
-
- dispatch(actions.completedTx(id))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function cancelTypedMsg (msgData) {
- return (dispatch, getState) => {
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- const id = msgData.id
- background.cancelTypedMessage(id, (err, newState) => {
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- return reject(err)
- }
-
- dispatch(actions.completedTx(id))
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return resolve(msgData)
- })
- })
- }
-}
-
-function cancelTx (txData) {
- return (dispatch, getState) => {
- log.debug(`background.cancelTransaction`)
- dispatch(actions.showLoadingIndication())
-
- return new Promise((resolve, reject) => {
- background.cancelTransaction(txData.id, err => {
- if (err) {
- return reject(err)
- }
-
- resolve()
- })
- })
- .then(() => updateMetamaskStateFromBackground())
- .then(newState => dispatch(actions.updateMetamaskState(newState)))
- .then(() => {
- dispatch(actions.clearSend())
- dispatch(actions.completedTx(txData.id))
- dispatch(actions.hideLoadingIndication())
-
- if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
- !hasUnconfirmedTransactions(getState())) {
- return global.platform.closeCurrentWindow()
- }
-
- return 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) => {
- background.cancelTransaction(txData.id, () => {
- dispatch(actions.completedTx(txData.id))
- i === txsData.length - 1 ? dispatch(actions.goHome()) : null
- })
- })
- }
-}
-//
-// initialize screen
-//
-
-function showCreateVault () {
- return {
- type: actions.SHOW_CREATE_VAULT,
- }
-}
-
-function showRestoreVault () {
- return {
- type: actions.SHOW_RESTORE_VAULT,
- }
-}
-
-function markPasswordForgotten () {
- return (dispatch) => {
- return background.markPasswordForgotten(() => {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.forgotPassword())
- forceUpdateMetamaskState(dispatch)
- })
- }
-}
-
-function unMarkPasswordForgotten () {
- return dispatch => {
- return new Promise(resolve => {
- background.unMarkPasswordForgotten(() => {
- dispatch(actions.forgotPassword(false))
- resolve()
- })
- })
- .then(() => forceUpdateMetamaskState(dispatch))
- }
-}
-
-function forgotPassword (forgotPasswordState = true) {
- return {
- type: actions.FORGOT_PASSWORD,
- value: forgotPasswordState,
- }
-}
-
-function showInitializeMenu () {
- return {
- type: actions.SHOW_INIT_MENU,
- }
-}
-
-function showImportPage () {
- return {
- type: actions.SHOW_IMPORT_PAGE,
- }
-}
-
-function showNewAccountPage (formToSelect) {
- return {
- type: actions.SHOW_NEW_ACCOUNT_PAGE,
- formToSelect,
- }
-}
-
-function setNewAccountForm (formToSelect) {
- return {
- type: actions.SET_NEW_ACCOUNT_FORM,
- formToSelect,
- }
-}
-
-function createNewVaultInProgress () {
- return {
- type: actions.CREATE_NEW_VAULT_IN_PROGRESS,
- }
-}
-
-function showNewVaultSeed (seed) {
- return {
- type: actions.SHOW_NEW_VAULT_SEED,
- value: seed,
- }
-}
-
-function closeWelcomeScreen () {
- return {
- type: actions.CLOSE_WELCOME_SCREEN,
- }
-}
-
-function backToUnlockView () {
- return {
- type: actions.BACK_TO_UNLOCK_VIEW,
- }
-}
-
-function showNewKeychain () {
- return {
- type: actions.SHOW_NEW_KEYCHAIN,
- }
-}
-
-//
-// unlock screen
-//
-
-function unlockInProgress () {
- return {
- type: actions.UNLOCK_IN_PROGRESS,
- }
-}
-
-function unlockFailed (message) {
- return {
- type: actions.UNLOCK_FAILED,
- value: message,
- }
-}
-
-function unlockSucceeded (message) {
- return {
- type: actions.UNLOCK_SUCCEEDED,
- value: message,
- }
-}
-
-function unlockMetamask (account) {
- return {
- type: actions.UNLOCK_METAMASK,
- value: account,
- }
-}
-
-function updateMetamaskState (newState) {
- return {
- type: actions.UPDATE_METAMASK_STATE,
- value: newState,
- }
-}
-
-const backgroundSetLocked = () => {
- return new Promise((resolve, reject) => {
- background.setLocked(error => {
- if (error) {
- return reject(error)
- }
- resolve()
- })
- })
-}
-
-const updateMetamaskStateFromBackground = () => {
- log.debug(`background.getState`)
-
- return new Promise((resolve, reject) => {
- background.getState((error, newState) => {
- if (error) {
- return reject(error)
- }
-
- resolve(newState)
- })
- })
-}
-
-function lockMetamask () {
- log.debug(`background.setLocked`)
-
- return dispatch => {
- dispatch(actions.showLoadingIndication())
-
- return backgroundSetLocked()
- .then(() => updateMetamaskStateFromBackground())
- .catch(error => {
- dispatch(actions.displayWarning(error.message))
- return Promise.reject(error)
- })
- .then(newState => {
- dispatch(actions.updateMetamaskState(newState))
- dispatch(actions.hideLoadingIndication())
- dispatch({ type: actions.LOCK_METAMASK })
- })
- .catch(() => {
- dispatch(actions.hideLoadingIndication())
- dispatch({ type: actions.LOCK_METAMASK })
- })
- }
-}
-
-function setCurrentAccountTab (newTabName) {
- log.debug(`background.setCurrentAccountTab: ${newTabName}`)
- return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName)
-}
-
-function setSelectedToken (tokenAddress) {
- return {
- type: actions.SET_SELECTED_TOKEN,
- value: tokenAddress || null,
- }
-}
-
-function setSelectedAddress (address) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.setSelectedAddress`)
- background.setSelectedAddress(address, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- })
- }
-}
-
-function showAccountDetail (address) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.setSelectedAddress`)
- background.setSelectedAddress(address, (err, tokens) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- dispatch(updateTokens(tokens))
- dispatch({
- type: actions.SHOW_ACCOUNT_DETAIL,
- value: address,
- })
- dispatch(actions.setSelectedToken())
- })
- }
-}
-
-function backToAccountDetail (address) {
- return {
- type: actions.BACK_TO_ACCOUNT_DETAIL,
- value: address,
- }
-}
-
-function showAccountsPage () {
- return {
- type: actions.SHOW_ACCOUNTS_PAGE,
- }
-}
-
-function showConfTxPage ({transForward = true, id}) {
- return {
- type: actions.SHOW_CONF_TX_PAGE,
- transForward,
- id,
- }
-}
-
-function nextTx () {
- return {
- type: actions.NEXT_TX,
- }
-}
-
-function viewPendingTx (txId) {
- return {
- type: actions.VIEW_PENDING_TX,
- value: txId,
- }
-}
-
-function previousTx () {
- return {
- type: actions.PREVIOUS_TX,
- }
-}
-
-function editTx (txId) {
- return {
- type: actions.EDIT_TX,
- value: txId,
- }
-}
-
-function showConfigPage (transitionForward = true) {
- return {
- type: actions.SHOW_CONFIG_PAGE,
- value: transitionForward,
- }
-}
-
-function showAddTokenPage (transitionForward = true) {
- return {
- type: actions.SHOW_ADD_TOKEN_PAGE,
- value: transitionForward,
- }
-}
-
-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, image, (err, tokens) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- reject(err)
- }
- dispatch(actions.updateTokens(tokens))
- resolve(tokens)
- })
- })
- }
-}
-
-function removeToken (address) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.removeToken(address, (err, tokens) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- reject(err)
- }
- dispatch(actions.updateTokens(tokens))
- resolve(tokens)
- })
- })
- }
-}
-
-function addTokens (tokens) {
- return dispatch => {
- if (Array.isArray(tokens)) {
- dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens[0])))
- return Promise.all(tokens.map(({ address, symbol, decimals }) => (
- dispatch(addToken(address, symbol, decimals))
- )))
- } else {
- dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens)))
- return Promise.all(
- Object
- .entries(tokens)
- .map(([_, { address, symbol, decimals }]) => (
- dispatch(addToken(address, symbol, decimals))
- ))
- )
- }
- }
-}
-
-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 addKnownMethodData (fourBytePrefix, methodData) {
- return (dispatch) => {
- background.addKnownMethodData(fourBytePrefix, methodData)
- }
-}
-
-function updateTokens (newTokens) {
- return {
- type: actions.UPDATE_TOKENS,
- newTokens,
- }
-}
-
-function clearPendingTokens () {
- return {
- type: actions.CLEAR_PENDING_TOKENS,
- }
-}
-
-function goBackToInitView () {
- return {
- type: actions.BACK_TO_INIT_MENU,
- }
-}
-
-//
-// notice
-//
-
-function markNoticeRead (notice) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.markNoticeRead`)
- return new Promise((resolve, reject) => {
- background.markNoticeRead(notice, (err, notice) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- if (notice) {
- dispatch(actions.showNotice(notice))
- resolve(true)
- } else {
- dispatch(actions.clearNotices())
- resolve(false)
- }
- })
- })
- }
-}
-
-function showNotice (notice) {
- return {
- type: actions.SHOW_NOTICE,
- value: notice,
- }
-}
-
-function clearNotices () {
- return {
- type: actions.CLEAR_NOTICES,
- }
-}
-
-function markAccountsFound () {
- log.debug(`background.markAccountsFound`)
- return callBackgroundThenUpdate(background.markAccountsFound)
-}
-
-function retryTransaction (txId, gasPrice) {
- log.debug(`background.retryTransaction`)
- let newTxId
-
- return dispatch => {
- return new Promise((resolve, reject) => {
- background.retryTransaction(txId, gasPrice, (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)
- }
-}
-
-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)
- }
-}
-
-function createSpeedUpTransaction (txId, customGasPrice) {
- log.debug('background.createSpeedUpTransaction')
- let newTx
-
- return dispatch => {
- return new Promise((resolve, reject) => {
- background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- reject(err)
- }
-
- const { selectedAddressTxList } = newState
- newTx = selectedAddressTxList[selectedAddressTxList.length - 1]
- resolve(newState)
- })
- })
- .then(newState => dispatch(actions.updateMetamaskState(newState)))
- .then(() => newTx)
- }
-}
-
-//
-// config
-//
-
-function setProviderType (type) {
- return (dispatch, getState) => {
- const { type: currentProviderType } = getState().metamask.provider
- log.debug(`background.setProviderType`, type)
- background.setProviderType(type, (err, result) => {
- if (err) {
- log.error(err)
- return dispatch(actions.displayWarning('Had a problem changing networks!'))
- }
- dispatch(setPreviousProvider(currentProviderType))
- dispatch(actions.updateProviderType(type))
- dispatch(actions.setSelectedToken())
- })
-
- }
-}
-
-function updateProviderType (type) {
- return {
- type: actions.SET_PROVIDER_TYPE,
- value: type,
- }
-}
-
-function setPreviousProvider (type) {
- return {
- type: actions.SET_PREVIOUS_PROVIDER,
- value: type,
- }
-}
-
-function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) {
- return (dispatch) => {
- log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`)
- background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => {
- if (err) {
- log.error(err)
- return dispatch(actions.displayWarning('Had a problem changing networks!'))
- }
- dispatch({
- type: actions.SET_RPC_TARGET,
- value: newRpc,
- })
- })
- }
-}
-
-function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) {
- return (dispatch) => {
- log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
- background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => {
- if (err) {
- log.error(err)
- 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())
- })
- }
-}
-
-// Calls the addressBookController to add a new address.
-function addToAddressBook (recipient, nickname = '') {
- log.debug(`background.addToAddressBook`)
- return (dispatch) => {
- background.setAddressBook(recipient, nickname, (err, result) => {
- if (err) {
- log.error(err)
- return dispatch(self.displayWarning('Address book failed to update'))
- }
- })
- }
-}
-
-function useEtherscanProvider () {
- log.debug(`background.useEtherscanProvider`)
- background.useEtherscanProvider()
- return {
- type: actions.USE_ETHERSCAN_PROVIDER,
- }
-}
-
-function showNetworkDropdown () {
- return {
- type: actions.NETWORK_DROPDOWN_OPEN,
- }
-}
-
-function hideNetworkDropdown () {
- return {
- type: actions.NETWORK_DROPDOWN_CLOSE,
- }
-}
-
-
-function showModal (payload) {
- return {
- type: actions.MODAL_OPEN,
- payload,
- }
-}
-
-function hideModal (payload) {
- return {
- type: actions.MODAL_CLOSE,
- payload,
- }
-}
-
-function showSidebar ({ transitionName, type, props }) {
- return {
- type: actions.SIDEBAR_OPEN,
- value: {
- transitionName,
- type,
- props,
- },
- }
-}
-
-function hideSidebar () {
- return {
- type: actions.SIDEBAR_CLOSE,
- }
-}
-
-function showAlert (msg) {
- return {
- type: actions.ALERT_OPEN,
- value: msg,
- }
-}
-
-function hideAlert () {
- return {
- type: actions.ALERT_CLOSE,
- }
-}
-
-/**
- * This action will receive two types of values via qrCodeData
- * an object with the following structure {type, values}
- * or null (used to clear the previous value)
- */
-function qrCodeDetected (qrCodeData) {
- return {
- type: actions.QR_CODE_DETECTED,
- value: qrCodeData,
- }
-}
-
-function showLoadingIndication (message) {
- return {
- type: actions.SHOW_LOADING,
- value: message,
- }
-}
-
-function setHardwareWalletDefaultHdPath ({ device, path }) {
- return {
- type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
- value: {device, path},
- }
-}
-
-function hideLoadingIndication () {
- return {
- type: actions.HIDE_LOADING,
- }
-}
-
-function showSubLoadingIndication () {
- return {
- type: actions.SHOW_SUB_LOADING_INDICATION,
- }
-}
-
-function hideSubLoadingIndication () {
- return {
- type: actions.HIDE_SUB_LOADING_INDICATION,
- }
-}
-
-function displayWarning (text) {
- return {
- type: actions.DISPLAY_WARNING,
- value: text,
- }
-}
-
-function hideWarning () {
- return {
- type: actions.HIDE_WARNING,
- }
-}
-
-function requestExportAccount () {
- return {
- type: actions.REQUEST_ACCOUNT_EXPORT,
- }
-}
-
-function exportAccount (password, address) {
- var self = this
-
- return function (dispatch) {
- dispatch(self.showLoadingIndication())
-
- log.debug(`background.submitPassword`)
- return new Promise((resolve, reject) => {
- background.submitPassword(password, function (err) {
- if (err) {
- log.error('Error in submiting password.')
- dispatch(self.hideLoadingIndication())
- dispatch(self.displayWarning('Incorrect Password.'))
- return reject(err)
- }
- log.debug(`background.exportAccount`)
- return background.exportAccount(address, function (err, result) {
- dispatch(self.hideLoadingIndication())
-
- if (err) {
- log.error(err)
- dispatch(self.displayWarning('Had a problem exporting the account.'))
- return reject(err)
- }
-
- // dispatch(self.exportAccountComplete())
- dispatch(self.showPrivateKey(result))
-
- return resolve(result)
- })
- })
- })
- }
-}
-
-function exportAccountComplete () {
- return {
- type: actions.EXPORT_ACCOUNT,
- }
-}
-
-function showPrivateKey (key) {
- return {
- type: actions.SHOW_PRIVATE_KEY,
- value: key,
- }
-}
-
-function setAccountLabel (account, label) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.setAccountLabel`)
-
- return new Promise((resolve, reject) => {
- background.setAccountLabel(account, label, (err) => {
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- dispatch(actions.displayWarning(err.message))
- reject(err)
- }
-
- dispatch({
- type: actions.SET_ACCOUNT_LABEL,
- value: { account, label },
- })
-
- resolve(account)
- })
- })
- }
-}
-
-function showSendPage () {
- return {
- type: actions.SHOW_SEND_PAGE,
- }
-}
-
-function showSendTokenPage () {
- return {
- type: actions.SHOW_SEND_TOKEN_PAGE,
- }
-}
-
-function buyEth (opts) {
- return (dispatch) => {
- const url = getBuyEthUrl(opts)
- global.platform.openWindow({ url })
- dispatch({
- type: actions.BUY_ETH,
- })
- }
-}
-
-function onboardingBuyEthView (address) {
- return {
- type: actions.ONBOARDING_BUY_ETH_VIEW,
- value: address,
- }
-}
-
-function buyEthView (address) {
- return {
- type: actions.BUY_ETH_VIEW,
- value: address,
- }
-}
-
-function coinBaseSubview () {
- return {
- type: actions.COINBASE_SUBVIEW,
- }
-}
-
-function pairUpdate (coin) {
- return (dispatch) => {
- dispatch(actions.showSubLoadingIndication())
- dispatch(actions.hideWarning())
- shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
- dispatch(actions.hideSubLoadingIndication())
- if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
- dispatch({
- type: actions.PAIR_UPDATE,
- value: {
- marketinfo: mktResponse,
- },
- })
- })
- }
-}
-
-function shapeShiftSubview (network) {
- var pair = 'btc_eth'
- return (dispatch) => {
- dispatch(actions.showSubLoadingIndication())
- shapeShiftRequest('marketinfo', {pair}, (mktResponse) => {
- shapeShiftRequest('getcoins', {}, (response) => {
- dispatch(actions.hideSubLoadingIndication())
- if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
- dispatch({
- type: actions.SHAPESHIFT_SUBVIEW,
- value: {
- marketinfo: mktResponse,
- coinOptions: response,
- },
- })
- })
- })
- }
-}
-
-function coinShiftRquest (data, marketData) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- shapeShiftRequest('shift', { method: 'POST', data}, (response) => {
- dispatch(actions.hideLoadingIndication())
- if (response.error) return dispatch(actions.displayWarning(response.error))
- var message = `
- Deposit your ${response.depositType} to the address below:`
- log.debug(`background.createShapeShiftTx`)
- background.createShapeShiftTx(response.deposit, response.depositType)
- dispatch(actions.showQrView(response.deposit, [message].concat(marketData)))
- })
- }
-}
-
-function buyWithShapeShift (data) {
- return dispatch => new Promise((resolve, reject) => {
- shapeShiftRequest('shift', { method: 'POST', data}, (response) => {
- if (response.error) {
- return reject(response.error)
- }
- background.createShapeShiftTx(response.deposit, response.depositType)
- return resolve(response)
- })
- })
-}
-
-function showQrView (data, message) {
- return {
- type: actions.SHOW_QR_VIEW,
- value: {
- message: message,
- data: data,
- },
- }
-}
-function reshowQrCode (data, coin) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
- if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
-
- var message = [
- `Deposit your ${coin} to the address below:`,
- `Deposit Limit: ${mktResponse.limit}`,
- `Deposit Minimum:${mktResponse.minimum}`,
- ]
-
- dispatch(actions.hideLoadingIndication())
- return dispatch(actions.showQrView(data, message))
- // return dispatch(actions.showModal({
- // name: 'SHAPESHIFT_DEPOSIT_TX',
- // Qr: { data, message },
- // }))
- })
- }
-}
-
-function shapeShiftRequest (query, options, cb) {
- var queryResponse, method
- !options ? options = {} : null
- options.method ? method = options.method : method = 'GET'
-
- var requestListner = function (request) {
- try {
- queryResponse = JSON.parse(this.responseText)
- cb ? cb(queryResponse) : null
- return queryResponse
- } catch (e) {
- cb ? cb({error: e}) : null
- return e
- }
- }
-
- var shapShiftReq = new XMLHttpRequest()
- shapShiftReq.addEventListener('load', requestListner)
- shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true)
-
- if (options.method === 'POST') {
- var jsonObj = JSON.stringify(options.data)
- shapShiftReq.setRequestHeader('Content-Type', 'application/json')
- return shapShiftReq.send(jsonObj)
- } else {
- return shapShiftReq.send()
- }
-}
-
-function setFeatureFlag (feature, activated, notificationType) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.setFeatureFlag(feature, activated, (err, updatedFeatureFlags) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
- dispatch(actions.updateFeatureFlags(updatedFeatureFlags))
- notificationType && dispatch(actions.showModal({ name: notificationType }))
- resolve(updatedFeatureFlags)
- })
- })
- }
-}
-
-function updateFeatureFlags (updatedFeatureFlags) {
- return {
- type: actions.UPDATE_FEATURE_FLAGS,
- value: updatedFeatureFlags,
- }
-}
-
-function setPreference (preference, value) {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.setPreference(preference, value, (err, updatedPreferences) => {
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.updatePreferences(updatedPreferences))
- resolve(updatedPreferences)
- })
- })
- }
-}
-
-function updatePreferences (value) {
- return {
- type: actions.UPDATE_PREFERENCES,
- value,
- }
-}
-
-function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) {
- return setPreference('useNativeCurrencyAsPrimaryCurrency', value)
-}
-
-function setShowFiatConversionOnTestnetsPreference (value) {
- return setPreference('showFiatInTestnets', value)
-}
-
-function setCompletedOnboarding () {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.markAllNoticesRead(err => {
-
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.clearNotices())
- resolve(false)
- })
- })
- .then(() => {
- return new Promise((resolve, reject) => {
- background.completeOnboarding(err => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.completeOnboarding())
- dispatch(actions.hideLoadingIndication())
- resolve()
- })
- })
- })
- }
-}
-
-function completeOnboarding () {
- return {
- type: actions.COMPLETE_ONBOARDING,
- }
-}
-
-function setCompletedUiMigration () {
- return dispatch => {
- dispatch(actions.showLoadingIndication())
- return new Promise((resolve, reject) => {
- background.completeUiMigration(err => {
- dispatch(actions.hideLoadingIndication())
-
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.completeUiMigration())
- resolve()
- })
- })
- }
-}
-
-function completeUiMigration () {
- return {
- type: actions.COMPLETE_UI_MIGRATION,
- }
-}
-
-function setNetworkNonce (networkNonce) {
- return {
- type: actions.SET_NETWORK_NONCE,
- value: networkNonce,
- }
-}
-
-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)
- })
- })
- }
-}
-
-function setMouseUserState (isMouseUser) {
- return {
- type: actions.SET_MOUSE_USER_STATE,
- value: isMouseUser,
- }
-}
-
-// Call Background Then Update
-//
-// A function generator for a common pattern wherein:
-// We show loading indication.
-// We call a background method.
-// We hide loading indication.
-// If it errored, we show a warning.
-// If it didn't, we update the state.
-function callBackgroundThenUpdateNoSpinner (method, ...args) {
- return (dispatch) => {
- method.call(background, ...args, (err) => {
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- forceUpdateMetamaskState(dispatch)
- })
- }
-}
-
-function callBackgroundThenUpdate (method, ...args) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- method.call(background, ...args, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- forceUpdateMetamaskState(dispatch)
- })
- }
-}
-
-function forceUpdateMetamaskState (dispatch) {
- log.debug(`background.getState`)
- return new Promise((resolve, reject) => {
- background.getState((err, newState) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch(actions.updateMetamaskState(newState))
- resolve(newState)
- })
- })
-}
-
-function toggleAccountMenu () {
- return {
- type: actions.TOGGLE_ACCOUNT_MENU,
- }
-}
-
-function setParticipateInMetaMetrics (val) {
- return (dispatch) => {
- log.debug(`background.setParticipateInMetaMetrics`)
- return new Promise((resolve, reject) => {
- background.setParticipateInMetaMetrics(val, (err, metaMetricsId) => {
- log.debug(err)
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch({
- type: actions.SET_PARTICIPATE_IN_METAMETRICS,
- value: val,
- })
-
- resolve([val, metaMetricsId])
- })
- })
- }
-}
-
-function setMetaMetricsSendCount (val) {
- return (dispatch) => {
- log.debug(`background.setMetaMetricsSendCount`)
- return new Promise((resolve, reject) => {
- background.setMetaMetricsSendCount(val, (err) => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
-
- dispatch({
- type: actions.SET_METAMETRICS_SEND_COUNT,
- value: val,
- })
-
- resolve(val)
- })
- })
- }
-}
-
-function setUseBlockie (val) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- log.debug(`background.setUseBlockie`)
- background.setUseBlockie(val, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- })
- dispatch({
- type: actions.SET_USE_BLOCKIE,
- value: val,
- })
- }
-}
-
-function updateCurrentLocale (key) {
- return (dispatch) => {
- dispatch(actions.showLoadingIndication())
- return fetchLocale(key)
- .then((localeMessages) => {
- log.debug(`background.setCurrentLocale`)
- background.setCurrentLocale(key, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- dispatch(actions.setCurrentLocale(key))
- dispatch(actions.setLocaleMessages(localeMessages))
- })
- })
- }
-}
-
-function setCurrentLocale (key) {
- return {
- type: actions.SET_CURRENT_LOCALE,
- value: key,
- }
-}
-
-function setLocaleMessages (localeMessages) {
- return {
- type: actions.SET_LOCALE_MESSAGES,
- value: localeMessages,
- }
-}
-
-function updateNetworkEndpointType (networkEndpointType) {
- return {
- type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,
- value: networkEndpointType,
- }
-}
-
-function setPendingTokens (pendingTokens) {
- const { customToken = {}, selectedTokens = {} } = pendingTokens
- const { address, symbol, decimals } = customToken
- const tokens = address && symbol && decimals
- ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
- : selectedTokens
-
- return {
- type: actions.SET_PENDING_TOKENS,
- payload: tokens,
- }
-}
-
-function approveProviderRequest (tabID) {
- return (dispatch) => {
- background.approveProviderRequest(tabID)
- }
-}
-
-function rejectProviderRequest (tabID) {
- return (dispatch) => {
- background.rejectProviderRequest(tabID)
- }
-}
-
-function clearApprovedOrigins () {
- return (dispatch) => {
- background.clearApprovedOrigins()
- }
-}
-
-function setFirstTimeFlowType (type) {
- return (dispatch) => {
- log.debug(`background.setFirstTimeFlowType`)
- background.setFirstTimeFlowType(type, (err) => {
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- })
- dispatch({
- type: actions.SET_FIRST_TIME_FLOW_TYPE,
- value: type,
- })
- }
-}
diff --git a/ui/app/app.js b/ui/app/app.js
deleted file mode 100644
index efec4e49a..000000000
--- a/ui/app/app.js
+++ /dev/null
@@ -1,441 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import { connect } from 'react-redux'
-import { Route, Switch, withRouter, matchPath } from 'react-router-dom'
-import { compose } from 'recompose'
-import actions from './actions'
-import log from 'loglevel'
-import { getMetaMaskAccounts, getNetworkIdentifier } from './selectors'
-
-// init
-import FirstTimeFlow from './components/pages/first-time-flow'
-// accounts
-const SendTransactionScreen = require('./components/send/send.container')
-const ConfirmTransaction = require('./components/pages/confirm-transaction')
-
-// slideout menu
-const Sidebar = require('./components/sidebars').default
-const { WALLET_VIEW_SIDEBAR } = require('./components/sidebars/sidebar.constants')
-
-// other views
-import Home from './components/pages/home'
-import Settings from './components/pages/settings'
-import Authenticated from './higher-order-components/authenticated'
-import Initialized from './higher-order-components/initialized'
-import Lock from './components/pages/lock'
-import UiMigrationAnnouncement from './components/ui-migration-annoucement'
-const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
-const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
-const MobileSyncPage = require('./components/pages/mobile-sync')
-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 LoadingNetwork = require('./components/loading-network-screen').default
-const NetworkDropdown = require('./components/dropdowns/network-dropdown')
-import AccountMenu from './components/account-menu'
-
-// Global Modals
-const Modal = require('./components/modals/index').Modal
-// Global Alert
-const Alert = require('./components/alert')
-
-import AppHeader from './components/app-header'
-import UnlockPage from './components/pages/unlock-page'
-
-import {
- submittedPendingTransactionsSelector,
-} from './selectors/transactions'
-
-// Routes
-import {
- DEFAULT_ROUTE,
- LOCK_ROUTE,
- UNLOCK_ROUTE,
- SETTINGS_ROUTE,
- REVEAL_SEED_ROUTE,
- MOBILE_SYNC_ROUTE,
- RESTORE_VAULT_ROUTE,
- ADD_TOKEN_ROUTE,
- CONFIRM_ADD_TOKEN_ROUTE,
- CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
- NEW_ACCOUNT_ROUTE,
- SEND_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- INITIALIZE_ROUTE,
- INITIALIZE_UNLOCK_ROUTE,
- NOTICE_ROUTE,
-} from './routes'
-
-// enums
-import {
- ENVIRONMENT_TYPE_NOTIFICATION,
- ENVIRONMENT_TYPE_POPUP,
-} from '../../app/scripts/lib/enums'
-
-class App extends Component {
- componentWillMount () {
- const { currentCurrency, setCurrentCurrencyToUSD } = this.props
-
- if (!currentCurrency) {
- setCurrentCurrencyToUSD()
- }
-
- this.props.history.listen((locationObj, action) => {
- if (action === 'PUSH') {
- const url = `&url=${encodeURIComponent('http://www.metamask.io/metametrics' + locationObj.pathname)}`
- this.context.metricsEvent({}, {
- currentPath: '',
- pathname: locationObj.pathname,
- url,
- pageOpts: {
- hideDimensions: true,
- },
- })
- }
- })
- }
-
- renderRoutes () {
- return (
- <Switch>
- <Route path={LOCK_ROUTE} component={Lock} exact />
- <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} />
- <Initialized path={UNLOCK_ROUTE} component={UnlockPage} exact />
- <Initialized path={RESTORE_VAULT_ROUTE} component={RestoreVaultPage} exact />
- <Authenticated path={REVEAL_SEED_ROUTE} component={RevealSeedConfirmation} exact />
- <Authenticated path={MOBILE_SYNC_ROUTE} component={MobileSyncPage} exact />
- <Authenticated path={SETTINGS_ROUTE} component={Settings} />
- <Authenticated path={NOTICE_ROUTE} component={NoticeScreen} exact />
- <Authenticated path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransaction} />
- <Authenticated path={SEND_ROUTE} component={SendTransactionScreen} exact />
- <Authenticated path={ADD_TOKEN_ROUTE} component={AddTokenPage} exact />
- <Authenticated path={CONFIRM_ADD_TOKEN_ROUTE} component={ConfirmAddTokenPage} exact />
- <Authenticated path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE} component={ConfirmAddSuggestedTokenPage} exact />
- <Authenticated path={NEW_ACCOUNT_ROUTE} component={CreateAccountPage} />
- <Authenticated path={DEFAULT_ROUTE} component={Home} exact />
- </Switch>
- )
- }
-
- onInitializationUnlockPage () {
- const { location } = this.props
- return Boolean(matchPath(location.pathname, { path: INITIALIZE_UNLOCK_ROUTE, exact: true }))
- }
-
- onConfirmPage () {
- const { location } = this.props
- return Boolean(matchPath(location.pathname, { path: CONFIRM_TRANSACTION_ROUTE, exact: false }))
- }
-
- hasProviderRequests () {
- const { providerRequests } = this.props
- return Array.isArray(providerRequests) && providerRequests.length > 0
- }
-
- hideAppHeader () {
- const { location } = this.props
-
- const isInitializing = Boolean(matchPath(location.pathname, {
- path: INITIALIZE_ROUTE, exact: false,
- }))
-
- if (isInitializing && !this.onInitializationUnlockPage()) {
- return true
- }
-
- if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
- return true
- }
-
- if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP) {
- return this.onConfirmPage() || this.hasProviderRequests()
- }
- }
-
- render () {
- const {
- isLoading,
- alertMessage,
- loadingMessage,
- network,
- provider,
- frequentRpcListDetail,
- currentView,
- setMouseUserState,
- sidebar,
- submittedPendingTransactions,
- } = this.props
- const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
- const loadMessage = loadingMessage || isLoadingNetwork ?
- this.getConnectingLabel(loadingMessage) : null
- log.debug('Main ui render function')
-
- const sidebarOnOverlayClose = sidebarType === WALLET_VIEW_SIDEBAR
- ? () => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Wallet Sidebar',
- name: 'Closed Sidebare Via Overlay',
- },
- })
- }
- : null
-
- const {
- isOpen: sidebarIsOpen,
- transitionName: sidebarTransitionName,
- type: sidebarType,
- props,
- } = sidebar
- const { transaction: sidebarTransaction } = props || {}
-
- return (
- <div
- className="app"
- onClick={() => setMouseUserState(true)}
- onKeyDown={e => {
- if (e.keyCode === 9) {
- setMouseUserState(false)
- }
- }}
- >
- <UiMigrationAnnouncement />
- <Modal />
- <Alert
- visible={this.props.alertOpen}
- msg={alertMessage}
- />
- {
- !this.hideAppHeader() && (
- <AppHeader
- hideNetworkIndicator={this.onInitializationUnlockPage()}
- disabled={this.onConfirmPage()}
- />
- )
- }
- <Sidebar
- sidebarOpen={sidebarIsOpen}
- sidebarShouldClose={sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)}
- hideSidebar={this.props.hideSidebar}
- transitionName={sidebarTransitionName}
- type={sidebarType}
- sidebarProps={sidebar.props}
- onOverlayClose={sidebarOnOverlayClose}
- />
- <NetworkDropdown
- provider={provider}
- frequentRpcListDetail={frequentRpcListDetail}
- />
- <AccountMenu />
- <div className="main-container-wrapper">
- { isLoading && <Loading loadingMessage={loadMessage} /> }
- { !isLoading && isLoadingNetwork && <LoadingNetwork /> }
- { this.renderRoutes() }
- </div>
- </div>
- )
- }
-
- toggleMetamaskActive () {
- if (!this.props.isUnlocked) {
- // currently inactive: redirect to password box
- var passwordBox = document.querySelector('input[type=password]')
- if (!passwordBox) return
- passwordBox.focus()
- } else {
- // currently active: deactivate
- this.props.dispatch(actions.lockMetamask(false))
- }
- }
-
- getConnectingLabel = function (loadingMessage) {
- if (loadingMessage) {
- return loadingMessage
- }
- const { provider, providerId } = this.props
- const providerName = provider.type
-
- let name
-
- if (providerName === 'mainnet') {
- name = this.context.t('connectingToMainnet')
- } else if (providerName === 'ropsten') {
- name = this.context.t('connectingToRopsten')
- } else if (providerName === 'kovan') {
- name = this.context.t('connectingToKovan')
- } else if (providerName === 'rinkeby') {
- name = this.context.t('connectingToRinkeby')
- } else {
- name = this.context.t('connectingTo', [providerId])
- }
-
- return name
- }
-
- getNetworkName () {
- const { provider } = this.props
- const providerName = provider.type
-
- let name
-
- if (providerName === 'mainnet') {
- name = this.context.t('mainnet')
- } else if (providerName === 'ropsten') {
- name = this.context.t('ropsten')
- } else if (providerName === 'kovan') {
- name = this.context.t('kovan')
- } else if (providerName === 'rinkeby') {
- name = this.context.t('rinkeby')
- } else {
- name = this.context.t('unknownNetwork')
- }
-
- return name
- }
-}
-
-App.propTypes = {
- currentCurrency: PropTypes.string,
- setCurrentCurrencyToUSD: PropTypes.func,
- isLoading: PropTypes.bool,
- loadingMessage: PropTypes.string,
- alertMessage: PropTypes.string,
- network: PropTypes.string,
- provider: PropTypes.object,
- frequentRpcListDetail: PropTypes.array,
- currentView: PropTypes.object,
- sidebar: PropTypes.object,
- alertOpen: PropTypes.bool,
- hideSidebar: PropTypes.func,
- isOnboarding: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- networkDropdownOpen: PropTypes.bool,
- showNetworkDropdown: PropTypes.func,
- hideNetworkDropdown: PropTypes.func,
- history: PropTypes.object,
- location: PropTypes.object,
- dispatch: PropTypes.func,
- toggleAccountMenu: 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,
- submittedPendingTransactions: PropTypes.array,
- unapprovedMsgCount: PropTypes.number,
- unapprovedPersonalMsgCount: PropTypes.number,
- unapprovedTypedMessagesCount: PropTypes.number,
- welcomeScreenSeen: PropTypes.bool,
- isPopup: PropTypes.bool,
- isMouseUser: PropTypes.bool,
- setMouseUserState: PropTypes.func,
- t: PropTypes.func,
- providerId: PropTypes.string,
- providerRequests: PropTypes.array,
-}
-
-function mapStateToProps (state) {
- const { appState, metamask } = state
- const {
- networkDropdownOpen,
- sidebar,
- alertOpen,
- alertMessage,
- isLoading,
- loadingMessage,
- } = appState
-
- const accounts = getMetaMaskAccounts(state)
-
- const {
- identities,
- address,
- keyrings,
- isInitialized,
- noActiveNotices,
- seedWords,
- unapprovedTxs,
- nextUnreadNotice,
- lostAccounts,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- providerRequests,
- } = metamask
- const selected = address || Object.keys(accounts)[0]
-
- return {
- // state from plugin
- networkDropdownOpen,
- sidebar,
- alertOpen,
- alertMessage,
- isLoading,
- loadingMessage,
- noActiveNotices,
- isInitialized,
- isUnlocked: state.metamask.isUnlocked,
- selectedAddress: state.metamask.selectedAddress,
- currentView: state.appState.currentView,
- activeAddress: state.appState.activeAddress,
- transForward: state.appState.transForward,
- isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
- isPopup: state.metamask.isPopup,
- seedWords: state.metamask.seedWords,
- submittedPendingTransactions: submittedPendingTransactionsSelector(state),
- 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,
- frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
- currentCurrency: state.metamask.currentCurrency,
- isMouseUser: state.appState.isMouseUser,
- isRevealingSeedWords: state.metamask.isRevealingSeedWords,
- Qr: state.appState.Qr,
- welcomeScreenSeen: state.metamask.welcomeScreenSeen,
- providerId: getNetworkIdentifier(state),
-
- // state needed to get account dropdown temporarily rendering from app bar
- identities,
- selected,
- keyrings,
- providerRequests,
- }
-}
-
-function mapDispatchToProps (dispatch, ownProps) {
- return {
- dispatch,
- hideSidebar: () => dispatch(actions.hideSidebar()),
- showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
- hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
- setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
- toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
- setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
- }
-}
-
-App.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(App)
diff --git a/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js
deleted file mode 100644
index 8a171d0c6..000000000
--- a/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import AccountListItem from '../send/account-list-item/account-list-item.component'
-
-export default class AccountDropdownMini extends PureComponent {
- static propTypes = {
- accounts: PropTypes.array.isRequired,
- closeDropdown: PropTypes.func,
- disabled: PropTypes.bool,
- dropdownOpen: PropTypes.bool,
- onSelect: PropTypes.func,
- openDropdown: PropTypes.func,
- selectedAccount: PropTypes.object.isRequired,
- }
-
- static defaultProps = {
- closeDropdown: () => {},
- disabled: false,
- dropdownOpen: false,
- onSelect: () => {},
- openDropdown: () => {},
- }
-
- getListItemIcon (currentAccount, selectedAccount) {
- return currentAccount.address === selectedAccount.address && (
- <i
- className="fa fa-check fa-lg"
- style={{ color: '#02c9b1' }}
- />
- )
- }
-
- renderDropdown () {
- const { accounts, selectedAccount, closeDropdown, onSelect } = this.props
-
- return (
- <div>
- <div
- className="account-dropdown-mini__close-area"
- onClick={closeDropdown}
- />
- <div className="account-dropdown-mini__list">
- {
- accounts.map(account => (
- <AccountListItem
- key={account.address}
- account={account}
- displayBalance={false}
- displayAddress={false}
- handleClick={() => {
- onSelect(account)
- closeDropdown()
- }}
- icon={this.getListItemIcon(account, selectedAccount)}
- />
- ))
- }
- </div>
- </div>
- )
- }
-
- render () {
- const { disabled, selectedAccount, openDropdown, dropdownOpen } = this.props
-
- return (
- <div className="account-dropdown-mini">
- <AccountListItem
- account={selectedAccount}
- handleClick={() => !disabled && openDropdown()}
- displayBalance={false}
- displayAddress={false}
- icon={
- !disabled && <i
- className="fa fa-caret-down fa-lg"
- style={{ color: '#dedede' }}
- />
- }
- />
- { !disabled && dropdownOpen && this.renderDropdown() }
- </div>
- )
- }
-}
diff --git a/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js
deleted file mode 100644
index abd2f7c75..000000000
--- a/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import AccountDropdownMini from '../account-dropdown-mini.component'
-import AccountListItem from '../../send/account-list-item/account-list-item.component'
-
-describe('AccountDropdownMini', () => {
- it('should render an account with an icon', () => {
- const accounts = [
- {
- address: '0x1',
- name: 'account1',
- balance: '0x1',
- },
- {
- address: '0x2',
- name: 'account2',
- balance: '0x2',
- },
- {
- address: '0x3',
- name: 'account3',
- balance: '0x3',
- },
- ]
-
- const wrapper = shallow(
- <AccountDropdownMini
- selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
- accounts={accounts}
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(AccountListItem).length, 1)
- const accountListItemProps = wrapper.find(AccountListItem).at(0).props()
- assert.equal(accountListItemProps.account.address, '0x1')
- const iconProps = accountListItemProps.icon.props
- assert.equal(iconProps.className, 'fa fa-caret-down fa-lg')
- })
-
- it('should render a list of accounts', () => {
- const accounts = [
- {
- address: '0x1',
- name: 'account1',
- balance: '0x1',
- },
- {
- address: '0x2',
- name: 'account2',
- balance: '0x2',
- },
- {
- address: '0x3',
- name: 'account3',
- balance: '0x3',
- },
- ]
-
- const wrapper = shallow(
- <AccountDropdownMini
- selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
- accounts={accounts}
- dropdownOpen={true}
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(AccountListItem).length, 4)
- })
-
- it('should render a single account when disabled', () => {
- const accounts = [
- {
- address: '0x1',
- name: 'account1',
- balance: '0x1',
- },
- {
- address: '0x2',
- name: 'account2',
- balance: '0x2',
- },
- {
- address: '0x3',
- name: 'account3',
- balance: '0x3',
- },
- ]
-
- const wrapper = shallow(
- <AccountDropdownMini
- selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
- accounts={accounts}
- dropdownOpen={false}
- disabled={true}
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(AccountListItem).length, 1)
- const accountListItemProps = wrapper.find(AccountListItem).at(0).props()
- assert.equal(accountListItemProps.account.address, '0x1')
- assert.equal(accountListItemProps.icon, false)
- })
-})
diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js
deleted file mode 100644
index b05ba219c..000000000
--- a/ui/app/components/account-dropdowns.js
+++ /dev/null
@@ -1,338 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const actions = require('../actions')
-const genAccountLink = require('etherscan-link').createAccountLink
-const connect = require('react-redux').connect
-const Dropdown = require('./dropdown').Dropdown
-const DropdownMenuItem = require('./dropdown').DropdownMenuItem
-const copyToClipboard = require('copy-to-clipboard')
-const { checksumAddress } = require('../util')
-
-import Identicon from './identicon'
-
-class AccountDropdowns extends Component {
- constructor (props) {
- super(props)
- this.state = {
- accountSelectorActive: false,
- optionsMenuActive: false,
- }
- this.accountSelectorToggleClassName = 'accounts-selector'
- this.optionsMenuToggleClassName = 'fa-ellipsis-h'
- }
-
- renderAccounts () {
- const { identities, selected, keyrings } = this.props
-
- return Object.keys(identities).map((key, index) => {
- const identity = identities[key]
- const isSelected = identity.address === selected
-
- const simpleAddress = identity.address.substring(2).toLowerCase()
-
- const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(simpleAddress) ||
- kr.accounts.includes(identity.address)
- })
-
- return h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- this.props.actions.showAccountDetail(identity.address)
- },
- style: {
- marginTop: index === 0 ? '5px' : '',
- fontSize: '24px',
- },
- },
- [
- h(
- Identicon,
- {
- address: identity.address,
- diameter: 32,
- style: {
- marginLeft: '10px',
- },
- },
- ),
- this.indicateIfLoose(keyring),
- h('span', {
- style: {
- marginLeft: '20px',
- fontSize: '24px',
- maxWidth: '145px',
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- },
- }, identity.name || ''),
- h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
- ]
- )
- })
- }
-
- indicateIfLoose (keyring) {
- try { // Sometimes keyrings aren't loaded yet:
- const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
- return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null
- } catch (e) { return }
- }
-
- renderAccountSelector () {
- const { actions } = this.props
- const { accountSelectorActive } = this.state
-
- return h(
- Dropdown,
- {
- useCssTransition: true, // Hardcoded because account selector is temporarily in app-header
- style: {
- marginLeft: '-238px',
- marginTop: '38px',
- minWidth: '180px',
- overflowY: 'auto',
- maxHeight: '300px',
- width: '300px',
- },
- innerStyle: {
- padding: '8px 25px',
- },
- isOpen: accountSelectorActive,
- onClickOutside: (event) => {
- const { classList } = event.target
- const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
- if (accountSelectorActive && isNotToggleElement) {
- this.setState({ accountSelectorActive: false })
- }
- },
- },
- [
- ...this.renderAccounts(),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => actions.addNewAccount(),
- },
- [
- h(
- Identicon,
- {
- style: {
- marginLeft: '10px',
- },
- diameter: 32,
- },
- ),
- h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, this.context.t('createAccount')),
- ],
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => actions.showImportPage(),
- },
- [
- h(
- Identicon,
- {
- style: {
- marginLeft: '10px',
- },
- diameter: 32,
- },
- ),
- h('span', {
- style: {
- marginLeft: '20px',
- fontSize: '24px',
- marginBottom: '5px',
- },
- }, this.context.t('importAccount')),
- ]
- ),
- ]
- )
- }
-
- renderAccountOptions () {
- const { actions } = this.props
- const { optionsMenuActive } = this.state
-
- return h(
- Dropdown,
- {
- style: {
- marginLeft: '-215px',
- minWidth: '180px',
- },
- isOpen: optionsMenuActive,
- onClickOutside: (event) => {
- const { classList } = event.target
- const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
- if (optionsMenuActive && isNotToggleElement) {
- this.setState({ optionsMenuActive: false })
- }
- },
- },
- [
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- const { selected, network } = this.props
- const url = genAccountLink(selected, network)
- global.platform.openWindow({ url })
- },
- },
- this.context.t('etherscanView'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- const { selected, identities } = this.props
- var identity = identities[selected]
- actions.showQrView(selected, identity ? identity.name : '')
- },
- },
- this.context.t('showQRCode'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- const { selected } = this.props
- copyToClipboard(checksumAddress(selected))
- },
- },
- this.context.t('copyAddress'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- actions.requestAccountExport()
- },
- },
- this.context.t('exportPrivateKey'),
- ),
- ]
- )
- }
-
- render () {
- const { metricsEvent } = this.context
- const { style, enableAccountsSelector, enableAccountOptions } = this.props
- const { optionsMenuActive, accountSelectorActive } = this.state
-
- return h(
- 'span',
- {
- style: style,
- },
- [
- enableAccountsSelector && h(
- // 'i.fa.fa-angle-down',
- 'div.cursor-pointer.color-orange.accounts-selector',
- {
- style: {
- // fontSize: '1.8em',
- background: 'url(images/switch_acc.svg) white center center no-repeat',
- height: '25px',
- width: '25px',
- transform: 'scale(0.75)',
- marginRight: '3px',
- },
- onClick: (event) => {
- event.stopPropagation()
- this.setState({
- accountSelectorActive: !accountSelectorActive,
- optionsMenuActive: false,
- })
- },
- },
- this.renderAccountSelector(),
- ),
- enableAccountOptions && h(
- 'i.fa.fa-ellipsis-h',
- {
- style: {
- marginRight: '0.5em',
- fontSize: '1.8em',
- },
- onClick: (event) => {
- metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'userClick',
- name: 'accountsOpenedMenu',
- },
- pageOpts: {
- section: 'header',
- component: 'accountDropdownIcon',
- },
- })
- event.stopPropagation()
- this.setState({
- accountSelectorActive: false,
- optionsMenuActive: !optionsMenuActive,
- })
- },
- },
- this.renderAccountOptions()
- ),
- ]
- )
- }
-}
-
-AccountDropdowns.defaultProps = {
- enableAccountsSelector: false,
- enableAccountOptions: false,
-}
-
-AccountDropdowns.propTypes = {
- identities: PropTypes.objectOf(PropTypes.object),
- selected: PropTypes.string,
- keyrings: PropTypes.array,
- actions: PropTypes.objectOf(PropTypes.func),
- network: PropTypes.string,
- style: PropTypes.object,
- enableAccountOptions: PropTypes.bool,
- enableAccountsSelector: PropTypes.bool,
- t: PropTypes.func,
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- actions: {
- showConfigPage: () => dispatch(actions.showConfigPage()),
- requestAccountExport: () => dispatch(actions.requestExportAccount()),
- showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
- addNewAccount: () => dispatch(actions.addNewAccount()),
- showImportPage: () => dispatch(actions.showImportPage()),
- showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
- },
- }
-}
-
-AccountDropdowns.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = {
- AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
-}
diff --git a/ui/app/components/account-menu/account-menu.component.js b/ui/app/components/account-menu/account-menu.component.js
deleted file mode 100644
index f7c962874..000000000
--- a/ui/app/components/account-menu/account-menu.component.js
+++ /dev/null
@@ -1,340 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import debounce from 'lodash.debounce'
-import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
-import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
-import { getEnvironmentType } from '../../../../app/scripts/lib/util'
-import Tooltip from '../tooltip'
-import Identicon from '../identicon'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import { PRIMARY } from '../../constants/common'
-import {
- SETTINGS_ROUTE,
- INFO_ROUTE,
- NEW_ACCOUNT_ROUTE,
- IMPORT_ACCOUNT_ROUTE,
- CONNECT_HARDWARE_ROUTE,
- DEFAULT_ROUTE,
-} from '../../routes'
-
-export default class AccountMenu extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- accounts: PropTypes.object,
- history: PropTypes.object,
- identities: PropTypes.object,
- isAccountMenuOpen: PropTypes.bool,
- prevIsAccountMenuOpen: PropTypes.bool,
- keyrings: PropTypes.array,
- lockMetamask: PropTypes.func,
- selectedAddress: PropTypes.string,
- showAccountDetail: PropTypes.func,
- showRemoveAccountConfirmationModal: PropTypes.func,
- toggleAccountMenu: PropTypes.func,
- }
-
- state = {
- atAccountListBottom: false,
- }
-
- componentDidUpdate (prevProps) {
- const { prevIsAccountMenuOpen } = prevProps
- const { isAccountMenuOpen } = this.props
-
- if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
- this.setAtAccountListBottom()
- }
- }
-
- renderAccounts () {
- const {
- identities,
- accounts,
- selectedAddress,
- keyrings,
- showAccountDetail,
- } = this.props
-
- const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
-
- return accountOrder.filter(address => !!identities[address]).map(address => {
- const identity = identities[address]
- const isSelected = identity.address === selectedAddress
-
- const balanceValue = accounts[address] ? accounts[address].balance : ''
- const simpleAddress = identity.address.substring(2).toLowerCase()
-
- const keyring = keyrings.find(kr => {
- return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address)
- })
-
- return (
- <div
- className="account-menu__account menu__item--clickable"
- onClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Main Menu',
- name: 'Switched Account',
- },
- })
- showAccountDetail(identity.address)
- }}
- key={identity.address}
- >
- <div className="account-menu__check-mark">
- { isSelected && <div className="account-menu__check-mark-icon" /> }
- </div>
- <Identicon
- address={identity.address}
- diameter={24}
- />
- <div className="account-menu__account-info">
- <div className="account-menu__name">
- { identity.name || '' }
- </div>
- <UserPreferencedCurrencyDisplay
- className="account-menu__balance"
- value={balanceValue}
- type={PRIMARY}
- />
- </div>
- { this.renderKeyringType(keyring) }
- { this.renderRemoveAccount(keyring, identity) }
- </div>
- )
- })
- }
-
- renderRemoveAccount (keyring, identity) {
- const { t } = this.context
- // Any account that's not from the HD wallet Keyring can be removed
- const { type } = keyring
- const isRemovable = type !== 'HD Key Tree'
-
- return isRemovable && (
- <Tooltip
- title={t('removeAccount')}
- position="bottom"
- >
- <a
- className="remove-account-icon"
- onClick={e => this.removeAccount(e, identity)}
- />
- </Tooltip>
- )
- }
-
- removeAccount (e, identity) {
- e.preventDefault()
- e.stopPropagation()
- const { showRemoveAccountConfirmationModal } = this.props
- showRemoveAccountConfirmationModal(identity)
- }
-
- renderKeyringType (keyring) {
- const { t } = this.context
-
- // Sometimes keyrings aren't loaded yet
- if (!keyring) {
- return null
- }
-
- const { type } = keyring
- let label
-
- switch (type) {
- case 'Trezor Hardware':
- case 'Ledger Hardware':
- label = t('hardware')
- break
- case 'Simple Key Pair':
- label = t('imported')
- break
- }
-
- return label && (
- <div className="keyring-label allcaps">
- { label }
- </div>
- )
- }
-
- setAtAccountListBottom = () => {
- const target = document.querySelector('.account-menu__accounts')
- const { scrollTop, offsetHeight, scrollHeight } = target
- const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
- this.setState({ atAccountListBottom })
- }
-
- onScroll = debounce(this.setAtAccountListBottom, 25)
-
- handleScrollDown = e => {
- e.stopPropagation()
- const target = document.querySelector('.account-menu__accounts')
- const { scrollHeight } = target
- target.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' })
- this.setAtAccountListBottom()
- }
-
- renderScrollButton () {
- const { accounts } = this.props
- const { atAccountListBottom } = this.state
-
- return !atAccountListBottom && Object.keys(accounts).length > 3 && (
- <div
- className="account-menu__scroll-button"
- onClick={this.handleScrollDown}
- >
- <img
- src="./images/icons/down-arrow.svg"
- width={28}
- height={28}
- />
- </div>
- )
- }
-
- render () {
- const { t } = this.context
- const {
- isAccountMenuOpen,
- toggleAccountMenu,
- lockMetamask,
- history,
- } = this.props
- const { metricsEvent } = this.context
-
- return (
- <Menu
- className="account-menu"
- isShowing={isAccountMenuOpen}
- >
- <CloseArea onClick={toggleAccountMenu} />
- <Item className="account-menu__header">
- { t('myAccounts') }
- <button
- className="account-menu__logout-button"
- onClick={() => {
- lockMetamask()
- history.push(DEFAULT_ROUTE)
- }}
- >
- { t('logout') }
- </button>
- </Item>
- <Divider />
- <div className="account-menu__accounts-container">
- <div
- className="account-menu__accounts"
- onScroll={this.onScroll}
- >
- { this.renderAccounts() }
- </div>
- { this.renderScrollButton() }
- </div>
- <Divider />
- <Item
- onClick={() => {
- toggleAccountMenu()
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Main Menu',
- name: 'Clicked Create Account',
- },
- })
- history.push(NEW_ACCOUNT_ROUTE)
- }}
- icon={
- <img
- className="account-menu__item-icon"
- src="images/plus-btn-white.svg"
- />
- }
- text={t('createAccount')}
- />
- <Item
- onClick={() => {
- toggleAccountMenu()
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Main Menu',
- name: 'Clicked Import Account',
- },
- })
- history.push(IMPORT_ACCOUNT_ROUTE)
- }}
- icon={
- <img
- className="account-menu__item-icon"
- src="images/import-account.svg"
- />
- }
- text={t('importAccount')}
- />
- <Item
- onClick={() => {
- toggleAccountMenu()
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Main Menu',
- name: 'Clicked Connect Hardware',
- },
- })
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
- } else {
- history.push(CONNECT_HARDWARE_ROUTE)
- }
- }}
- icon={
- <img
- className="account-menu__item-icon"
- src="images/connect-icon.svg"
- />
- }
- text={t('connectHardwareWallet')}
- />
- <Divider />
- <Item
- onClick={() => {
- toggleAccountMenu()
- history.push(INFO_ROUTE)
- }}
- icon={
- <img src="images/mm-info-icon.svg" />
- }
- text={t('infoHelp')}
- />
- <Item
- onClick={() => {
- toggleAccountMenu()
- history.push(SETTINGS_ROUTE)
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Main Menu',
- name: 'Opened Settings',
- },
- })
- }}
- icon={
- <img
- className="account-menu__item-icon"
- src="images/settings.svg"
- />
- }
- text={t('settings')}
- />
- </Menu>
- )
- }
-}
diff --git a/ui/app/components/account-menu/account-menu.container.js b/ui/app/components/account-menu/account-menu.container.js
deleted file mode 100644
index 93246ec72..000000000
--- a/ui/app/components/account-menu/account-menu.container.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router-dom'
-import {
- toggleAccountMenu,
- showAccountDetail,
- hideSidebar,
- lockMetamask,
- hideWarning,
- showConfigPage,
- showInfoPage,
- showModal,
-} from '../../actions'
-import { getMetaMaskAccounts } from '../../selectors'
-import AccountMenu from './account-menu.component'
-
-function mapStateToProps (state) {
- const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state
-
- return {
- selectedAddress,
- isAccountMenuOpen,
- keyrings,
- identities,
- accounts: getMetaMaskAccounts(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- toggleAccountMenu: () => dispatch(toggleAccountMenu()),
- showAccountDetail: address => {
- dispatch(showAccountDetail(address))
- dispatch(hideSidebar())
- dispatch(toggleAccountMenu())
- },
- lockMetamask: () => {
- dispatch(lockMetamask())
- dispatch(hideWarning())
- dispatch(hideSidebar())
- dispatch(toggleAccountMenu())
- },
- showConfigPage: () => {
- dispatch(showConfigPage())
- dispatch(hideSidebar())
- dispatch(toggleAccountMenu())
- },
- showInfoPage: () => {
- dispatch(showInfoPage())
- dispatch(hideSidebar())
- dispatch(toggleAccountMenu())
- },
- showRemoveAccountConfirmationModal: identity => {
- return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
- },
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(AccountMenu)
diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js
deleted file mode 100644
index a379ed3ac..000000000
--- a/ui/app/components/account-panel.js
+++ /dev/null
@@ -1,86 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-import Identicon from './identicon'
-const formatBalance = require('../util').formatBalance
-const addressSummary = require('../util').addressSummary
-
-module.exports = AccountPanel
-
-
-inherits(AccountPanel, Component)
-function AccountPanel () {
- Component.call(this)
-}
-
-AccountPanel.prototype.render = function () {
- var state = this.props
- var identity = state.identity || {}
- var account = state.account || {}
- var isFauceting = state.isFauceting
-
- var panelState = {
- key: `accountPanel${identity.address}`,
- identiconKey: identity.address,
- identiconLabel: identity.name || '',
- attributes: [
- {
- key: 'ADDRESS',
- value: addressSummary(identity.address),
- },
- balanceOrFaucetingIndication(account, isFauceting),
- ],
- }
-
- return (
-
- h('.identity-panel.flex-row.flex-space-between', {
- style: {
- flex: '1 0 auto',
- cursor: panelState.onClick ? 'pointer' : undefined,
- },
- onClick: panelState.onClick,
- }, [
-
- // account identicon
- h('.identicon-wrapper.flex-column.select-none', [
- h(Identicon, {
- address: panelState.identiconKey,
- imageify: state.imageifyIdenticons,
- }),
- h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'),
- ]),
-
- // account address, balance
- h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
-
- panelState.attributes.map((attr) => {
- return h('.flex-row.flex-space-between', {
- key: '' + Math.round(Math.random() * 1000000),
- }, [
- h('label.font-small.no-select', attr.key),
- h('span.font-small', attr.value),
- ])
- }),
- ]),
-
- ])
-
- )
-}
-
-function balanceOrFaucetingIndication (account, isFauceting) {
- // Temporarily deactivating isFauceting indication
- // because it shows fauceting for empty restored accounts.
- if (/* isFauceting*/ false) {
- return {
- key: 'Account is auto-funding.',
- value: 'Please wait.',
- }
- } else {
- return {
- key: 'BALANCE',
- value: formatBalance(account.balance),
- }
- }
-}
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
deleted file mode 100644
index 14f8b9f30..000000000
--- a/ui/app/components/app-header/app-header.component.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import Identicon from '../identicon'
-import { DEFAULT_ROUTE } from '../../routes'
-const NetworkIndicator = require('../network')
-
-export default class AppHeader extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- network: PropTypes.string,
- provider: PropTypes.object,
- networkDropdownOpen: PropTypes.bool,
- showNetworkDropdown: PropTypes.func,
- hideNetworkDropdown: PropTypes.func,
- toggleAccountMenu: PropTypes.func,
- selectedAddress: PropTypes.string,
- isUnlocked: PropTypes.bool,
- hideNetworkIndicator: PropTypes.bool,
- disabled: PropTypes.bool,
- isAccountMenuOpen: PropTypes.bool,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- handleNetworkIndicatorClick (event) {
- event.preventDefault()
- event.stopPropagation()
-
- const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
-
- if (networkDropdownOpen === false) {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Opened Network Menu',
- },
- })
- showNetworkDropdown()
- } else {
- hideNetworkDropdown()
- }
- }
-
- renderAccountMenu () {
- const { isUnlocked, toggleAccountMenu, selectedAddress, disabled, isAccountMenuOpen } = this.props
-
- return isUnlocked && (
- <div
- className={classnames('account-menu__icon', {
- 'account-menu__icon--disabled': disabled,
- })}
- onClick={() => {
- if (!disabled) {
- !isAccountMenuOpen && this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Opened Main Menu',
- },
- })
- toggleAccountMenu()
- }
- }}
- >
- <Identicon
- address={selectedAddress}
- diameter={32}
- />
- </div>
- )
- }
-
- render () {
- const {
- history,
- network,
- provider,
- isUnlocked,
- hideNetworkIndicator,
- disabled,
- } = this.props
-
- return (
- <div
- className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
- <div className="app-header__contents">
- <div
- className="app-header__logo-container"
- onClick={() => history.push(DEFAULT_ROUTE)}
- >
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <div className="app-header__account-menu-container">
- {
- !hideNetworkIndicator && (
- <div className="app-header__network-component-wrapper">
- <NetworkIndicator
- network={network}
- provider={provider}
- onClick={event => this.handleNetworkIndicatorClick(event)}
- disabled={disabled}
- />
- </div>
- )
- }
- { this.renderAccountMenu() }
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js
deleted file mode 100644
index 1abc2afeb..000000000
--- a/ui/app/components/app-header/app-header.container.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-
-import AppHeader from './app-header.component'
-const actions = require('../../actions')
-
-const mapStateToProps = state => {
- const { appState, metamask } = state
- const { networkDropdownOpen } = appState
- const {
- network,
- provider,
- selectedAddress,
- isUnlocked,
- isAccountMenuOpen,
- } = metamask
-
- return {
- networkDropdownOpen,
- network,
- provider,
- selectedAddress,
- isUnlocked,
- isAccountMenuOpen,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
- hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
- toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(AppHeader)
diff --git a/ui/app/components/app/account-dropdowns.js b/ui/app/components/app/account-dropdowns.js
new file mode 100644
index 000000000..e02d17e54
--- /dev/null
+++ b/ui/app/components/app/account-dropdowns.js
@@ -0,0 +1,338 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const actions = require('../../store/actions')
+const genAccountLink = require('etherscan-link').createAccountLink
+const connect = require('react-redux').connect
+const Dropdown = require('./dropdown').Dropdown
+const DropdownMenuItem = require('./dropdown').DropdownMenuItem
+const copyToClipboard = require('copy-to-clipboard')
+const { checksumAddress } = require('../../helpers/utils/util')
+
+import Identicon from '../ui/identicon'
+
+class AccountDropdowns extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ accountSelectorActive: false,
+ optionsMenuActive: false,
+ }
+ this.accountSelectorToggleClassName = 'accounts-selector'
+ this.optionsMenuToggleClassName = 'fa-ellipsis-h'
+ }
+
+ renderAccounts () {
+ const { identities, selected, keyrings } = this.props
+
+ return Object.keys(identities).map((key, index) => {
+ const identity = identities[key]
+ const isSelected = identity.address === selected
+
+ const simpleAddress = identity.address.substring(2).toLowerCase()
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(simpleAddress) ||
+ kr.accounts.includes(identity.address)
+ })
+
+ return h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ this.props.actions.showAccountDetail(identity.address)
+ },
+ style: {
+ marginTop: index === 0 ? '5px' : '',
+ fontSize: '24px',
+ },
+ },
+ [
+ h(
+ Identicon,
+ {
+ address: identity.address,
+ diameter: 32,
+ style: {
+ marginLeft: '10px',
+ },
+ },
+ ),
+ this.indicateIfLoose(keyring),
+ h('span', {
+ style: {
+ marginLeft: '20px',
+ fontSize: '24px',
+ maxWidth: '145px',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ }, identity.name || ''),
+ h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
+ ]
+ )
+ })
+ }
+
+ indicateIfLoose (keyring) {
+ try { // Sometimes keyrings aren't loaded yet:
+ const type = keyring.type
+ const isLoose = type !== 'HD Key Tree'
+ return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null
+ } catch (e) { return }
+ }
+
+ renderAccountSelector () {
+ const { actions } = this.props
+ const { accountSelectorActive } = this.state
+
+ return h(
+ Dropdown,
+ {
+ useCssTransition: true, // Hardcoded because account selector is temporarily in app-header
+ style: {
+ marginLeft: '-238px',
+ marginTop: '38px',
+ minWidth: '180px',
+ overflowY: 'auto',
+ maxHeight: '300px',
+ width: '300px',
+ },
+ innerStyle: {
+ padding: '8px 25px',
+ },
+ isOpen: accountSelectorActive,
+ onClickOutside: (event) => {
+ const { classList } = event.target
+ const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
+ if (accountSelectorActive && isNotToggleElement) {
+ this.setState({ accountSelectorActive: false })
+ }
+ },
+ },
+ [
+ ...this.renderAccounts(),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => actions.addNewAccount(),
+ },
+ [
+ h(
+ Identicon,
+ {
+ style: {
+ marginLeft: '10px',
+ },
+ diameter: 32,
+ },
+ ),
+ h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, this.context.t('createAccount')),
+ ],
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => actions.showImportPage(),
+ },
+ [
+ h(
+ Identicon,
+ {
+ style: {
+ marginLeft: '10px',
+ },
+ diameter: 32,
+ },
+ ),
+ h('span', {
+ style: {
+ marginLeft: '20px',
+ fontSize: '24px',
+ marginBottom: '5px',
+ },
+ }, this.context.t('importAccount')),
+ ]
+ ),
+ ]
+ )
+ }
+
+ renderAccountOptions () {
+ const { actions } = this.props
+ const { optionsMenuActive } = this.state
+
+ return h(
+ Dropdown,
+ {
+ style: {
+ marginLeft: '-215px',
+ minWidth: '180px',
+ },
+ isOpen: optionsMenuActive,
+ onClickOutside: (event) => {
+ const { classList } = event.target
+ const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
+ if (optionsMenuActive && isNotToggleElement) {
+ this.setState({ optionsMenuActive: false })
+ }
+ },
+ },
+ [
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ const { selected, network } = this.props
+ const url = genAccountLink(selected, network)
+ global.platform.openWindow({ url })
+ },
+ },
+ this.context.t('etherscanView'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ const { selected, identities } = this.props
+ var identity = identities[selected]
+ actions.showQrView(selected, identity ? identity.name : '')
+ },
+ },
+ this.context.t('showQRCode'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ const { selected } = this.props
+ copyToClipboard(checksumAddress(selected))
+ },
+ },
+ this.context.t('copyAddress'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ actions.requestAccountExport()
+ },
+ },
+ this.context.t('exportPrivateKey'),
+ ),
+ ]
+ )
+ }
+
+ render () {
+ const { metricsEvent } = this.context
+ const { style, enableAccountsSelector, enableAccountOptions } = this.props
+ const { optionsMenuActive, accountSelectorActive } = this.state
+
+ return h(
+ 'span',
+ {
+ style: style,
+ },
+ [
+ enableAccountsSelector && h(
+ // 'i.fa.fa-angle-down',
+ 'div.cursor-pointer.color-orange.accounts-selector',
+ {
+ style: {
+ // fontSize: '1.8em',
+ background: 'url(images/switch_acc.svg) white center center no-repeat',
+ height: '25px',
+ width: '25px',
+ transform: 'scale(0.75)',
+ marginRight: '3px',
+ },
+ onClick: (event) => {
+ event.stopPropagation()
+ this.setState({
+ accountSelectorActive: !accountSelectorActive,
+ optionsMenuActive: false,
+ })
+ },
+ },
+ this.renderAccountSelector(),
+ ),
+ enableAccountOptions && h(
+ 'i.fa.fa-ellipsis-h',
+ {
+ style: {
+ marginRight: '0.5em',
+ fontSize: '1.8em',
+ },
+ onClick: (event) => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'userClick',
+ name: 'accountsOpenedMenu',
+ },
+ pageOpts: {
+ section: 'header',
+ component: 'accountDropdownIcon',
+ },
+ })
+ event.stopPropagation()
+ this.setState({
+ accountSelectorActive: false,
+ optionsMenuActive: !optionsMenuActive,
+ })
+ },
+ },
+ this.renderAccountOptions()
+ ),
+ ]
+ )
+ }
+}
+
+AccountDropdowns.defaultProps = {
+ enableAccountsSelector: false,
+ enableAccountOptions: false,
+}
+
+AccountDropdowns.propTypes = {
+ identities: PropTypes.objectOf(PropTypes.object),
+ selected: PropTypes.string,
+ keyrings: PropTypes.array,
+ actions: PropTypes.objectOf(PropTypes.func),
+ network: PropTypes.string,
+ style: PropTypes.object,
+ enableAccountOptions: PropTypes.bool,
+ enableAccountsSelector: PropTypes.bool,
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ actions: {
+ showConfigPage: () => dispatch(actions.showConfigPage()),
+ requestAccountExport: () => dispatch(actions.requestExportAccount()),
+ showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
+ addNewAccount: () => dispatch(actions.addNewAccount()),
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
+ },
+ }
+}
+
+AccountDropdowns.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = {
+ AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
+}
diff --git a/ui/app/components/app/account-menu/account-menu.component.js b/ui/app/components/app/account-menu/account-menu.component.js
new file mode 100644
index 000000000..972ea492e
--- /dev/null
+++ b/ui/app/components/app/account-menu/account-menu.component.js
@@ -0,0 +1,340 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import debounce from 'lodash.debounce'
+import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import Tooltip from '../../ui/tooltip'
+import Identicon from '../../ui/identicon'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
+import { PRIMARY } from '../../../helpers/constants/common'
+import {
+ SETTINGS_ROUTE,
+ INFO_ROUTE,
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+ DEFAULT_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class AccountMenu extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ accounts: PropTypes.object,
+ history: PropTypes.object,
+ identities: PropTypes.object,
+ isAccountMenuOpen: PropTypes.bool,
+ prevIsAccountMenuOpen: PropTypes.bool,
+ keyrings: PropTypes.array,
+ lockMetamask: PropTypes.func,
+ selectedAddress: PropTypes.string,
+ showAccountDetail: PropTypes.func,
+ showRemoveAccountConfirmationModal: PropTypes.func,
+ toggleAccountMenu: PropTypes.func,
+ }
+
+ state = {
+ atAccountListBottom: false,
+ }
+
+ componentDidUpdate (prevProps) {
+ const { prevIsAccountMenuOpen } = prevProps
+ const { isAccountMenuOpen } = this.props
+
+ if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
+ this.setAtAccountListBottom()
+ }
+ }
+
+ renderAccounts () {
+ const {
+ identities,
+ accounts,
+ selectedAddress,
+ keyrings,
+ showAccountDetail,
+ } = this.props
+
+ const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
+
+ return accountOrder.filter(address => !!identities[address]).map(address => {
+ const identity = identities[address]
+ const isSelected = identity.address === selectedAddress
+
+ const balanceValue = accounts[address] ? accounts[address].balance : ''
+ const simpleAddress = identity.address.substring(2).toLowerCase()
+
+ const keyring = keyrings.find(kr => {
+ return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address)
+ })
+
+ return (
+ <div
+ className="account-menu__account menu__item--clickable"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Main Menu',
+ name: 'Switched Account',
+ },
+ })
+ showAccountDetail(identity.address)
+ }}
+ key={identity.address}
+ >
+ <div className="account-menu__check-mark">
+ { isSelected && <div className="account-menu__check-mark-icon" /> }
+ </div>
+ <Identicon
+ address={identity.address}
+ diameter={24}
+ />
+ <div className="account-menu__account-info">
+ <div className="account-menu__name">
+ { identity.name || '' }
+ </div>
+ <UserPreferencedCurrencyDisplay
+ className="account-menu__balance"
+ value={balanceValue}
+ type={PRIMARY}
+ />
+ </div>
+ { this.renderKeyringType(keyring) }
+ { this.renderRemoveAccount(keyring, identity) }
+ </div>
+ )
+ })
+ }
+
+ renderRemoveAccount (keyring, identity) {
+ const { t } = this.context
+ // Any account that's not from the HD wallet Keyring can be removed
+ const { type } = keyring
+ const isRemovable = type !== 'HD Key Tree'
+
+ return isRemovable && (
+ <Tooltip
+ title={t('removeAccount')}
+ position="bottom"
+ >
+ <a
+ className="remove-account-icon"
+ onClick={e => this.removeAccount(e, identity)}
+ />
+ </Tooltip>
+ )
+ }
+
+ removeAccount (e, identity) {
+ e.preventDefault()
+ e.stopPropagation()
+ const { showRemoveAccountConfirmationModal } = this.props
+ showRemoveAccountConfirmationModal(identity)
+ }
+
+ renderKeyringType (keyring) {
+ const { t } = this.context
+
+ // Sometimes keyrings aren't loaded yet
+ if (!keyring) {
+ return null
+ }
+
+ const { type } = keyring
+ let label
+
+ switch (type) {
+ case 'Trezor Hardware':
+ case 'Ledger Hardware':
+ label = t('hardware')
+ break
+ case 'Simple Key Pair':
+ label = t('imported')
+ break
+ }
+
+ return label && (
+ <div className="keyring-label allcaps">
+ { label }
+ </div>
+ )
+ }
+
+ setAtAccountListBottom = () => {
+ const target = document.querySelector('.account-menu__accounts')
+ const { scrollTop, offsetHeight, scrollHeight } = target
+ const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
+ this.setState({ atAccountListBottom })
+ }
+
+ onScroll = debounce(this.setAtAccountListBottom, 25)
+
+ handleScrollDown = e => {
+ e.stopPropagation()
+ const target = document.querySelector('.account-menu__accounts')
+ const { scrollHeight } = target
+ target.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' })
+ this.setAtAccountListBottom()
+ }
+
+ renderScrollButton () {
+ const { accounts } = this.props
+ const { atAccountListBottom } = this.state
+
+ return !atAccountListBottom && Object.keys(accounts).length > 3 && (
+ <div
+ className="account-menu__scroll-button"
+ onClick={this.handleScrollDown}
+ >
+ <img
+ src="./images/icons/down-arrow.svg"
+ width={28}
+ height={28}
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const {
+ isAccountMenuOpen,
+ toggleAccountMenu,
+ lockMetamask,
+ history,
+ } = this.props
+ const { metricsEvent } = this.context
+
+ return (
+ <Menu
+ className="account-menu"
+ isShowing={isAccountMenuOpen}
+ >
+ <CloseArea onClick={toggleAccountMenu} />
+ <Item className="account-menu__header">
+ { t('myAccounts') }
+ <button
+ className="account-menu__logout-button"
+ onClick={() => {
+ lockMetamask()
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { t('logout') }
+ </button>
+ </Item>
+ <Divider />
+ <div className="account-menu__accounts-container">
+ <div
+ className="account-menu__accounts"
+ onScroll={this.onScroll}
+ >
+ { this.renderAccounts() }
+ </div>
+ { this.renderScrollButton() }
+ </div>
+ <Divider />
+ <Item
+ onClick={() => {
+ toggleAccountMenu()
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Main Menu',
+ name: 'Clicked Create Account',
+ },
+ })
+ history.push(NEW_ACCOUNT_ROUTE)
+ }}
+ icon={
+ <img
+ className="account-menu__item-icon"
+ src="images/plus-btn-white.svg"
+ />
+ }
+ text={t('createAccount')}
+ />
+ <Item
+ onClick={() => {
+ toggleAccountMenu()
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Main Menu',
+ name: 'Clicked Import Account',
+ },
+ })
+ history.push(IMPORT_ACCOUNT_ROUTE)
+ }}
+ icon={
+ <img
+ className="account-menu__item-icon"
+ src="images/import-account.svg"
+ />
+ }
+ text={t('importAccount')}
+ />
+ <Item
+ onClick={() => {
+ toggleAccountMenu()
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Main Menu',
+ name: 'Clicked Connect Hardware',
+ },
+ })
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
+ } else {
+ history.push(CONNECT_HARDWARE_ROUTE)
+ }
+ }}
+ icon={
+ <img
+ className="account-menu__item-icon"
+ src="images/connect-icon.svg"
+ />
+ }
+ text={t('connectHardwareWallet')}
+ />
+ <Divider />
+ <Item
+ onClick={() => {
+ toggleAccountMenu()
+ history.push(INFO_ROUTE)
+ }}
+ icon={
+ <img src="images/mm-info-icon.svg" />
+ }
+ text={t('infoHelp')}
+ />
+ <Item
+ onClick={() => {
+ toggleAccountMenu()
+ history.push(SETTINGS_ROUTE)
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Main Menu',
+ name: 'Opened Settings',
+ },
+ })
+ }}
+ icon={
+ <img
+ className="account-menu__item-icon"
+ src="images/settings.svg"
+ />
+ }
+ text={t('settings')}
+ />
+ </Menu>
+ )
+ }
+}
diff --git a/ui/app/components/app/account-menu/account-menu.container.js b/ui/app/components/app/account-menu/account-menu.container.js
new file mode 100644
index 000000000..ae2e28e76
--- /dev/null
+++ b/ui/app/components/app/account-menu/account-menu.container.js
@@ -0,0 +1,62 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import {
+ toggleAccountMenu,
+ showAccountDetail,
+ hideSidebar,
+ lockMetamask,
+ hideWarning,
+ showConfigPage,
+ showInfoPage,
+ showModal,
+} from '../../../store/actions'
+import { getMetaMaskAccounts } from '../../../selectors/selectors'
+import AccountMenu from './account-menu.component'
+
+function mapStateToProps (state) {
+ const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state
+
+ return {
+ selectedAddress,
+ isAccountMenuOpen,
+ keyrings,
+ identities,
+ accounts: getMetaMaskAccounts(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ toggleAccountMenu: () => dispatch(toggleAccountMenu()),
+ showAccountDetail: address => {
+ dispatch(showAccountDetail(address))
+ dispatch(hideSidebar())
+ dispatch(toggleAccountMenu())
+ },
+ lockMetamask: () => {
+ dispatch(lockMetamask())
+ dispatch(hideWarning())
+ dispatch(hideSidebar())
+ dispatch(toggleAccountMenu())
+ },
+ showConfigPage: () => {
+ dispatch(showConfigPage())
+ dispatch(hideSidebar())
+ dispatch(toggleAccountMenu())
+ },
+ showInfoPage: () => {
+ dispatch(showInfoPage())
+ dispatch(hideSidebar())
+ dispatch(toggleAccountMenu())
+ },
+ showRemoveAccountConfirmationModal: identity => {
+ return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(AccountMenu)
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/app/account-menu/index.js
index b2b4e4c6f..b2b4e4c6f 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/app/account-menu/index.js
diff --git a/ui/app/components/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss
index 9a61bf887..9a61bf887 100644
--- a/ui/app/components/account-menu/index.scss
+++ b/ui/app/components/app/account-menu/index.scss
diff --git a/ui/app/components/app/account-panel.js b/ui/app/components/app/account-panel.js
new file mode 100644
index 000000000..79882f34a
--- /dev/null
+++ b/ui/app/components/app/account-panel.js
@@ -0,0 +1,86 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+import Identicon from '../ui/identicon'
+const formatBalance = require('../../helpers/utils/util').formatBalance
+const addressSummary = require('../../helpers/utils/util').addressSummary
+
+module.exports = AccountPanel
+
+
+inherits(AccountPanel, Component)
+function AccountPanel () {
+ Component.call(this)
+}
+
+AccountPanel.prototype.render = function () {
+ var state = this.props
+ var identity = state.identity || {}
+ var account = state.account || {}
+ var isFauceting = state.isFauceting
+
+ var panelState = {
+ key: `accountPanel${identity.address}`,
+ identiconKey: identity.address,
+ identiconLabel: identity.name || '',
+ attributes: [
+ {
+ key: 'ADDRESS',
+ value: addressSummary(identity.address),
+ },
+ balanceOrFaucetingIndication(account, isFauceting),
+ ],
+ }
+
+ return (
+
+ h('.identity-panel.flex-row.flex-space-between', {
+ style: {
+ flex: '1 0 auto',
+ cursor: panelState.onClick ? 'pointer' : undefined,
+ },
+ onClick: panelState.onClick,
+ }, [
+
+ // account identicon
+ h('.identicon-wrapper.flex-column.select-none', [
+ h(Identicon, {
+ address: panelState.identiconKey,
+ imageify: state.imageifyIdenticons,
+ }),
+ h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'),
+ ]),
+
+ // account address, balance
+ h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
+
+ panelState.attributes.map((attr) => {
+ return h('.flex-row.flex-space-between', {
+ key: '' + Math.round(Math.random() * 1000000),
+ }, [
+ h('label.font-small.no-select', attr.key),
+ h('span.font-small', attr.value),
+ ])
+ }),
+ ]),
+
+ ])
+
+ )
+}
+
+function balanceOrFaucetingIndication (account, isFauceting) {
+ // Temporarily deactivating isFauceting indication
+ // because it shows fauceting for empty restored accounts.
+ if (/* isFauceting*/ false) {
+ return {
+ key: 'Account is auto-funding.',
+ value: 'Please wait.',
+ }
+ } else {
+ return {
+ key: 'BALANCE',
+ value: formatBalance(account.balance),
+ }
+ }
+}
diff --git a/ui/app/components/add-token-button/add-token-button.component.js b/ui/app/components/app/add-token-button/add-token-button.component.js
index 10887aed8..10887aed8 100644
--- a/ui/app/components/add-token-button/add-token-button.component.js
+++ b/ui/app/components/app/add-token-button/add-token-button.component.js
diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/app/add-token-button/index.js
index 15c4fe6ca..15c4fe6ca 100644
--- a/ui/app/components/add-token-button/index.js
+++ b/ui/app/components/app/add-token-button/index.js
diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/app/add-token-button/index.scss
index 39f404716..39f404716 100644
--- a/ui/app/components/add-token-button/index.scss
+++ b/ui/app/components/app/add-token-button/index.scss
diff --git a/ui/app/components/app/app-header/app-header.component.js b/ui/app/components/app/app-header/app-header.component.js
new file mode 100644
index 000000000..343e0daab
--- /dev/null
+++ b/ui/app/components/app/app-header/app-header.component.js
@@ -0,0 +1,127 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../../ui/identicon'
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
+const NetworkIndicator = require('../network')
+
+export default class AppHeader extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ networkDropdownOpen: PropTypes.bool,
+ showNetworkDropdown: PropTypes.func,
+ hideNetworkDropdown: PropTypes.func,
+ toggleAccountMenu: PropTypes.func,
+ selectedAddress: PropTypes.string,
+ isUnlocked: PropTypes.bool,
+ hideNetworkIndicator: PropTypes.bool,
+ disabled: PropTypes.bool,
+ isAccountMenuOpen: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ handleNetworkIndicatorClick (event) {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
+
+ if (networkDropdownOpen === false) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Opened Network Menu',
+ },
+ })
+ showNetworkDropdown()
+ } else {
+ hideNetworkDropdown()
+ }
+ }
+
+ renderAccountMenu () {
+ const { isUnlocked, toggleAccountMenu, selectedAddress, disabled, isAccountMenuOpen } = this.props
+
+ return isUnlocked && (
+ <div
+ className={classnames('account-menu__icon', {
+ 'account-menu__icon--disabled': disabled,
+ })}
+ onClick={() => {
+ if (!disabled) {
+ !isAccountMenuOpen && this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Opened Main Menu',
+ },
+ })
+ toggleAccountMenu()
+ }
+ }}
+ >
+ <Identicon
+ address={selectedAddress}
+ diameter={32}
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const {
+ history,
+ network,
+ provider,
+ isUnlocked,
+ hideNetworkIndicator,
+ disabled,
+ } = this.props
+
+ return (
+ <div
+ className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
+ <div className="app-header__contents">
+ <div
+ className="app-header__logo-container"
+ onClick={() => history.push(DEFAULT_ROUTE)}
+ >
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <div className="app-header__account-menu-container">
+ {
+ !hideNetworkIndicator && (
+ <div className="app-header__network-component-wrapper">
+ <NetworkIndicator
+ network={network}
+ provider={provider}
+ onClick={event => this.handleNetworkIndicatorClick(event)}
+ disabled={disabled}
+ />
+ </div>
+ )
+ }
+ { this.renderAccountMenu() }
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/app-header/app-header.container.js b/ui/app/components/app/app-header/app-header.container.js
new file mode 100644
index 000000000..b67338245
--- /dev/null
+++ b/ui/app/components/app/app-header/app-header.container.js
@@ -0,0 +1,40 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+
+import AppHeader from './app-header.component'
+const actions = require('../../../store/actions')
+
+const mapStateToProps = state => {
+ const { appState, metamask } = state
+ const { networkDropdownOpen } = appState
+ const {
+ network,
+ provider,
+ selectedAddress,
+ isUnlocked,
+ isAccountMenuOpen,
+ } = metamask
+
+ return {
+ networkDropdownOpen,
+ network,
+ provider,
+ selectedAddress,
+ isUnlocked,
+ isAccountMenuOpen,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
+ toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(AppHeader)
diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app/app-header/index.js
index 6de2f9c78..6de2f9c78 100644
--- a/ui/app/components/app-header/index.js
+++ b/ui/app/components/app/app-header/index.js
diff --git a/ui/app/components/app-header/index.scss b/ui/app/components/app/app-header/index.scss
index 325844af5..325844af5 100644
--- a/ui/app/components/app-header/index.scss
+++ b/ui/app/components/app/app-header/index.scss
diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/app/bn-as-decimal-input.js
index 9a033f893..9a033f893 100644
--- a/ui/app/components/bn-as-decimal-input.js
+++ b/ui/app/components/app/bn-as-decimal-input.js
diff --git a/ui/app/components/app/coinbase-form.js b/ui/app/components/app/coinbase-form.js
new file mode 100644
index 000000000..24d287604
--- /dev/null
+++ b/ui/app/components/app/coinbase-form.js
@@ -0,0 +1,69 @@
+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('../../store/actions')
+
+CoinbaseForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps)(CoinbaseForm)
+
+
+function mapStateToProps (state) {
+ return {
+ warning: state.appState.warning,
+ }
+}
+
+inherits(CoinbaseForm, Component)
+
+function CoinbaseForm () {
+ Component.call(this)
+}
+
+CoinbaseForm.prototype.render = function () {
+ var props = this.props
+
+ return h('.flex-column', {
+ style: {
+ marginTop: '35px',
+ padding: '25px',
+ width: '100%',
+ },
+ }, [
+ h('.flex-row', {
+ style: {
+ justifyContent: 'space-around',
+ margin: '33px',
+ marginTop: '0px',
+ },
+ }, [
+ h('button.btn-green', {
+ onClick: this.toCoinbase.bind(this),
+ }, this.context.t('continueToCoinbase')),
+
+ h('button.btn-red', {
+ onClick: () => props.dispatch(actions.goHome()),
+ }, this.context.t('cancel')),
+ ]),
+ ])
+}
+
+CoinbaseForm.prototype.toCoinbase = function () {
+ const props = this.props
+ const address = props.buyView.buyAddress
+ props.dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
+}
+
+CoinbaseForm.prototype.renderLoading = function () {
+ return h('img', {
+ style: {
+ width: '27px',
+ marginRight: '-27px',
+ },
+ src: 'images/loading.svg',
+ })
+}
diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
new file mode 100644
index 000000000..18571eccb
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
@@ -0,0 +1,84 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'
+
+const ConfirmDetailRow = props => {
+ const {
+ label,
+ primaryText,
+ secondaryText,
+ onHeaderClick,
+ primaryValueTextColor,
+ headerText,
+ headerTextClassName,
+ value,
+ } = props
+
+ return (
+ <div className="confirm-detail-row">
+ <div className="confirm-detail-row__label">
+ { label }
+ </div>
+ <div className="confirm-detail-row__details">
+ <div
+ className={classnames('confirm-detail-row__header-text', headerTextClassName)}
+ onClick={() => onHeaderClick && onHeaderClick()}
+ >
+ { headerText }
+ </div>
+ {
+ primaryText
+ ? (
+ <div
+ className="confirm-detail-row__primary"
+ style={{ color: primaryValueTextColor }}
+ >
+ { primaryText }
+ </div>
+ ) : (
+ <UserPreferencedCurrencyDisplay
+ className="confirm-detail-row__primary"
+ type={PRIMARY}
+ value={value}
+ showEthLogo
+ ethLogoHeight="18"
+ style={{ color: primaryValueTextColor }}
+ hideLabel
+ />
+ )
+ }
+ {
+ secondaryText
+ ? (
+ <div className="confirm-detail-row__secondary">
+ { secondaryText }
+ </div>
+ ) : (
+ <UserPreferencedCurrencyDisplay
+ className="confirm-detail-row__secondary"
+ type={SECONDARY}
+ value={value}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+ </div>
+ </div>
+ )
+}
+
+ConfirmDetailRow.propTypes = {
+ headerText: PropTypes.string,
+ headerTextClassName: PropTypes.string,
+ label: PropTypes.string,
+ onHeaderClick: PropTypes.func,
+ primaryValueTextColor: PropTypes.string,
+ primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ secondaryText: PropTypes.string,
+ value: PropTypes.string,
+}
+
+export default ConfirmDetailRow
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js
index 056afff04..056afff04 100644
--- a/ui/app/components/confirm-page-container/confirm-detail-row/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss
index 1672ef8c6..1672ef8c6 100644
--- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
+++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
index c8507985d..c8507985d 100644
--- a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
+++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
new file mode 100644
index 000000000..8a5f90c76
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -0,0 +1,110 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { Tabs, Tab } from '../../../ui/tabs'
+import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.'
+import ErrorMessage from '../../../ui/error-message'
+
+export default class ConfirmPageContainerContent extends Component {
+ static propTypes = {
+ action: PropTypes.string,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+ assetImage: PropTypes.string,
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ subtitleComponent: PropTypes.node,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ titleComponent: PropTypes.node,
+ warning: PropTypes.string,
+ }
+
+ renderContent () {
+ const { detailsComponent, dataComponent } = this.props
+
+ if (detailsComponent && dataComponent) {
+ return this.renderTabs()
+ } else {
+ return detailsComponent || dataComponent
+ }
+ }
+
+ renderTabs () {
+ const { detailsComponent, dataComponent } = this.props
+
+ return (
+ <Tabs>
+ <Tab name="Details">
+ { detailsComponent }
+ </Tab>
+ <Tab name="Data">
+ { dataComponent }
+ </Tab>
+ </Tabs>
+ )
+ }
+
+ render () {
+ const {
+ action,
+ errorKey,
+ errorMessage,
+ title,
+ titleComponent,
+ subtitle,
+ subtitleComponent,
+ hideSubtitle,
+ identiconAddress,
+ nonce,
+ assetImage,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ warning,
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content">
+ {
+ warning && (
+ <ConfirmPageContainerWarning warning={warning} />
+ )
+ }
+ {
+ summaryComponent || (
+ <ConfirmPageContainerSummary
+ className={classnames({
+ 'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
+ })}
+ action={action}
+ title={title}
+ titleComponent={titleComponent}
+ subtitle={subtitle}
+ subtitleComponent={subtitleComponent}
+ hideSubtitle={hideSubtitle}
+ identiconAddress={identiconAddress}
+ nonce={nonce}
+ assetImage={assetImage}
+ />
+ )
+ }
+ { this.renderContent() }
+ {
+ (errorKey || errorMessage) && (
+ <div className="confirm-page-container-content__error-container">
+ <ErrorMessage
+ errorMessage={errorMessage}
+ errorKey={errorKey}
+ />
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
new file mode 100644
index 000000000..0cc4d8262
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../../../../ui/identicon'
+
+const ConfirmPageContainerSummary = props => {
+ const {
+ action,
+ title,
+ titleComponent,
+ subtitle,
+ subtitleComponent,
+ hideSubtitle,
+ className,
+ identiconAddress,
+ nonce,
+ assetImage,
+ } = props
+
+ return (
+ <div className={classnames('confirm-page-container-summary', className)}>
+ <div className="confirm-page-container-summary__action-row">
+ <div className="confirm-page-container-summary__action">
+ { action }
+ </div>
+ {
+ nonce && (
+ <div className="confirm-page-container-summary__nonce">
+ { `#${nonce}` }
+ </div>
+ )
+ }
+ </div>
+ <div className="confirm-page-container-summary__title">
+ {
+ identiconAddress && (
+ <Identicon
+ className="confirm-page-container-summary__identicon"
+ diameter={36}
+ address={identiconAddress}
+ image={assetImage}
+ />
+ )
+ }
+ <div className="confirm-page-container-summary__title-text">
+ { titleComponent || title }
+ </div>
+ </div>
+ {
+ hideSubtitle || <div className="confirm-page-container-summary__subtitle">
+ { subtitleComponent || subtitle }
+ </div>
+ }
+ </div>
+ )
+}
+
+ConfirmPageContainerSummary.propTypes = {
+ action: PropTypes.string,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ titleComponent: PropTypes.node,
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ subtitleComponent: PropTypes.node,
+ hideSubtitle: PropTypes.bool,
+ 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/confirm-page-container-summary/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js
index ed1b28cf2..ed1b28cf2 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
index 7f0f5d37a..7f0f5d37a 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
index 79901c8fc..79901c8fc 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js
index 6e48bd144..6e48bd144 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss
index 50545a1a2..50545a1a2 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js
index 4dfd89d92..4dfd89d92 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss
new file mode 100644
index 000000000..602a46848
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss
@@ -0,0 +1,68 @@
+@import 'confirm-page-container-warning/index';
+
+@import 'confirm-page-container-summary/index';
+
+.confirm-page-container-content {
+ overflow-y: auto;
+ flex: 1;
+
+ &__error-container {
+ padding: 0 16px 16px 16px;
+ }
+
+ &__details {
+ box-sizing: border-box;
+ padding: 0 24px;
+ }
+
+ &__data {
+ padding: 16px;
+ color: $oslo-gray;
+ }
+
+ &__data-box {
+ background-color: #f9fafa;
+ padding: 12px;
+ font-size: .75rem;
+ margin-bottom: 16px;
+ word-wrap: break-word;
+ max-height: 200px;
+ overflow-y: auto;
+
+ &-label {
+ text-transform: uppercase;
+ padding: 8px 0 12px;
+ font-size: 12px;
+ }
+ }
+
+ &__data-field {
+ display: flex;
+ flex-direction: row;
+
+ &-label {
+ font-weight: 500;
+ padding-right: 16px;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+ }
+
+ &__gas-fee {
+ border-bottom: 1px solid $geyser;
+
+ .advanced-gas-inputs__gas-edit-rows {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__function-type {
+ font-size: .875rem;
+ font-weight: 500;
+ text-transform: capitalize;
+ color: $black;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
new file mode 100644
index 000000000..84ca40da5
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+} from '../../../../../../app/scripts/lib/enums'
+import NetworkDisplay from '../../network-display'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ showEdit: PropTypes.bool,
+ onEdit: PropTypes.func,
+ children: PropTypes.node,
+ }
+
+ renderTop () {
+ const { onEdit, showEdit } = this.props
+ const windowType = window.METAMASK_UI_TYPE
+ const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
+ windowType !== ENVIRONMENT_TYPE_POPUP
+
+ if (!showEdit && isFullScreen) {
+ return null
+ }
+
+ return (
+ <div className="confirm-page-container-header__row">
+ <div
+ className="confirm-page-container-header__back-button-container"
+ style={{
+ visibility: showEdit ? 'initial' : 'hidden',
+ }}
+ >
+ <img
+ src="/images/caret-left.svg"
+ />
+ <span
+ className="confirm-page-container-header__back-button"
+ onClick={() => onEdit()}
+ >
+ { this.context.t('edit') }
+ </span>
+ </div>
+ { !isFullScreen && <NetworkDisplay /> }
+ </div>
+ )
+ }
+
+ render () {
+ const { children } = this.props
+
+ return (
+ <div className="confirm-page-container-header">
+ { this.renderTop() }
+ { children }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js
index 71feb6931..71feb6931 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss
index be77edbdf..be77edbdf 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js
index 8327f997b..8327f997b 100755
--- a/ui/app/components/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-navigation/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js
index d97c1b447..d97c1b447 100755
--- a/ui/app/components/confirm-page-container/confirm-page-container-navigation/index.js
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-navigation/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss
index 0cf184c60..0cf184c60 100755
--- a/ui/app/components/confirm-page-container/confirm-page-container-navigation/index.scss
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js
new file mode 100644
index 000000000..326e4f83e
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js
@@ -0,0 +1,170 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SenderToRecipient from '../../ui/sender-to-recipient'
+import { PageContainerFooter } from '../../ui/page-container'
+import { ConfirmPageContainerHeader, ConfirmPageContainerContent, ConfirmPageContainerNavigation } from '.'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ // Header
+ action: PropTypes.string,
+ hideSubtitle: PropTypes.bool,
+ onEdit: PropTypes.func,
+ showEdit: PropTypes.bool,
+ subtitle: PropTypes.string,
+ subtitleComponent: PropTypes.node,
+ title: PropTypes.string,
+ titleComponent: PropTypes.node,
+ // Sender to Recipient
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ toAddress: PropTypes.string,
+ toName: PropTypes.string,
+ // Content
+ contentComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ onEditGas: PropTypes.func,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+ assetImage: PropTypes.string,
+ summaryComponent: PropTypes.node,
+ warning: PropTypes.string,
+ unapprovedTxCount: PropTypes.number,
+ // Navigation
+ totalTx: PropTypes.number,
+ positionOfCurrentTx: PropTypes.number,
+ nextTxId: PropTypes.string,
+ prevTxId: PropTypes.string,
+ showNavigation: PropTypes.bool,
+ onNextTx: PropTypes.func,
+ firstTx: PropTypes.string,
+ lastTx: PropTypes.string,
+ ofText: PropTypes.string,
+ requestsWaitingText: PropTypes.string,
+ // Footer
+ onCancelAll: PropTypes.func,
+ onCancel: PropTypes.func,
+ onSubmit: PropTypes.func,
+ disabled: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ showEdit,
+ onEdit,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ disabled,
+ errorKey,
+ errorMessage,
+ contentComponent,
+ action,
+ title,
+ titleComponent,
+ subtitle,
+ subtitleComponent,
+ hideSubtitle,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ onCancelAll,
+ onCancel,
+ onSubmit,
+ identiconAddress,
+ nonce,
+ unapprovedTxCount,
+ assetImage,
+ warning,
+ totalTx,
+ positionOfCurrentTx,
+ nextTxId,
+ prevTxId,
+ showNavigation,
+ onNextTx,
+ firstTx,
+ lastTx,
+ ofText,
+ requestsWaitingText,
+ } = this.props
+ const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
+
+ return (
+ <div className="page-container">
+ <ConfirmPageContainerNavigation
+ totalTx={totalTx}
+ positionOfCurrentTx={positionOfCurrentTx}
+ nextTxId={nextTxId}
+ prevTxId={prevTxId}
+ showNavigation={showNavigation}
+ onNextTx={(txId) => onNextTx(txId)}
+ firstTx={firstTx}
+ lastTx={lastTx}
+ ofText={ofText}
+ requestsWaitingText={requestsWaitingText}
+ />
+ <ConfirmPageContainerHeader
+ showEdit={showEdit}
+ onEdit={() => onEdit()}
+ >
+ <SenderToRecipient
+ senderName={fromName}
+ senderAddress={fromAddress}
+ recipientName={toName}
+ recipientAddress={toAddress}
+ assetImage={renderAssetImage ? assetImage : undefined}
+ />
+ </ConfirmPageContainerHeader>
+ {
+ contentComponent || (
+ <ConfirmPageContainerContent
+ action={action}
+ title={title}
+ titleComponent={titleComponent}
+ subtitle={subtitle}
+ subtitleComponent={subtitleComponent}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={detailsComponent}
+ dataComponent={dataComponent}
+ errorMessage={errorMessage}
+ 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/confirm-page-container/index.js b/ui/app/components/app/confirm-page-container/index.js
index 28b17614e..28b17614e 100644
--- a/ui/app/components/confirm-page-container/index.js
+++ b/ui/app/components/app/confirm-page-container/index.js
diff --git a/ui/app/components/app/confirm-page-container/index.scss b/ui/app/components/app/confirm-page-container/index.scss
new file mode 100644
index 000000000..c0277eff5
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/index.scss
@@ -0,0 +1,7 @@
+@import 'confirm-page-container-content/index';
+
+@import 'confirm-page-container-header/index';
+
+@import 'confirm-detail-row/index';
+
+@import 'confirm-page-container-navigation/index';
diff --git a/ui/app/components/app/copyable.js b/ui/app/components/app/copyable.js
new file mode 100644
index 000000000..6869d674d
--- /dev/null
+++ b/ui/app/components/app/copyable.js
@@ -0,0 +1,53 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+const Tooltip = require('../ui/tooltip')
+const copyToClipboard = require('copy-to-clipboard')
+const connect = require('react-redux').connect
+
+Copyable.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect()(Copyable)
+
+
+inherits(Copyable, Component)
+function Copyable () {
+ Component.call(this)
+ this.state = {
+ copied: false,
+ }
+}
+
+Copyable.prototype.render = function () {
+ const props = this.props
+ const state = this.state
+ const { value, children } = props
+ const { copied } = state
+
+ return h(Tooltip, {
+ title: copied ? this.context.t('copiedExclamation') : this.context.t('copy'),
+ position: 'bottom',
+ }, h('span', {
+ style: {
+ cursor: 'pointer',
+ },
+ onClick: (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ copyToClipboard(value)
+ this.debounceRestore()
+ },
+ }, children))
+}
+
+Copyable.prototype.debounceRestore = function () {
+ this.setState({ copied: true })
+ clearTimeout(this.timeout)
+ this.timeout = setTimeout(() => {
+ this.setState({ copied: false })
+ }, 850)
+}
diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/app/customize-gas-modal/gas-modal-card.js
index 23754d819..23754d819 100644
--- a/ui/app/components/customize-gas-modal/gas-modal-card.js
+++ b/ui/app/components/app/customize-gas-modal/gas-modal-card.js
diff --git a/ui/app/components/customize-gas-modal/gas-slider.js b/ui/app/components/app/customize-gas-modal/gas-slider.js
index 69fd6f985..69fd6f985 100644
--- a/ui/app/components/customize-gas-modal/gas-slider.js
+++ b/ui/app/components/app/customize-gas-modal/gas-slider.js
diff --git a/ui/app/components/app/customize-gas-modal/index.js b/ui/app/components/app/customize-gas-modal/index.js
new file mode 100644
index 000000000..dca77bb00
--- /dev/null
+++ b/ui/app/components/app/customize-gas-modal/index.js
@@ -0,0 +1,396 @@
+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 BigNumber = require('bignumber.js')
+const actions = require('../../../store/actions')
+const GasModalCard = require('./gas-modal-card')
+import Button from '../../ui/button'
+
+const ethUtil = require('ethereumjs-util')
+
+import {
+ updateSendErrors,
+} from '../../../ducks/send/send.duck'
+
+const {
+ MIN_GAS_PRICE_DEC,
+ MIN_GAS_LIMIT_DEC,
+ MIN_GAS_PRICE_GWEI,
+} = require('../send/send.constants')
+
+const {
+ isBalanceSufficient,
+} = require('../send/send.utils')
+
+const {
+ conversionUtil,
+ multiplyCurrencies,
+ conversionGreaterThan,
+ conversionMax,
+ subtractCurrencies,
+} = require('../../../helpers/utils/conversion-util')
+
+const {
+ getGasIsLoading,
+ getForceGasMin,
+ conversionRateSelector,
+ getSendAmount,
+ getSelectedToken,
+ getSendFrom,
+ getCurrentAccountWithSendEtherInfo,
+ getSelectedTokenToFiatRate,
+ getSendMaxModeState,
+} = require('../../../selectors/selectors')
+
+const {
+ getGasPrice,
+ getGasLimit,
+} = require('../send/send.selectors')
+
+function mapStateToProps (state) {
+ const selectedToken = getSelectedToken(state)
+ const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
+ const conversionRate = conversionRateSelector(state)
+
+ return {
+ gasPrice: getGasPrice(state),
+ gasLimit: getGasLimit(state),
+ gasIsLoading: getGasIsLoading(state),
+ forceGasMin: getForceGasMin(state),
+ conversionRate,
+ amount: getSendAmount(state),
+ maxModeOn: getSendMaxModeState(state),
+ balance: currentAccount.balance,
+ primaryCurrency: selectedToken && selectedToken.symbol,
+ selectedToken,
+ amountConversionRate: selectedToken ? getSelectedTokenToFiatRate(state) : conversionRate,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => dispatch(actions.hideModal()),
+ setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
+ setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
+ setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
+ updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
+ updateSendErrors: error => dispatch(updateSendErrors(error)),
+ }
+}
+
+function getFreshState (props) {
+ const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
+ const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ return {
+ gasPrice,
+ gasLimit,
+ gasTotal,
+ error: null,
+ priceSigZeros: '',
+ priceSigDec: '',
+ }
+}
+
+inherits(CustomizeGasModal, Component)
+function CustomizeGasModal (props) {
+ Component.call(this)
+
+ const originalState = getFreshState(props)
+ this.state = {
+ ...originalState,
+ originalState,
+ }
+}
+
+CustomizeGasModal.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
+
+CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
+ const currentState = getFreshState(this.props)
+ const {
+ gasPrice: currentGasPrice,
+ gasLimit: currentGasLimit,
+ } = currentState
+ const newState = getFreshState(nextProps)
+ const {
+ gasPrice: newGasPrice,
+ gasLimit: newGasLimit,
+ gasTotal: newGasTotal,
+ } = newState
+ const gasPriceChanged = currentGasPrice !== newGasPrice
+ const gasLimitChanged = currentGasLimit !== newGasLimit
+
+ if (gasPriceChanged) {
+ this.setState({
+ gasPrice: newGasPrice,
+ gasTotal: newGasTotal,
+ priceSigZeros: '',
+ priceSigDec: '',
+ })
+ }
+ if (gasLimitChanged) {
+ this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal })
+ }
+ if (gasLimitChanged || gasPriceChanged) {
+ this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal })
+ }
+}
+
+CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
+ const { metricsEvent } = this.context
+ const {
+ setGasPrice,
+ setGasLimit,
+ hideModal,
+ setGasTotal,
+ maxModeOn,
+ selectedToken,
+ balance,
+ updateSendAmount,
+ updateSendErrors,
+ } = this.props
+ const {
+ originalState,
+ } = this.state
+
+ if (maxModeOn && !selectedToken) {
+ const maxAmount = subtractCurrencies(
+ ethUtil.addHexPrefix(balance),
+ ethUtil.addHexPrefix(gasTotal),
+ { toNumericBase: 'hex' }
+ )
+ updateSendAmount(maxAmount)
+ }
+
+ metricsEvent({
+ eventOpts: {
+ category: 'Activation',
+ action: 'userCloses',
+ name: 'closeCustomizeGas',
+ },
+ pageOpts: {
+ section: 'customizeGasModal',
+ component: 'customizeGasSaveButton',
+ },
+ customVariables: {
+ gasPriceChange: (new BigNumber(ethUtil.addHexPrefix(gasPrice))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasPrice))).toString(10),
+ gasLimitChange: (new BigNumber(ethUtil.addHexPrefix(gasLimit))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasLimit))).toString(10),
+ },
+ })
+
+ setGasPrice(ethUtil.addHexPrefix(gasPrice))
+ setGasLimit(ethUtil.addHexPrefix(gasLimit))
+ setGasTotal(ethUtil.addHexPrefix(gasTotal))
+ updateSendErrors({ insufficientFunds: false })
+ hideModal()
+}
+
+CustomizeGasModal.prototype.revert = function () {
+ this.setState(this.state.originalState)
+}
+
+CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
+ const {
+ amount,
+ balance,
+ selectedToken,
+ amountConversionRate,
+ conversionRate,
+ maxModeOn,
+ } = this.props
+
+ let error = null
+
+ const balanceIsSufficient = isBalanceSufficient({
+ amount: selectedToken || maxModeOn ? '0' : amount,
+ gasTotal,
+ balance,
+ selectedToken,
+ amountConversionRate,
+ conversionRate,
+ })
+
+ if (!balanceIsSufficient) {
+ error = this.context.t('balanceIsInsufficientGas')
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ error = this.context.t('gasLimitTooLow')
+ }
+
+ this.setState({ error })
+ return error
+}
+
+CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
+ const { gasPrice } = this.state
+
+ const gasLimit = conversionUtil(newGasLimit, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ this.validate({ gasTotal, gasLimit })
+
+ this.setState({ gasTotal, gasLimit })
+}
+
+CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
+ const { gasLimit } = this.state
+ const sigZeros = String(newGasPrice).match(/^\d+[.]\d*?(0+)$/)
+ const sigDec = String(newGasPrice).match(/^\d+([.])0*$/)
+
+ this.setState({
+ priceSigZeros: sigZeros && sigZeros[1] || '',
+ priceSigDec: sigDec && sigDec[1] || '',
+ })
+
+ const gasPrice = conversionUtil(newGasPrice, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ })
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ this.validate({ gasTotal })
+
+ this.setState({ gasTotal, gasPrice })
+}
+
+CustomizeGasModal.prototype.render = function () {
+ const { hideModal, forceGasMin, gasIsLoading } = this.props
+ const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
+
+ let convertedGasPrice = conversionUtil(gasPrice, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+
+ convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
+
+ let newGasPrice = gasPrice
+ if (forceGasMin) {
+ const convertedMinPrice = conversionUtil(forceGasMin, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+ convertedGasPrice = conversionMax(
+ { value: convertedMinPrice, fromNumericBase: 'dec' },
+ { value: convertedGasPrice, fromNumericBase: 'dec' }
+ )
+ newGasPrice = conversionMax(
+ { value: gasPrice, fromNumericBase: 'hex' },
+ { value: forceGasMin, fromNumericBase: 'hex' }
+ )
+ }
+
+ const convertedGasLimit = conversionUtil(gasLimit, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+
+ return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
+ h('div.send-v2__customize-gas__content', {
+ }, [
+ h('div.send-v2__customize-gas__header', {}, [
+
+ h('div.send-v2__customize-gas__title', this.context.t('customGas')),
+
+ h('div.send-v2__customize-gas__close', {
+ onClick: hideModal,
+ }),
+
+ ]),
+
+ h('div.send-v2__customize-gas__body', {}, [
+
+ h(GasModalCard, {
+ value: convertedGasPrice,
+ min: forceGasMin || MIN_GAS_PRICE_GWEI,
+ step: 1,
+ onChange: value => this.convertAndSetGasPrice(value),
+ title: this.context.t('gasPrice'),
+ copy: this.context.t('gasPriceCalculation'),
+ gasIsLoading,
+ }),
+
+ h(GasModalCard, {
+ value: convertedGasLimit,
+ min: 1,
+ step: 1,
+ onChange: value => this.convertAndSetGasLimit(value),
+ title: this.context.t('gasLimit'),
+ copy: this.context.t('gasLimitCalculation'),
+ gasIsLoading,
+ }),
+
+ ]),
+
+ h('div.send-v2__customize-gas__footer', {}, [
+
+ error && h('div.send-v2__customize-gas__error-message', [
+ error,
+ ]),
+
+ h('div.send-v2__customize-gas__revert', {
+ onClick: () => this.revert(),
+ }, [this.context.t('revert')]),
+
+ h('div.send-v2__customize-gas__buttons', [
+ h(Button, {
+ type: 'default',
+ className: 'send-v2__customize-gas__cancel',
+ onClick: this.props.hideModal,
+ }, [this.context.t('cancel')]),
+ h(Button, {
+ type: 'primary',
+ className: 'send-v2__customize-gas__save',
+ onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
+ disabled: error,
+ }, [this.context.t('save')]),
+ ]),
+
+ ]),
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/app/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js
new file mode 100644
index 000000000..3d4598946
--- /dev/null
+++ b/ui/app/components/app/dropdowns/account-details-dropdown.js
@@ -0,0 +1,131 @@
+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('../../../store/actions')
+const { getSelectedIdentity } = require('../../../selectors/selectors')
+const genAccountLink = require('../../../../lib/account-link.js')
+const { Menu, Item, CloseArea } = require('./components/menu')
+
+AccountDetailsDropdown.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: 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()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Account Options',
+ name: 'Clicked Expand View',
+ },
+ })
+ 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.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Account Options',
+ name: 'Viewed Account Details',
+ },
+ })
+ this.props.onClose()
+ },
+ text: this.context.t('accountDetails'),
+ icon: h(`img`, { src: 'images/info.svg', style: { height: '15px' } }),
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Account Options',
+ name: 'Clicked View on Etherscan',
+ },
+ })
+ 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/app/dropdowns/components/account-dropdowns.js b/ui/app/components/app/dropdowns/components/account-dropdowns.js
new file mode 100644
index 000000000..c603a9a9f
--- /dev/null
+++ b/ui/app/components/app/dropdowns/components/account-dropdowns.js
@@ -0,0 +1,473 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const actions = require('../../../../store/actions')
+const genAccountLink = require('../../../../../lib/account-link.js')
+const connect = require('react-redux').connect
+const Dropdown = require('./dropdown').Dropdown
+const DropdownMenuItem = require('./dropdown').DropdownMenuItem
+import Identicon from '../../../ui/identicon'
+const { checksumAddress } = require('../../../../helpers/utils/util')
+const copyToClipboard = require('copy-to-clipboard')
+const { formatBalance } = require('../../../../helpers/utils/util')
+
+
+class AccountDropdowns extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ accountSelectorActive: false,
+ optionsMenuActive: false,
+ }
+ // Used for orangeaccount selector icon
+ // this.accountSelectorToggleClassName = 'accounts-selector'
+ this.accountSelectorToggleClassName = 'fa-angle-down'
+ this.optionsMenuToggleClassName = 'fa-ellipsis-h'
+ }
+
+ renderAccounts () {
+ const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props
+
+ return Object.keys(identities).map((key, index) => {
+ const identity = identities[key]
+ const isSelected = identity.address === selected
+
+ const balanceValue = accounts[key].balance
+ const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, true, ticker) : '...'
+ const simpleAddress = identity.address.substring(2).toLowerCase()
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(simpleAddress) ||
+ kr.accounts.includes(identity.address)
+ })
+
+ return h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ this.props.actions.showAccountDetail(identity.address)
+ },
+ style: Object.assign(
+ {
+ marginTop: index === 0 ? '5px' : '',
+ fontSize: '24px',
+ width: '260px',
+ },
+ menuItemStyles,
+ ),
+ },
+ [
+ h('div.flex-row.flex-center', {}, [
+
+ h('span', {
+ style: {
+ flex: '1 1 0',
+ minWidth: '20px',
+ minHeight: '30px',
+ },
+ }, [
+ h('span', {
+ style: {
+ flex: '1 1 auto',
+ fontSize: '14px',
+ },
+ }, isSelected ? h('i.fa.fa-check') : null),
+ ]),
+
+ h(
+ Identicon,
+ {
+ address: identity.address,
+ diameter: 24,
+ style: {
+ flex: '1 1 auto',
+ marginLeft: '10px',
+ },
+ },
+ ),
+
+ h('span.flex-column', {
+ style: {
+ flex: '10 10 auto',
+ width: '175px',
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ marginLeft: '10px',
+ position: 'relative',
+ },
+ }, [
+ this.indicateIfLoose(keyring),
+ h('span.account-dropdown-name', {
+ style: {
+ fontSize: '18px',
+ maxWidth: '145px',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ }, identity.name || ''),
+
+ h('span.account-dropdown-balance', {
+ style: {
+ fontSize: '14px',
+ fontFamily: 'Avenir',
+ fontWeight: 500,
+ },
+ }, formattedBalance),
+ ]),
+
+ h('span', {
+ style: {
+ flex: '3 3 auto',
+ },
+ }, [
+ h('span.account-dropdown-edit-button.allcaps', {
+ style: {
+ fontSize: '16px',
+ },
+ onClick: () => {
+ actions.showEditAccountModal(identity)
+ },
+ }, [
+ this.context.t('edit'),
+ ]),
+ ]),
+
+ ]),
+ ]
+ )
+ })
+ }
+
+ indicateIfLoose (keyring) {
+ try { // Sometimes keyrings aren't loaded yet:
+ const type = keyring.type
+ const isLoose = type !== 'HD Key Tree'
+ return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null
+ } catch (e) { return }
+ }
+
+ renderAccountSelector () {
+ const { actions, useCssTransition, innerStyle, sidebarOpen } = this.props
+ const { accountSelectorActive, menuItemStyles } = this.state
+
+ return h(
+ Dropdown,
+ {
+ useCssTransition,
+ style: {
+ marginLeft: '-185px',
+ marginTop: '50px',
+ minWidth: '180px',
+ overflowY: 'auto',
+ maxHeight: '300px',
+ width: '300px',
+ },
+ innerStyle,
+ isOpen: accountSelectorActive,
+ onClickOutside: (event) => {
+ const { classList } = event.target
+ const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
+ if (accountSelectorActive && isNotToggleElement) {
+ this.setState({ accountSelectorActive: false })
+ }
+ },
+ },
+ [
+ ...this.renderAccounts(),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ style: Object.assign(
+ {},
+ menuItemStyles,
+ ),
+ onClick: () => actions.showNewAccountPageCreateForm(),
+ },
+ [
+ h(
+ 'i.fa.fa-plus.fa-lg',
+ {
+ style: {
+ marginLeft: '8px',
+ },
+ }
+ ),
+ h('span', {
+ style: {
+ marginLeft: '14px',
+ fontFamily: 'DIN OT',
+ fontSize: '16px',
+ lineHeight: '23px',
+ },
+ }, this.context.t('createAccount')),
+ ],
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {
+ if (sidebarOpen) {
+ actions.hideSidebar()
+ }
+ },
+ onClick: () => actions.showNewAccountPageImportForm(),
+ style: Object.assign(
+ {},
+ menuItemStyles,
+ ),
+ },
+ [
+ h(
+ 'i.fa.fa-download.fa-lg',
+ {
+ style: {
+ marginLeft: '8px',
+ },
+ }
+ ),
+ h('span', {
+ style: {
+ marginLeft: '20px',
+ marginBottom: '5px',
+ fontFamily: 'DIN OT',
+ fontSize: '16px',
+ lineHeight: '23px',
+ },
+ }, this.context.t('importAccount')),
+ ]
+ ),
+ ]
+ )
+ }
+
+ renderAccountOptions () {
+ const { actions, dropdownWrapperStyle, useCssTransition } = this.props
+ const { optionsMenuActive, menuItemStyles } = this.state
+ const dropdownMenuItemStyle = {
+ fontFamily: 'DIN OT',
+ fontSize: 16,
+ lineHeight: '24px',
+ padding: '8px',
+ }
+
+ return h(
+ Dropdown,
+ {
+ useCssTransition,
+ style: Object.assign(
+ {
+ marginLeft: '-10px',
+ position: 'absolute',
+ width: '29vh', // affects both mobile and laptop views
+ },
+ dropdownWrapperStyle,
+ ),
+ isOpen: optionsMenuActive,
+ onClickOutside: (event) => {
+ const { classList } = event.target
+ const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
+ if (optionsMenuActive && isNotToggleElement) {
+ this.setState({ optionsMenuActive: false })
+ }
+ },
+ },
+ [
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ this.props.actions.showAccountDetailModal()
+ },
+ style: Object.assign(
+ dropdownMenuItemStyle,
+ menuItemStyles,
+ ),
+ },
+ this.context.t('accountDetails'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ const { selected, network } = this.props
+ const url = genAccountLink(selected, network)
+ global.platform.openWindow({ url })
+ },
+ style: Object.assign(
+ dropdownMenuItemStyle,
+ menuItemStyles,
+ ),
+ },
+ this.context.t('etherscanView'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ const { selected } = this.props
+ copyToClipboard(checksumAddress(selected))
+ },
+ style: Object.assign(
+ dropdownMenuItemStyle,
+ menuItemStyles,
+ ),
+ },
+ this.context.t('copyAddress'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => this.props.actions.showExportPrivateKeyModal(),
+ style: Object.assign(
+ dropdownMenuItemStyle,
+ menuItemStyles,
+ ),
+ },
+ this.context.t('exportPrivateKey'),
+ ),
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => {},
+ onClick: () => {
+ actions.hideSidebar()
+ actions.showAddTokenPage()
+ },
+ style: Object.assign(
+ dropdownMenuItemStyle,
+ menuItemStyles,
+ ),
+ },
+ this.context.t('addToken'),
+ ),
+
+ ]
+ )
+ }
+
+ render () {
+ const { style, enableAccountsSelector, enableAccountOptions } = this.props
+ const { optionsMenuActive, accountSelectorActive } = this.state
+
+ return h(
+ 'span',
+ {
+ style: style,
+ },
+ [
+ enableAccountsSelector && h(
+ 'i.fa.fa-angle-down',
+ {
+ style: {
+ cursor: 'pointer',
+ },
+ onClick: (event) => {
+ event.stopPropagation()
+ this.setState({
+ accountSelectorActive: !accountSelectorActive,
+ optionsMenuActive: false,
+ })
+ },
+ },
+ this.renderAccountSelector(),
+ ),
+ enableAccountOptions && h(
+ 'i.fa.fa-ellipsis-h',
+ {
+ style: {
+ fontSize: '135%',
+ cursor: 'pointer',
+ },
+ onClick: (event) => {
+ event.stopPropagation()
+ this.setState({
+ accountSelectorActive: false,
+ optionsMenuActive: !optionsMenuActive,
+ })
+ },
+ },
+ this.renderAccountOptions()
+ ),
+ ]
+ )
+ }
+}
+
+AccountDropdowns.defaultProps = {
+ enableAccountsSelector: false,
+ enableAccountOptions: false,
+}
+
+AccountDropdowns.propTypes = {
+ identities: PropTypes.objectOf(PropTypes.object),
+ selected: PropTypes.string,
+ keyrings: PropTypes.array,
+ accounts: PropTypes.object,
+ menuItemStyles: PropTypes.object,
+ actions: PropTypes.object,
+ // actions.showAccountDetail: ,
+ useCssTransition: PropTypes.bool,
+ innerStyle: PropTypes.object,
+ sidebarOpen: PropTypes.bool,
+ dropdownWrapperStyle: PropTypes.string,
+ // actions.showAccountDetailModal: ,
+ network: PropTypes.number,
+ // actions.showExportPrivateKeyModal: ,
+ style: PropTypes.object,
+ ticker: PropTypes.string,
+ enableAccountsSelector: PropTypes.bool,
+ enableAccountOption: PropTypes.bool,
+ enableAccountOptions: PropTypes.bool,
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ actions: {
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ showConfigPage: () => dispatch(actions.showConfigPage()),
+ showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ showEditAccountModal: (identity) => {
+ dispatch(actions.showModal({
+ name: 'EDIT_ACCOUNT_NAME',
+ identity,
+ }))
+ },
+ showNewAccountPageCreateForm: () => dispatch(actions.showNewAccountPage({ form: 'CREATE' })),
+ showExportPrivateKeyModal: () => {
+ dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
+ },
+ showAddTokenPage: () => {
+ dispatch(actions.showAddTokenPage())
+ },
+ addNewAccount: () => dispatch(actions.addNewAccount()),
+ showNewAccountPageImportForm: () => dispatch(actions.showNewAccountPage({ form: 'IMPORT' })),
+ showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
+ },
+ }
+}
+
+function mapStateToProps (state) {
+ return {
+ ticker: state.metamask.ticker,
+ keyrings: state.metamask.keyrings,
+ sidebarOpen: state.appState.sidebar.isOpen,
+ }
+}
+
+AccountDropdowns.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns)
+
diff --git a/ui/app/components/dropdowns/components/dropdown.js b/ui/app/components/app/dropdowns/components/dropdown.js
index 149f063a7..149f063a7 100644
--- a/ui/app/components/dropdowns/components/dropdown.js
+++ b/ui/app/components/app/dropdowns/components/dropdown.js
diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/app/dropdowns/components/menu.js
index f6d8a139e..f6d8a139e 100644
--- a/ui/app/components/dropdowns/components/menu.js
+++ b/ui/app/components/app/dropdowns/components/menu.js
diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js
index d4a2c2ff7..d4a2c2ff7 100644
--- a/ui/app/components/dropdowns/components/network-dropdown-icon.js
+++ b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js
diff --git a/ui/app/components/dropdowns/index.js b/ui/app/components/app/dropdowns/index.js
index 605507058..605507058 100644
--- a/ui/app/components/dropdowns/index.js
+++ b/ui/app/components/app/dropdowns/index.js
diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js
new file mode 100644
index 000000000..3d9037a06
--- /dev/null
+++ b/ui/app/components/app/dropdowns/network-dropdown.js
@@ -0,0 +1,378 @@
+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 { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const actions = require('../../../store/actions')
+const Dropdown = require('./components/dropdown').Dropdown
+const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
+const NetworkDropdownIcon = require('./components/network-dropdown-icon')
+const R = require('ramda')
+const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes')
+
+// classes from nodes of the toggle element.
+const notToggleElementClassnames = [
+ 'menu-icon',
+ 'network-name',
+ 'network-indicator',
+ 'network-caret',
+ 'network-component',
+]
+
+function mapStateToProps (state) {
+ return {
+ provider: state.metamask.provider,
+ frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
+ networkDropdownOpen: state.appState.networkDropdownOpen,
+ network: state.metamask.network,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ setProviderType: (type) => {
+ dispatch(actions.setProviderType(type))
+ },
+ setDefaultRpcTarget: type => {
+ dispatch(actions.setDefaultRpcTarget(type))
+ },
+ setRpcTarget: (target, network, ticker, nickname) => {
+ dispatch(actions.setRpcTarget(target, network, ticker, nickname))
+ },
+ delRpcTarget: (target) => {
+ dispatch(actions.delRpcTarget(target))
+ },
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
+ }
+}
+
+
+inherits(NetworkDropdown, Component)
+function NetworkDropdown () {
+ Component.call(this)
+}
+
+NetworkDropdown.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(NetworkDropdown)
+
+
+// TODO: specify default props and proptypes
+NetworkDropdown.prototype.render = function () {
+ const props = this.props
+ const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
+ const rpcListDetail = props.frequentRpcListDetail
+ const isOpen = this.props.networkDropdownOpen
+ const dropdownMenuItemStyle = {
+ fontSize: '16px',
+ lineHeight: '20px',
+ padding: '12px 0',
+ }
+
+ return h(Dropdown, {
+ isOpen,
+ onClickOutside: (event) => {
+ const { classList } = event.target
+ const isInClassList = className => classList.contains(className)
+ const notToggleElementIndex = R.findIndex(isInClassList)(notToggleElementClassnames)
+
+ if (notToggleElementIndex === -1) {
+ this.props.hideNetworkDropdown()
+ }
+ },
+ containerClassName: 'network-droppo',
+ zIndex: 55,
+ style: {
+ position: 'absolute',
+ top: '58px',
+ width: '309px',
+ zIndex: '55px',
+ },
+ innerStyle: {
+ padding: '18px 8px',
+ },
+ }, [
+
+ h('div.network-dropdown-header', {}, [
+ h('div.network-dropdown-title', {}, this.context.t('networks')),
+
+ h('div.network-dropdown-divider'),
+
+ h('div.network-dropdown-content',
+ {},
+ this.context.t('defaultNetwork')
+ ),
+ ]),
+
+ h(
+ DropdownMenuItem,
+ {
+ key: 'main',
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.handleClick('mainnet'),
+ style: { ...dropdownMenuItemStyle, borderColor: '#038789' },
+ },
+ [
+ providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#29B6AF', // $java
+ isSelected: providerType === 'mainnet',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: providerType === 'mainnet' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('mainnet')),
+ ]
+ ),
+
+ h(
+ DropdownMenuItem,
+ {
+ key: 'ropsten',
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.handleClick('ropsten'),
+ style: dropdownMenuItemStyle,
+ },
+ [
+ providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#ff4a8d', // $wild-strawberry
+ isSelected: providerType === 'ropsten',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: providerType === 'ropsten' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('ropsten')),
+ ]
+ ),
+
+ h(
+ DropdownMenuItem,
+ {
+ key: 'kovan',
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.handleClick('kovan'),
+ style: dropdownMenuItemStyle,
+ },
+ [
+ providerType === 'kovan' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#7057ff', // $cornflower-blue
+ isSelected: providerType === 'kovan',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: providerType === 'kovan' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('kovan')),
+ ]
+ ),
+
+ h(
+ DropdownMenuItem,
+ {
+ key: 'rinkeby',
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.handleClick('rinkeby'),
+ style: dropdownMenuItemStyle,
+ },
+ [
+ providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#f6c343', // $saffron
+ isSelected: providerType === 'rinkeby',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: providerType === 'rinkeby' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('rinkeby')),
+ ]
+ ),
+
+ h(
+ DropdownMenuItem,
+ {
+ key: 'default',
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.handleClick('localhost'),
+ style: dropdownMenuItemStyle,
+ },
+ [
+ providerType === 'localhost' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ isSelected: providerType === 'localhost',
+ innerBorder: '1px solid #9b9b9b',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: providerType === 'localhost' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('localhost')),
+ ]
+ ),
+
+ this.renderCustomOption(props.provider),
+ this.renderCommonRpc(rpcListDetail, props.provider),
+
+ h(
+ DropdownMenuItem,
+ {
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => this.props.history.push(ADVANCED_ROUTE),
+ style: dropdownMenuItemStyle,
+ },
+ [
+ activeNetwork === 'custom' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ h(NetworkDropdownIcon, {
+ isSelected: activeNetwork === 'custom',
+ innerBorder: '1px solid #9b9b9b',
+ }),
+ h('span.network-name-item', {
+ style: {
+ color: activeNetwork === 'custom' ? '#ffffff' : '#9b9b9b',
+ },
+ }, this.context.t('customRPC')),
+ ]
+ ),
+
+ ])
+}
+
+NetworkDropdown.prototype.handleClick = function (newProviderType) {
+ const { provider: { type: providerType }, setProviderType } = this.props
+ const { metricsEvent } = this.context
+
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Switched Networks',
+ },
+ customVariables: {
+ fromNetwork: providerType,
+ toNetwork: newProviderType,
+ },
+ })
+ setProviderType(newProviderType)
+}
+
+NetworkDropdown.prototype.getNetworkName = function () {
+ const { provider } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('mainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('ropsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('kovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('rinkeby')
+ } else {
+ name = provider.nickname || this.context.t('unknownNetwork')
+ }
+
+ return name
+}
+
+NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) {
+ const props = this.props
+ const reversedRpcListDetail = rpcListDetail.slice().reverse()
+
+ return reversedRpcListDetail.map((entry) => {
+ const rpc = entry.rpcUrl
+ const ticker = entry.ticker || 'ETH'
+ const nickname = entry.nickname || ''
+ const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
+
+ if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
+ return null
+ } else {
+ const chainId = entry.chainId
+ return h(
+ DropdownMenuItem,
+ {
+ key: `common${rpc}`,
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname),
+ style: {
+ fontSize: '16px',
+ lineHeight: '20px',
+ padding: '12px 0',
+ },
+ },
+ [
+ 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: currentRpcTarget ? '#ffffff' : '#9b9b9b',
+ },
+ }, nickname || rpc),
+ h('i.fa.fa-times.delete',
+ {
+ onClick: (e) => {
+ e.stopPropagation()
+ props.delRpcTarget(rpc)
+ },
+ }),
+ ]
+ )
+ }
+ })
+}
+
+NetworkDropdown.prototype.renderCustomOption = function (provider) {
+ const { rpcTarget, type, ticker, nickname } = provider
+ const props = this.props
+ const network = props.network
+
+ if (type !== 'rpc') return null
+
+ switch (rpcTarget) {
+
+ case 'http://localhost:8545':
+ return null
+
+ default:
+ return h(
+ DropdownMenuItem,
+ {
+ key: rpcTarget,
+ onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname),
+ closeMenu: () => this.props.hideNetworkDropdown(),
+ style: {
+ fontSize: '16px',
+ lineHeight: '20px',
+ padding: '12px 0',
+ },
+ },
+ [
+ h('i.fa.fa-check'),
+ h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
+ h('span.network-name-item', {
+ style: {
+ color: '#ffffff',
+ },
+ }, nickname || rpcTarget),
+ ]
+ )
+ }
+}
diff --git a/ui/app/components/dropdowns/simple-dropdown.js b/ui/app/components/app/dropdowns/simple-dropdown.js
index bba088ed1..bba088ed1 100644
--- a/ui/app/components/dropdowns/simple-dropdown.js
+++ b/ui/app/components/app/dropdowns/simple-dropdown.js
diff --git a/ui/app/components/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/tests/dropdown.test.js
index 2b026589a..2b026589a 100644
--- a/ui/app/components/dropdowns/tests/dropdown.test.js
+++ b/ui/app/components/app/dropdowns/tests/dropdown.test.js
diff --git a/ui/app/components/dropdowns/tests/menu.test.js b/ui/app/components/app/dropdowns/tests/menu.test.js
index 9f5f13f00..9f5f13f00 100644
--- a/ui/app/components/dropdowns/tests/menu.test.js
+++ b/ui/app/components/app/dropdowns/tests/menu.test.js
diff --git a/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js
index 67b192c11..67b192c11 100644
--- a/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js
+++ b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js
diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js
new file mode 100644
index 000000000..91e7899a7
--- /dev/null
+++ b/ui/app/components/app/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',
+ },
+ frequentRpcListDetail: [
+ { rpcUrl: '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/app/dropdowns/token-menu-dropdown.js b/ui/app/components/app/dropdowns/token-menu-dropdown.js
new file mode 100644
index 000000000..e2730aea2
--- /dev/null
+++ b/ui/app/components/app/dropdowns/token-menu-dropdown.js
@@ -0,0 +1,68 @@
+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('../../../store/actions')
+const genAccountLink = require('etherscan-link').createAccountLink
+const { Menu, Item, CloseArea } = require('./components/menu')
+
+TokenMenuDropdown.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown)
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showHideTokenConfirmationModal: (token) => {
+ dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))
+ },
+ }
+}
+
+
+inherits(TokenMenuDropdown, Component)
+function TokenMenuDropdown () {
+ Component.call(this)
+
+ this.onClose = this.onClose.bind(this)
+}
+
+TokenMenuDropdown.prototype.onClose = function (e) {
+ e.stopPropagation()
+ this.props.onClose()
+}
+
+TokenMenuDropdown.prototype.render = function () {
+ const { showHideTokenConfirmationModal } = this.props
+
+ return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [
+ h(CloseArea, {
+ onClick: this.onClose,
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ showHideTokenConfirmationModal(this.props.token)
+ this.props.onClose()
+ },
+ text: this.context.t('hideToken'),
+ }),
+ h(Item, {
+ onClick: (e) => {
+ e.stopPropagation()
+ const url = genAccountLink(this.props.token.address, this.props.network)
+ global.platform.openWindow({ url })
+ this.props.onClose()
+ },
+ text: this.context.t('viewOnEtherscan'),
+ }),
+ ])
+}
diff --git a/ui/app/components/app/ens-input.js b/ui/app/components/app/ens-input.js
new file mode 100644
index 000000000..274058a1b
--- /dev/null
+++ b/ui/app/components/app/ens-input.js
@@ -0,0 +1,181 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const extend = require('xtend')
+const debounce = require('debounce')
+const copyToClipboard = require('copy-to-clipboard')
+const ENS = require('ethjs-ens')
+const networkMap = require('ethjs-ens/lib/network-map.json')
+const ensRE = /.+\..+$/
+const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
+const connect = require('react-redux').connect
+const ToAutoComplete = require('./send/to-autocomplete').default
+const log = require('loglevel')
+const { isValidENSAddress } = require('../../helpers/utils/util')
+
+EnsInput.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect()(EnsInput)
+
+
+inherits(EnsInput, Component)
+function EnsInput () {
+ Component.call(this)
+}
+
+EnsInput.prototype.onChange = function (recipient) {
+
+ const network = this.props.network
+ const networkHasEnsSupport = getNetworkEnsSupport(network)
+
+ this.props.onChange({ toAddress: recipient })
+
+ if (!networkHasEnsSupport) return
+
+ if (recipient.match(ensRE) === null) {
+ return this.setState({
+ loadingEns: false,
+ ensResolution: null,
+ ensFailure: null,
+ toError: null,
+ })
+ }
+
+ this.setState({
+ loadingEns: true,
+ })
+ this.checkName(recipient)
+}
+
+EnsInput.prototype.render = function () {
+ const props = this.props
+ const opts = extend(props, {
+ list: 'addresses',
+ onChange: this.onChange.bind(this),
+ qrScanner: true,
+ })
+ return h('div', {
+ style: { width: '100%', position: 'relative' },
+ }, [
+ h(ToAutoComplete, { ...opts }),
+ this.ensIcon(),
+ ])
+}
+
+EnsInput.prototype.componentDidMount = function () {
+ const network = this.props.network
+ const networkHasEnsSupport = getNetworkEnsSupport(network)
+ this.setState({ ensResolution: ZERO_ADDRESS })
+
+ if (networkHasEnsSupport) {
+ const provider = global.ethereumProvider
+ this.ens = new ENS({ provider, network })
+ this.checkName = debounce(this.lookupEnsName.bind(this), 200)
+ }
+}
+
+EnsInput.prototype.lookupEnsName = function (recipient) {
+ const { ensResolution } = this.state
+
+ log.info(`ENS attempting to resolve name: ${recipient}`)
+ this.ens.lookup(recipient.trim())
+ .then((address) => {
+ if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName'))
+ if (address !== ensResolution) {
+ this.setState({
+ loadingEns: false,
+ ensResolution: address,
+ nickname: recipient.trim(),
+ hoverText: address + '\n' + this.context.t('clickCopy'),
+ ensFailure: false,
+ toError: null,
+ })
+ }
+ })
+ .catch((reason) => {
+ const setStateObj = {
+ loadingEns: false,
+ ensResolution: recipient,
+ ensFailure: true,
+ toError: null,
+ }
+ if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
+ setStateObj.hoverText = this.context.t('ensNameNotFound')
+ setStateObj.toError = 'ensNameNotFound'
+ setStateObj.ensFailure = false
+ } else {
+ log.error(reason)
+ setStateObj.hoverText = reason.message
+ }
+
+ return this.setState(setStateObj)
+ })
+}
+
+EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
+ const state = this.state || {}
+ const ensResolution = state.ensResolution
+ // If an address is sent without a nickname, meaning not from ENS or from
+ // the user's own accounts, a default of a one-space string is used.
+ const nickname = state.nickname || ' '
+ if (prevProps.network !== this.props.network) {
+ const provider = global.ethereumProvider
+ this.ens = new ENS({ provider, network: this.props.network })
+ this.onChange(ensResolution)
+ }
+ if (prevState && ensResolution && this.props.onChange &&
+ ensResolution !== prevState.ensResolution) {
+ this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
+ }
+}
+
+EnsInput.prototype.ensIcon = function (recipient) {
+ const { hoverText } = this.state || {}
+ return h('span.#ensIcon', {
+ title: hoverText,
+ style: {
+ position: 'absolute',
+ top: '16px',
+ left: '-25px',
+ },
+ }, this.ensIconContents(recipient))
+}
+
+EnsInput.prototype.ensIconContents = function (recipient) {
+ const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
+
+ if (toError) return
+
+ if (loadingEns) {
+ return h('img', {
+ src: 'images/loading.svg',
+ style: {
+ width: '30px',
+ height: '30px',
+ transform: 'translateY(-6px)',
+ },
+ })
+ }
+
+ if (ensFailure) {
+ return h('i.fa.fa-warning.fa-lg.warning')
+ }
+
+ if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
+ return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
+ style: { color: 'green' },
+ onClick: (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ copyToClipboard(ensResolution)
+ },
+ })
+ }
+}
+
+function getNetworkEnsSupport (network) {
+ return Boolean(networkMap[network])
+}
diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
index 95894140c..95894140c 100644
--- a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
new file mode 100644
index 000000000..90fef1a1b
--- /dev/null
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
@@ -0,0 +1,38 @@
+import { connect } from 'react-redux'
+import { showModal } from '../../../../store/actions'
+import {
+ decGWEIToHexWEI,
+ decimalToHex,
+ hexWEIToDecGWEI,
+} from '../../../../helpers/utils/conversions.util'
+import AdvancedGasInputs from './advanced-gas-inputs.component'
+
+function convertGasPriceForInputs (gasPriceInHexWEI) {
+ return Number(hexWEIToDecGWEI(gasPriceInHexWEI))
+}
+
+function convertGasLimitForInputs (gasLimitInHexWEI) {
+ return parseInt(gasLimitInHexWEI, 16)
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })),
+ showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const {customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit} = ownProps
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ customGasPrice: convertGasPriceForInputs(customGasPrice),
+ customGasLimit: convertGasLimitForInputs(customGasLimit),
+ updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)),
+ updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)),
+ }
+}
+
+export default connect(null, mapDispatchToProps, mergeProps)(AdvancedGasInputs)
diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/index.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js
index bd8abaa3e..bd8abaa3e 100644
--- a/ui/app/components/gas-customization/advanced-gas-inputs/index.js
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js
diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/index.scss b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss
index 50953cbe5..50953cbe5 100644
--- a/ui/app/components/gas-customization/advanced-gas-inputs/index.scss
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
new file mode 100644
index 000000000..ad8628621
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
@@ -0,0 +1,226 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Loading from '../../../../ui/loading-screen'
+import GasPriceChart from '../../gas-price-chart'
+import debounce from 'lodash.debounce'
+
+export default class AdvancedTabContent extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ updateCustomGasPrice: PropTypes.func,
+ updateCustomGasLimit: PropTypes.func,
+ customGasPrice: PropTypes.number,
+ customGasLimit: PropTypes.number,
+ gasEstimatesLoading: PropTypes.bool,
+ millisecondsRemaining: PropTypes.number,
+ transactionFee: PropTypes.string,
+ timeRemaining: PropTypes.string,
+ gasChartProps: PropTypes.object,
+ insufficientBalance: PropTypes.bool,
+ customPriceIsSafe: PropTypes.bool,
+ isSpeedUp: PropTypes.bool,
+ isEthereumNetwork: PropTypes.bool,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.debouncedGasLimitReset = debounce((dVal) => {
+ if (dVal < 21000) {
+ props.updateCustomGasLimit(21000)
+ }
+ }, 1000, { trailing: true })
+ this.onChangeGasLimit = (val) => {
+ props.updateCustomGasLimit(val)
+ this.debouncedGasLimitReset(val)
+ }
+ }
+
+ gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) {
+ const { t } = this.context
+ let errorText
+ let errorType
+ let isInError = true
+
+
+ if (insufficientBalance) {
+ errorText = t('insufficientBalance')
+ errorType = 'error'
+ } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) {
+ errorText = t('zeroGasPriceOnSpeedUpError')
+ errorType = 'error'
+ } else if (labelKey === 'gasPrice' && !customPriceIsSafe) {
+ errorText = t('gasPriceExtremelyLow')
+ errorType = 'warning'
+ } else {
+ isInError = false
+ }
+
+ return {
+ isInError,
+ errorText,
+ errorType,
+ }
+ }
+
+ gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) {
+ const {
+ isInError,
+ errorText,
+ errorType,
+ } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value })
+
+ return (
+ <div className="advanced-tab__gas-edit-row__input-wrapper">
+ <input
+ className={classnames('advanced-tab__gas-edit-row__input', {
+ 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
+ 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
+ })}
+ type="number"
+ value={value}
+ onChange={event => onChange(Number(event.target.value))}
+ />
+ <div className={classnames('advanced-tab__gas-edit-row__input-arrows', {
+ 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
+ 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
+ })}>
+ <div
+ className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
+ onClick={() => onChange(value + 1)}
+ >
+ <i className="fa fa-sm fa-angle-up" />
+ </div>
+ <div
+ className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
+ onClick={() => onChange(Math.max(value - 1, 0))}
+ >
+ <i className="fa fa-sm fa-angle-down" />
+ </div>
+ </div>
+ { isInError
+ ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}>
+ { errorText }
+ </div>
+ : null }
+ </div>
+ )
+ }
+
+ infoButton (onClick) {
+ return <i className="fa fa-info-circle" onClick={onClick} />
+ }
+
+ renderDataSummary (transactionFee, timeRemaining) {
+ return (
+ <div className="advanced-tab__transaction-data-summary">
+ <div className="advanced-tab__transaction-data-summary__titles">
+ <span>{ this.context.t('newTransactionFee') }</span>
+ <span>~{ this.context.t('transactionTime') }</span>
+ </div>
+ <div className="advanced-tab__transaction-data-summary__container">
+ <div className="advanced-tab__transaction-data-summary__fee">
+ {transactionFee}
+ </div>
+ <div className="time-remaining">{timeRemaining}</div>
+ </div>
+ </div>
+ )
+ }
+
+ renderGasEditRow (gasInputArgs) {
+ return (
+ <div className="advanced-tab__gas-edit-row">
+ <div className="advanced-tab__gas-edit-row__label">
+ { this.context.t(gasInputArgs.labelKey) }
+ { this.infoButton(() => {}) }
+ </div>
+ { this.gasInput(gasInputArgs) }
+ </div>
+ )
+ }
+
+ renderGasEditRows ({
+ customGasPrice,
+ updateCustomGasPrice,
+ customGasLimit,
+ updateCustomGasLimit,
+ insufficientBalance,
+ customPriceIsSafe,
+ isSpeedUp,
+ }) {
+ return (
+ <div className="advanced-tab__gas-edit-rows">
+ { this.renderGasEditRow({
+ labelKey: 'gasPrice',
+ value: customGasPrice,
+ onChange: updateCustomGasPrice,
+ insufficientBalance,
+ customPriceIsSafe,
+ showGWEI: true,
+ isSpeedUp,
+ }) }
+ { this.renderGasEditRow({
+ labelKey: 'gasLimit',
+ value: customGasLimit,
+ onChange: this.onChangeGasLimit,
+ insufficientBalance,
+ customPriceIsSafe,
+ }) }
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const {
+ updateCustomGasPrice,
+ updateCustomGasLimit,
+ timeRemaining,
+ customGasPrice,
+ customGasLimit,
+ insufficientBalance,
+ gasChartProps,
+ gasEstimatesLoading,
+ customPriceIsSafe,
+ isSpeedUp,
+ transactionFee,
+ isEthereumNetwork,
+ } = this.props
+
+ return (
+ <div className="advanced-tab">
+ { this.renderDataSummary(transactionFee, timeRemaining) }
+ <div className="advanced-tab__fee-chart">
+ { this.renderGasEditRows({
+ customGasPrice,
+ updateCustomGasPrice,
+ customGasLimit,
+ updateCustomGasLimit,
+ insufficientBalance,
+ customPriceIsSafe,
+ isSpeedUp,
+ }) }
+ { isEthereumNetwork
+ ? <div>
+ <div className="advanced-tab__fee-chart__title">{ t('liveGasPricePredictions') }</div>
+ {!gasEstimatesLoading
+ ? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />
+ : <Loading />
+ }
+ <div className="advanced-tab__fee-chart__speed-buttons">
+ <span>{ t('slower') }</span>
+ <span>{ t('faster') }</span>
+ </div>
+ </div>
+ : <div className="advanced-tab__fee-chart__title">{ t('chartOnlyAvailableEth') }</div>
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js
index 492037f25..492037f25 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
new file mode 100644
index 000000000..20a503018
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
@@ -0,0 +1,207 @@
+@import './time-remaining/index';
+
+.advanced-tab {
+ display: flex;
+ flex-flow: column;
+
+ &__transaction-data-summary,
+ &__fee-chart-title {
+ padding-left: 24px;
+ padding-right: 24px;
+ }
+
+ &__transaction-data-summary {
+ display: flex;
+ flex-flow: column;
+ color: $mid-gray;
+ margin-top: 12px;
+ padding-left: 18px;
+ padding-right: 18px;
+
+ &__titles,
+ &__container {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-between;
+ font-size: 12px;
+ color: #888EA3;
+ }
+
+ &__container {
+ font-size: 16px;
+ margin-top: 0px;
+ }
+
+ &__fee {
+ font-size: 16px;
+ color: #313A5E;
+ }
+ }
+
+ &__fee-chart {
+ margin-top: 8px;
+ height: 265px;
+ background: #F8F9FB;
+ border-bottom: 1px solid #d2d8dd;
+ border-top: 1px solid #d2d8dd;
+ position: relative;
+
+ &__title {
+ font-size: 12px;
+ color: #313A5E;
+ margin-left: 22px;
+ }
+
+ &__speed-buttons {
+ position: absolute;
+ bottom: 13px;
+ display: flex;
+ justify-content: space-between;
+ padding-left: 20px;
+ padding-right: 19px;
+ width: 100%;
+ font-size: 10px;
+ color: #888EA3;
+ }
+
+ .loading-overlay {
+ height: auto;
+ }
+ }
+
+ &__slider-container {
+ padding-left: 27px;
+ padding-right: 27px;
+ }
+
+ &__gas-edit-rows {
+ height: 73px;
+ display: flex;
+ flex-flow: row;
+ justify-content: space-between;
+ margin-left: 20px;
+ margin-right: 10px;
+ margin-top: 9px;
+ }
+
+ &__gas-edit-row {
+ display: flex;
+ flex-flow: column;
+
+ &__label {
+ color: #313B5E;
+ font-size: 14px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .fa-info-circle {
+ color: $silver;
+ margin-left: 10px;
+ cursor: pointer;
+ }
+
+ .fa-info-circle:hover {
+ color: $mid-gray;
+ }
+ }
+
+ &__error-text {
+ font-size: 12px;
+ color: red;
+ }
+
+ &__warning-text {
+ font-size: 12px;
+ color: orange;
+ }
+
+ &__input-wrapper {
+ position: relative;
+ }
+
+ &__input {
+ border: 1px solid $dusty-gray;
+ border-radius: 4px;
+ color: $mid-gray;
+ font-size: 16px;
+ height: 24px;
+ width: 155px;
+ padding-left: 8px;
+ padding-top: 2px;
+ margin-top: 7px;
+ }
+
+ &__input--error {
+ border: 1px solid $red;
+ }
+
+ &__input--warning {
+ border: 1px solid $orange;
+ }
+
+ &__input-arrows {
+ position: absolute;
+ top: 7px;
+ right: 0px;
+ width: 17px;
+ height: 24px;
+ border: 1px solid #dadada;
+ border-top-right-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ color: #9b9b9b;
+ font-size: .8em;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+
+ &__i-wrap {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ cursor: pointer;
+ }
+
+ &__i-wrap:hover {
+ background: #4EADE7;
+ color: $white;
+ }
+
+ i:hover {
+ background: #4EADE7;
+ }
+
+ i {
+ font-size: 10px;
+ }
+ }
+
+ &__input-arrows--error {
+ border: 1px solid $red;
+ }
+
+ &__input-arrows--warning {
+ border: 1px solid $orange;
+ }
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ display: none;
+ }
+
+ &__gwei-symbol {
+ position: absolute;
+ top: 8px;
+ right: 10px;
+ color: $dusty-gray;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
new file mode 100644
index 000000000..5f7d90922
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
@@ -0,0 +1,365 @@
+import React from 'react'
+import assert from 'assert'
+import shallow from '../../../../../../../lib/shallow-with-context'
+import sinon from 'sinon'
+import AdvancedTabContent from '../advanced-tab-content.component.js'
+
+import GasPriceChart from '../../../gas-price-chart'
+import Loading from '../../../../../ui/loading-screen'
+
+const propsMethodSpies = {
+ updateCustomGasPrice: sinon.spy(),
+ updateCustomGasLimit: sinon.spy(),
+}
+
+sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow')
+sinon.spy(AdvancedTabContent.prototype, 'gasInput')
+sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows')
+sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary')
+sinon.spy(AdvancedTabContent.prototype, 'gasInputError')
+
+describe('AdvancedTabContent Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<AdvancedTabContent
+ updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice}
+ updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit}
+ customGasPrice={11}
+ customGasLimit={23456}
+ timeRemaining={21500}
+ transactionFee={'$0.25'}
+ insufficientBalance={false}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ isEthereumNetwork={true}
+ />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.updateCustomGasPrice.resetHistory()
+ propsMethodSpies.updateCustomGasLimit.resetHistory()
+ AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
+ AdvancedTabContent.prototype.gasInput.resetHistory()
+ AdvancedTabContent.prototype.renderGasEditRows.resetHistory()
+ AdvancedTabContent.prototype.renderDataSummary.resetHistory()
+ })
+
+ describe('render()', () => {
+ it('should render the advanced-tab root node', () => {
+ assert(wrapper.hasClass('advanced-tab'))
+ })
+
+ it('should render the expected four children of the advanced-tab div', () => {
+ const advancedTabChildren = wrapper.children()
+ assert.equal(advancedTabChildren.length, 2)
+
+ assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
+ assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
+
+ const feeChartDiv = advancedTabChildren.at(1)
+
+ assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
+ assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title'))
+ assert(feeChartDiv.childAt(1).childAt(1).is(GasPriceChart))
+ assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons'))
+ })
+
+ it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => {
+ wrapper.setProps({ gasEstimatesLoading: true })
+ const advancedTabChildren = wrapper.children()
+ assert.equal(advancedTabChildren.length, 2)
+
+ assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
+ assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
+
+ const feeChartDiv = advancedTabChildren.at(1)
+
+ assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
+ assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title'))
+ assert(feeChartDiv.childAt(1).childAt(1).is(Loading))
+ assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons'))
+ })
+
+ it('should call renderDataSummary with the expected params', () => {
+ assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
+ const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args
+ assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500])
+ })
+
+ it('should call renderGasEditRows with the expected params', () => {
+ assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
+ const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
+ assert.deepEqual(renderGasEditRowArgs, [{
+ customGasPrice: 11,
+ customGasLimit: 23456,
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
+ updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit,
+ isSpeedUp: false,
+ }])
+ })
+ })
+
+ describe('renderDataSummary()', () => {
+ let dataSummary
+
+ beforeEach(() => {
+ dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining'))
+ })
+
+ it('should render the transaction-data-summary root node', () => {
+ assert(dataSummary.hasClass('advanced-tab__transaction-data-summary'))
+ })
+
+ it('should render titles of the data', () => {
+ const titlesNode = dataSummary.children().at(0)
+ assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles'))
+ assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee')
+ assert.equal(titlesNode.children().at(1).text(), '~transactionTime')
+ })
+
+ it('should render the data', () => {
+ const dataNode = dataSummary.children().at(1)
+ assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container'))
+ assert.equal(dataNode.children().at(0).text(), 'mockTotalFee')
+ assert(dataNode.children().at(1).hasClass('time-remaining'))
+ assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining')
+ })
+ })
+
+ describe('renderGasEditRow()', () => {
+ let gasEditRow
+
+ beforeEach(() => {
+ AdvancedTabContent.prototype.gasInput.resetHistory()
+ gasEditRow = shallow(wrapper.instance().renderGasEditRow({
+ labelKey: 'mockLabelKey',
+ someArg: 'argA',
+ }))
+ })
+
+ it('should render the gas-edit-row root node', () => {
+ assert(gasEditRow.hasClass('advanced-tab__gas-edit-row'))
+ })
+
+ it('should render a label and an input', () => {
+ const gasEditRowChildren = gasEditRow.children()
+ assert.equal(gasEditRowChildren.length, 2)
+ assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label'))
+ assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper'))
+ })
+
+ it('should render the label key and info button', () => {
+ const gasRowLabelChildren = gasEditRow.children().at(0).children()
+ assert.equal(gasRowLabelChildren.length, 2)
+ assert(gasRowLabelChildren.at(0), 'mockLabelKey')
+ assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle'))
+ })
+
+ it('should call this.gasInput with the correct args', () => {
+ const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args
+ assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ])
+ })
+ })
+
+ describe('renderGasEditRows()', () => {
+ let gasEditRows
+ let tempOnChangeGasLimit
+
+ beforeEach(() => {
+ tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit
+ wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit'
+ AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
+ gasEditRows = shallow(wrapper.instance().renderGasEditRows(
+ 'mockGasPrice',
+ () => 'mockUpdateCustomGasPriceReturn',
+ 'mockGasLimit',
+ () => 'mockUpdateCustomGasLimitReturn',
+ false
+ ))
+ })
+
+ afterEach(() => {
+ wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit
+ })
+
+ it('should render the gas-edit-rows root node', () => {
+ assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows'))
+ })
+
+ it('should render two rows', () => {
+ const gasEditRowsChildren = gasEditRows.children()
+ assert.equal(gasEditRowsChildren.length, 2)
+ assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row'))
+ assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row'))
+ })
+
+ it('should call this.renderGasEditRow twice, with the expected args', () => {
+ const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
+ assert.equal(renderGasEditRowSpyArgs.length, 2)
+ assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{
+ labelKey: 'gasPrice',
+ value: 'mockGasLimit',
+ onChange: () => 'mockOnChangeGasLimit',
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ showGWEI: true,
+ }].map(String))
+ assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{
+ labelKey: 'gasPrice',
+ value: 'mockGasPrice',
+ onChange: () => 'mockUpdateCustomGasPriceReturn',
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ showGWEI: true,
+ }].map(String))
+ })
+ })
+
+ describe('infoButton()', () => {
+ let infoButton
+
+ beforeEach(() => {
+ AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
+ infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn'))
+ })
+
+ it('should render the i element', () => {
+ assert(infoButton.hasClass('fa-info-circle'))
+ })
+
+ it('should pass the onClick argument to the i tag onClick prop', () => {
+ assert(infoButton.props().onClick(), 'mockOnClickReturn')
+ })
+ })
+
+ describe('gasInput()', () => {
+ let gasInput
+
+ beforeEach(() => {
+ AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
+ AdvancedTabContent.prototype.gasInputError.resetHistory()
+ gasInput = shallow(wrapper.instance().gasInput({
+ labelKey: 'gasPrice',
+ value: 321,
+ onChange: value => value + 7,
+ insufficientBalance: false,
+ showGWEI: true,
+ customPriceIsSafe: true,
+ isSpeedUp: false,
+ }))
+ })
+
+ it('should render the input-wrapper root node', () => {
+ assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper'))
+ })
+
+ it('should render two children, including an input', () => {
+ assert.equal(gasInput.children().length, 2)
+ assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input'))
+ })
+
+ it('should call the passed onChange method with the value of the input onChange event', () => {
+ const inputOnChange = gasInput.find('input').props().onChange
+ assert.equal(inputOnChange({ target: { value: 8} }), 15)
+ })
+
+ it('should have two input arrows', () => {
+ const upArrow = gasInput.find('.fa-angle-up')
+ assert.equal(upArrow.length, 1)
+ const downArrow = gasInput.find('.fa-angle-down')
+ assert.equal(downArrow.length, 1)
+ })
+
+ it('should call onChange with the value incremented decremented when its onchange method is called', () => {
+ const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0)
+ assert.equal(upArrow.props().onClick(), 329)
+ const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1)
+ assert.equal(downArrow.props().onClick(), 327)
+ })
+
+ it('should call gasInputError with the expected params', () => {
+ assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1)
+ const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args
+ assert.deepEqual(gasInputErrorArgs, [{
+ labelKey: 'gasPrice',
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ value: 321,
+ isSpeedUp: false,
+ }])
+ })
+ })
+
+ describe('gasInputError()', () => {
+ let gasInputError
+
+ beforeEach(() => {
+ AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
+ gasInputError = wrapper.instance().gasInputError({
+ labelKey: '',
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ isSpeedUp: false,
+ })
+ })
+
+ it('should return an insufficientBalance error', () => {
+ const gasInputError = wrapper.instance().gasInputError({
+ labelKey: 'gasPrice',
+ insufficientBalance: true,
+ customPriceIsSafe: true,
+ isSpeedUp: false,
+ value: 1,
+ })
+ assert.deepEqual(gasInputError, {
+ isInError: true,
+ errorText: 'insufficientBalance',
+ errorType: 'error',
+ })
+ })
+
+ it('should return a zero gas on retry error', () => {
+ const gasInputError = wrapper.instance().gasInputError({
+ labelKey: 'gasPrice',
+ insufficientBalance: false,
+ customPriceIsSafe: false,
+ isSpeedUp: true,
+ value: 0,
+ })
+ assert.deepEqual(gasInputError, {
+ isInError: true,
+ errorText: 'zeroGasPriceOnSpeedUpError',
+ errorType: 'error',
+ })
+ })
+
+ it('should return a low gas warning', () => {
+ const gasInputError = wrapper.instance().gasInputError({
+ labelKey: 'gasPrice',
+ insufficientBalance: false,
+ customPriceIsSafe: false,
+ isSpeedUp: false,
+ value: 1,
+ })
+ assert.deepEqual(gasInputError, {
+ isInError: true,
+ errorText: 'gasPriceExtremelyLow',
+ errorType: 'warning',
+ })
+ })
+
+ it('should return isInError false if there is no error', () => {
+ gasInputError = wrapper.instance().gasInputError({
+ labelKey: 'gasPrice',
+ insufficientBalance: false,
+ customPriceIsSafe: true,
+ value: 1,
+ })
+ assert.equal(gasInputError.isInError, false)
+ })
+ })
+
+})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js
index 61b681e1a..61b681e1a 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss
index e2115af7f..e2115af7f 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js
new file mode 100644
index 000000000..17f0345d5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import assert from 'assert'
+import shallow from '../../../../../../../../lib/shallow-with-context'
+import TimeRemaining from '../time-remaining.component.js'
+
+describe('TimeRemaining Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<TimeRemaining
+ milliseconds={495000}
+ />)
+ })
+
+ describe('render()', () => {
+ it('should render the time-remaining root node', () => {
+ assert(wrapper.hasClass('time-remaining'))
+ })
+
+ it('should render minutes and seconds numbers and labels', () => {
+ const timeRemainingChildren = wrapper.children()
+ assert.equal(timeRemainingChildren.length, 4)
+ assert.equal(timeRemainingChildren.at(0).text(), 8)
+ assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand')
+ assert.equal(timeRemainingChildren.at(2).text(), 15)
+ assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand')
+ })
+ })
+
+})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js
index 826d41f9c..826d41f9c 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js
index cf43e0acb..cf43e0acb 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js
new file mode 100644
index 000000000..5f3925fa5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js
@@ -0,0 +1,35 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Loading from '../../../../ui/loading-screen'
+import GasPriceButtonGroup from '../../gas-price-button-group'
+
+export default class BasicTabContent extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ gasPriceButtonGroupProps: PropTypes.object,
+ }
+
+ render () {
+ const { t } = this.context
+ const { gasPriceButtonGroupProps } = this.props
+
+ return (
+ <div className="basic-tab-content">
+ <div className="basic-tab-content__title">{ t('estimatedProcessingTimes') }</div>
+ <div className="basic-tab-content__blurb">{ t('selectAHigherGasFee') }</div>
+ {!gasPriceButtonGroupProps.loading
+ ? <GasPriceButtonGroup
+ className="gas-price-button-group--alt"
+ showCheck={true}
+ {...gasPriceButtonGroupProps}
+ />
+ : <Loading />
+ }
+ <div className="basic-tab-content__footer-blurb">{ t('acceleratingATransaction') }</div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js
index 078d50fce..078d50fce 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss
index e34e4e328..e34e4e328 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js
new file mode 100644
index 000000000..0989ac677
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js
@@ -0,0 +1,82 @@
+import React from 'react'
+import assert from 'assert'
+import shallow from '../../../../../../../lib/shallow-with-context'
+import BasicTabContent from '../basic-tab-content.component'
+
+import GasPriceButtonGroup from '../../../gas-price-button-group'
+import Loading from '../../../../../ui/loading-screen'
+
+const mockGasPriceButtonGroupProps = {
+ buttonDataLoading: false,
+ className: 'gas-price-button-group',
+ gasButtonInfo: [
+ {
+ feeInPrimaryCurrency: '$0.52',
+ feeInSecondaryCurrency: '0.0048 ETH',
+ timeEstimate: '~ 1 min 0 sec',
+ priceInHexWei: '0xa1b2c3f',
+ },
+ {
+ feeInPrimaryCurrency: '$0.39',
+ feeInSecondaryCurrency: '0.004 ETH',
+ timeEstimate: '~ 1 min 30 sec',
+ priceInHexWei: '0xa1b2c39',
+ },
+ {
+ feeInPrimaryCurrency: '$0.30',
+ feeInSecondaryCurrency: '0.00354 ETH',
+ timeEstimate: '~ 2 min 1 sec',
+ priceInHexWei: '0xa1b2c30',
+ },
+ ],
+ handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice),
+ noButtonActiveByDefault: true,
+ showCheck: true,
+}
+
+describe('BasicTabContent Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<BasicTabContent
+ gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
+ />)
+ })
+
+ describe('render', () => {
+ it('should have a title', () => {
+ assert(wrapper.find('.basic-tab-content').childAt(0).hasClass('basic-tab-content__title'))
+ })
+
+ it('should render a GasPriceButtonGroup compenent', () => {
+ assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
+ })
+
+ it('should pass correct props to GasPriceButtonGroup', () => {
+ const {
+ buttonDataLoading,
+ className,
+ gasButtonInfo,
+ handleGasPriceSelection,
+ noButtonActiveByDefault,
+ showCheck,
+ } = wrapper.find(GasPriceButtonGroup).props()
+ assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
+ assert.equal(buttonDataLoading, mockGasPriceButtonGroupProps.buttonDataLoading)
+ assert.equal(className, mockGasPriceButtonGroupProps.className)
+ assert.equal(noButtonActiveByDefault, mockGasPriceButtonGroupProps.noButtonActiveByDefault)
+ assert.equal(showCheck, mockGasPriceButtonGroupProps.showCheck)
+ assert.deepEqual(gasButtonInfo, mockGasPriceButtonGroupProps.gasButtonInfo)
+ assert.equal(JSON.stringify(handleGasPriceSelection), JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection))
+ })
+
+ it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => {
+ wrapper.setProps({
+ gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, loading: true },
+ })
+
+ assert.equal(wrapper.find(GasPriceButtonGroup).length, 0)
+ assert.equal(wrapper.find(Loading).length, 1)
+ })
+ })
+})
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
new file mode 100644
index 000000000..d242f59f5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
@@ -0,0 +1,186 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainer from '../../../ui/page-container'
+import { Tabs, Tab } from '../../../ui/tabs'
+import AdvancedTabContent from './advanced-tab-content'
+import BasicTabContent from './basic-tab-content'
+
+export default class GasModalPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideModal: PropTypes.func,
+ hideBasic: PropTypes.bool,
+ updateCustomGasPrice: PropTypes.func,
+ updateCustomGasLimit: PropTypes.func,
+ customGasPrice: PropTypes.number,
+ customGasLimit: PropTypes.number,
+ fetchBasicGasAndTimeEstimates: PropTypes.func,
+ fetchGasEstimates: PropTypes.func,
+ gasPriceButtonGroupProps: PropTypes.object,
+ infoRowProps: PropTypes.shape({
+ originalTotalFiat: PropTypes.string,
+ originalTotalEth: PropTypes.string,
+ newTotalFiat: PropTypes.string,
+ newTotalEth: PropTypes.string,
+ }),
+ onSubmit: PropTypes.func,
+ customModalGasPriceInHex: PropTypes.string,
+ customModalGasLimitInHex: PropTypes.string,
+ cancelAndClose: PropTypes.func,
+ transactionFee: PropTypes.string,
+ blockTime: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ customPriceIsSafe: PropTypes.bool,
+ isSpeedUp: PropTypes.bool,
+ disableSave: PropTypes.bool,
+ }
+
+ state = {}
+
+ componentDidMount () {
+ const promise = this.props.hideBasic
+ ? Promise.resolve(this.props.blockTime)
+ : this.props.fetchBasicGasAndTimeEstimates()
+ .then(basicEstimates => basicEstimates.blockTime)
+
+ promise
+ .then(blockTime => {
+ this.props.fetchGasEstimates(blockTime)
+ })
+ }
+
+ renderBasicTabContent (gasPriceButtonGroupProps) {
+ return (
+ <BasicTabContent
+ gasPriceButtonGroupProps={gasPriceButtonGroupProps}
+ />
+ )
+ }
+
+ renderAdvancedTabContent ({
+ convertThenUpdateCustomGasPrice,
+ convertThenUpdateCustomGasLimit,
+ customGasPrice,
+ customGasLimit,
+ newTotalFiat,
+ gasChartProps,
+ currentTimeEstimate,
+ insufficientBalance,
+ gasEstimatesLoading,
+ customPriceIsSafe,
+ isSpeedUp,
+ transactionFee,
+ }) {
+ return (
+ <AdvancedTabContent
+ updateCustomGasPrice={convertThenUpdateCustomGasPrice}
+ updateCustomGasLimit={convertThenUpdateCustomGasLimit}
+ customGasPrice={customGasPrice}
+ customGasLimit={customGasLimit}
+ timeRemaining={currentTimeEstimate}
+ transactionFee={transactionFee}
+ totalFee={newTotalFiat}
+ gasChartProps={gasChartProps}
+ insufficientBalance={insufficientBalance}
+ gasEstimatesLoading={gasEstimatesLoading}
+ customPriceIsSafe={customPriceIsSafe}
+ isSpeedUp={isSpeedUp}
+ />
+ )
+ }
+
+ renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) {
+ return (
+ <div className="gas-modal-content__info-row-wrapper">
+ <div className="gas-modal-content__info-row">
+ <div className="gas-modal-content__info-row__send-info">
+ <span className="gas-modal-content__info-row__send-info__label">{this.context.t('sendAmount')}</span>
+ <span className="gas-modal-content__info-row__send-info__value">{sendAmount}</span>
+ </div>
+ <div className="gas-modal-content__info-row__transaction-info">
+ <span className={'gas-modal-content__info-row__transaction-info__label'}>{this.context.t('transactionFee')}</span>
+ <span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span>
+ </div>
+ <div className="gas-modal-content__info-row__total-info">
+ <span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span>
+ <span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span>
+ </div>
+ <div className="gas-modal-content__info-row__fiat-total-info">
+ <span className="gas-modal-content__info-row__fiat-total-info__value">{newTotalFiat}</span>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderTabs ({
+ originalTotalFiat,
+ originalTotalEth,
+ newTotalFiat,
+ newTotalEth,
+ sendAmount,
+ transactionFee,
+ },
+ {
+ gasPriceButtonGroupProps,
+ hideBasic,
+ ...advancedTabProps
+ }) {
+ let tabsToRender = [
+ { name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) },
+ { name: 'advanced', content: this.renderAdvancedTabContent({ transactionFee, ...advancedTabProps }) },
+ ]
+
+ if (hideBasic) {
+ tabsToRender = tabsToRender.slice(1)
+ }
+
+ return (
+ <Tabs>
+ {tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}>
+ <div className="gas-modal-content">
+ { content }
+ { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
+ </div>
+ </Tab>
+ )}
+ </Tabs>
+ )
+ }
+
+ render () {
+ const {
+ cancelAndClose,
+ infoRowProps,
+ onSubmit,
+ customModalGasPriceInHex,
+ customModalGasLimitInHex,
+ disableSave,
+ ...tabProps
+ } = this.props
+
+ return (
+ <div className="gas-modal-page-container">
+ <PageContainer
+ title={this.context.t('customGas')}
+ subtitle={this.context.t('customGasSubTitle')}
+ tabsComponent={this.renderTabs(infoRowProps, tabProps)}
+ disabled={disableSave}
+ onCancel={() => cancelAndClose()}
+ onClose={() => cancelAndClose()}
+ onSubmit={() => {
+ onSubmit(customModalGasLimitInHex, customModalGasPriceInHex)
+ }}
+ submitText={this.context.t('save')}
+ headerCloseText={this.context.t('close')}
+ hideCancel={true}
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
new file mode 100644
index 000000000..d541056f4
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
@@ -0,0 +1,295 @@
+import { connect } from 'react-redux'
+import { pipe, partialRight } from 'ramda'
+import GasModalPageContainer from './gas-modal-page-container.component'
+import {
+ hideModal,
+ setGasLimit,
+ setGasPrice,
+ createSpeedUpTransaction,
+ hideSidebar,
+} from '../../../../store/actions'
+import {
+ setCustomGasPrice,
+ setCustomGasLimit,
+ resetCustomData,
+ setCustomTimeEstimate,
+ fetchGasEstimates,
+ fetchBasicGasAndTimeEstimates,
+} from '../../../../ducks/gas/gas.duck'
+import {
+ hideGasButtonGroup,
+} from '../../../../ducks/send/send.duck'
+import {
+ updateGasAndCalculate,
+} from '../../../../ducks/confirm-transaction/confirm-transaction.duck'
+import {
+ conversionRateSelector as getConversionRate,
+ getCurrentCurrency,
+ getCurrentEthBalance,
+ getIsMainnet,
+ getSelectedToken,
+ isEthereumNetwork,
+ preferencesSelector,
+} from '../../../../selectors/selectors.js'
+import {
+ formatTimeEstimate,
+ getFastPriceEstimateInHexWEI,
+ getBasicGasEstimateLoadingStatus,
+ getGasEstimatesLoadingStatus,
+ getCustomGasLimit,
+ getCustomGasPrice,
+ getDefaultActiveButtonIndex,
+ getEstimatedGasPrices,
+ getEstimatedGasTimes,
+ getRenderableBasicEstimateData,
+ getBasicGasEstimateBlockTime,
+ isCustomPriceSafe,
+} from '../../../../selectors/custom-gas'
+import {
+ submittedPendingTransactionsSelector,
+} from '../../../../selectors/transactions'
+import {
+ formatCurrency,
+} from '../../../../helpers/utils/confirm-tx.util'
+import {
+ addHexWEIsToDec,
+ decEthToConvertedCurrency as ethTotalToConvertedCurrency,
+ decGWEIToHexWEI,
+ hexWEIToDecGWEI,
+} from '../../../../helpers/utils/conversions.util'
+import {
+ formatETHFee,
+} from '../../../../helpers/utils/formatters'
+import {
+ calcGasTotal,
+ isBalanceSufficient,
+} from '../../send/send.utils'
+import { addHexPrefix } from 'ethereumjs-util'
+import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
+
+const mapStateToProps = (state, ownProps) => {
+ const { transaction = {} } = ownProps
+ const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
+ const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
+
+ const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id)
+ const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
+ const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
+ const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
+
+ const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
+
+ const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex)
+
+ const currentCurrency = getCurrentCurrency(state)
+ const conversionRate = getConversionRate(state)
+
+ const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate)
+
+ const hideBasic = state.appState.modal.modalState.props.hideBasic
+
+ const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex)
+
+ const gasPrices = getEstimatedGasPrices(state)
+ const estimatedTimes = getEstimatedGasTimes(state)
+ const balance = getCurrentEthBalance(state)
+
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const showFiat = Boolean(isMainnet || showFiatInTestnets)
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount: value,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ return {
+ hideBasic,
+ isConfirm: isConfirm(state),
+ customModalGasPriceInHex,
+ customModalGasLimitInHex,
+ customGasPrice,
+ customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
+ newTotalFiat,
+ currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes),
+ blockTime: getBasicGasEstimateBlockTime(state),
+ customPriceIsSafe: isCustomPriceSafe(state),
+ gasPriceButtonGroupProps: {
+ buttonDataLoading,
+ defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
+ gasButtonInfo,
+ },
+ gasChartProps: {
+ currentPrice: customGasPrice,
+ gasPrices,
+ estimatedTimes,
+ gasPricesMax: gasPrices[gasPrices.length - 1],
+ estimatedTimesMax: estimatedTimes[0],
+ },
+ infoRowProps: {
+ originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate),
+ originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal),
+ newTotalFiat: showFiat ? newTotalFiat : '',
+ newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
+ transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
+ sendAmount: addHexWEIsToRenderableEth(value, '0x0'),
+ },
+ isSpeedUp: transaction.status === 'submitted',
+ txId: transaction.id,
+ insufficientBalance,
+ gasEstimatesLoading,
+ isMainnet,
+ isEthereumNetwork: isEthereumNetwork(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice)))
+
+ return {
+ cancelAndClose: () => {
+ dispatch(resetCustomData())
+ dispatch(hideModal())
+ },
+ hideModal: () => dispatch(hideModal()),
+ updateCustomGasPrice,
+ convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)),
+ convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))),
+ setGasData: (newLimit, newPrice) => {
+ dispatch(setGasLimit(newLimit))
+ dispatch(setGasPrice(newPrice))
+ },
+ updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => {
+ updateCustomGasPrice(gasPrice)
+ dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16))))
+ return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
+ },
+ createSpeedUpTransaction: (txId, gasPrice) => {
+ return dispatch(createSpeedUpTransaction(txId, gasPrice))
+ },
+ hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
+ setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
+ hideSidebar: () => dispatch(hideSidebar()),
+ fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
+ fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps
+ const {
+ updateCustomGasPrice: dispatchUpdateCustomGasPrice,
+ hideGasButtonGroup: dispatchHideGasButtonGroup,
+ setGasData: dispatchSetGasData,
+ updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
+ createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
+ hideSidebar: dispatchHideSidebar,
+ cancelAndClose: dispatchCancelAndClose,
+ hideModal: dispatchHideModal,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ onSubmit: (gasLimit, gasPrice) => {
+ if (isConfirm) {
+ dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice)
+ dispatchHideModal()
+ } else if (isSpeedUp) {
+ dispatchCreateSpeedUpTransaction(txId, gasPrice)
+ dispatchHideSidebar()
+ dispatchCancelAndClose()
+ } else {
+ dispatchSetGasData(gasLimit, gasPrice)
+ dispatchHideGasButtonGroup()
+ dispatchCancelAndClose()
+ }
+ },
+ gasPriceButtonGroupProps: {
+ ...gasPriceButtonGroupProps,
+ handleGasPriceSelection: dispatchUpdateCustomGasPrice,
+ },
+ cancelAndClose: () => {
+ dispatchCancelAndClose()
+ if (isSpeedUp) {
+ dispatchHideSidebar()
+ }
+ },
+ disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer)
+
+function isConfirm (state) {
+ return Boolean(Object.keys(state.confirmTransaction.txData).length)
+}
+
+function calcCustomGasPrice (customGasPriceInHex) {
+ return Number(hexWEIToDecGWEI(customGasPriceInHex))
+}
+
+function calcCustomGasLimit (customGasLimitInHex) {
+ return parseInt(customGasLimitInHex, 16)
+}
+
+function getTxParams (state, transactionId) {
+ const { confirmTransaction: { txData }, metamask: { send } } = state
+ const pendingTransactions = submittedPendingTransactionsSelector(state)
+ const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId)
+ const { txParams: pendingTxParams } = pendingTransaction || {}
+ return txData.txParams || pendingTxParams || {
+ from: send.from,
+ gas: send.gasLimit || '0x5208',
+ gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true),
+ to: send.to,
+ value: getSelectedToken(state) ? '0x0' : send.amount,
+ }
+}
+
+function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
+ return pipe(
+ addHexWEIsToDec,
+ formatETHFee
+ )(aHexWEI, bHexWEI)
+}
+
+function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) {
+ return pipe(
+ addHexWEIsToDec,
+ partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]),
+ partialRight(formatCurrency, [convertedCurrency]),
+ )(aHexWEI, bHexWEI)
+}
+
+function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) {
+ const minGasPrice = gasPrices[0]
+ const maxGasPrice = gasPrices[gasPrices.length - 1]
+ let priceForEstimation = currentGasPrice
+ if (currentGasPrice < minGasPrice) {
+ priceForEstimation = minGasPrice
+ } else if (currentGasPrice > maxGasPrice) {
+ priceForEstimation = maxGasPrice
+ }
+
+ const {
+ closestLowerValueIndex,
+ closestHigherValueIndex,
+ closestHigherValue,
+ closestLowerValue,
+ } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation })
+
+ const newTimeEstimate = extrapolateY({
+ higherY: estimatedTimes[closestHigherValueIndex],
+ lowerY: estimatedTimes[closestLowerValueIndex],
+ higherX: closestHigherValue,
+ lowerX: closestLowerValue,
+ xForExtrapolation: priceForEstimation,
+ })
+
+ return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice)
+}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js
index ec0ebad22..ec0ebad22 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/index.js
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss
index b9e0f59c4..b9e0f59c4 100644
--- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
new file mode 100644
index 000000000..7557eefe5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
@@ -0,0 +1,274 @@
+import React from 'react'
+import assert from 'assert'
+import shallow from '../../../../../../lib/shallow-with-context'
+import sinon from 'sinon'
+import GasModalPageContainer from '../gas-modal-page-container.component.js'
+import timeout from '../../../../../../lib/test-timeout'
+
+import PageContainer from '../../../../ui/page-container'
+
+import { Tab } from '../../../../ui/tabs'
+
+const mockBasicGasEstimates = {
+ blockTime: 'mockBlockTime',
+}
+
+const propsMethodSpies = {
+ cancelAndClose: sinon.spy(),
+ onSubmit: sinon.spy(),
+ fetchBasicGasAndTimeEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
+ fetchGasEstimates: sinon.spy(),
+}
+
+const mockGasPriceButtonGroupProps = {
+ buttonDataLoading: false,
+ className: 'gas-price-button-group',
+ gasButtonInfo: [
+ {
+ feeInPrimaryCurrency: '$0.52',
+ feeInSecondaryCurrency: '0.0048 ETH',
+ timeEstimate: '~ 1 min 0 sec',
+ priceInHexWei: '0xa1b2c3f',
+ },
+ {
+ feeInPrimaryCurrency: '$0.39',
+ feeInSecondaryCurrency: '0.004 ETH',
+ timeEstimate: '~ 1 min 30 sec',
+ priceInHexWei: '0xa1b2c39',
+ },
+ {
+ feeInPrimaryCurrency: '$0.30',
+ feeInSecondaryCurrency: '0.00354 ETH',
+ timeEstimate: '~ 2 min 1 sec',
+ priceInHexWei: '0xa1b2c30',
+ },
+ ],
+ handleGasPriceSelection: 'mockSelectionFunction',
+ noButtonActiveByDefault: true,
+ showCheck: true,
+ newTotalFiat: 'mockNewTotalFiat',
+ newTotalEth: 'mockNewTotalEth',
+}
+const mockInfoRowProps = {
+ originalTotalFiat: 'mockOriginalTotalFiat',
+ originalTotalEth: 'mockOriginalTotalEth',
+ newTotalFiat: 'mockNewTotalFiat',
+ newTotalEth: 'mockNewTotalEth',
+ sendAmount: 'mockSendAmount',
+ transactionFee: 'mockTransactionFee',
+}
+
+const GP = GasModalPageContainer.prototype
+describe('GasModalPageContainer Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasModalPageContainer
+ cancelAndClose={propsMethodSpies.cancelAndClose}
+ onSubmit={propsMethodSpies.onSubmit}
+ fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
+ fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
+ updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
+ updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
+ customGasPrice={21}
+ customGasLimit={54321}
+ gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
+ infoRowProps={mockInfoRowProps}
+ currentTimeEstimate={'1 min 31 sec'}
+ customGasPriceInHex={'mockCustomGasPriceInHex'}
+ customGasLimitInHex={'mockCustomGasLimitInHex'}
+ insufficientBalance={false}
+ disableSave={false}
+ />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.cancelAndClose.resetHistory()
+ })
+
+ describe('componentDidMount', () => {
+ it('should call props.fetchBasicGasAndTimeEstimates', () => {
+ propsMethodSpies.fetchBasicGasAndTimeEstimates.resetHistory()
+ assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 0)
+ wrapper.instance().componentDidMount()
+ assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 1)
+ })
+
+ it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasAndTimeEstimates', async () => {
+ propsMethodSpies.fetchGasEstimates.resetHistory()
+ assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0)
+ wrapper.instance().componentDidMount()
+ await timeout(250)
+ assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1)
+ assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime')
+ })
+ })
+
+ describe('render', () => {
+ it('should render a PageContainer compenent', () => {
+ assert.equal(wrapper.find(PageContainer).length, 1)
+ })
+
+ it('should pass correct props to PageContainer', () => {
+ const {
+ title,
+ subtitle,
+ disabled,
+ } = wrapper.find(PageContainer).props()
+ assert.equal(title, 'customGas')
+ assert.equal(subtitle, 'customGasSubTitle')
+ assert.equal(disabled, false)
+ })
+
+ it('should pass the correct onCancel and onClose methods to PageContainer', () => {
+ const {
+ onCancel,
+ onClose,
+ } = wrapper.find(PageContainer).props()
+ assert.equal(propsMethodSpies.cancelAndClose.callCount, 0)
+ onCancel()
+ assert.equal(propsMethodSpies.cancelAndClose.callCount, 1)
+ onClose()
+ assert.equal(propsMethodSpies.cancelAndClose.callCount, 2)
+ })
+
+ it('should pass the correct renderTabs property to PageContainer', () => {
+ sinon.stub(GP, 'renderTabs').returns('mockTabs')
+ const renderTabsWrapperTester = shallow(<GasModalPageContainer
+ fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
+ fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
+ />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
+ const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props()
+ assert.equal(tabsComponent, 'mockTabs')
+ GasModalPageContainer.prototype.renderTabs.restore()
+ })
+ })
+
+ describe('renderTabs', () => {
+ beforeEach(() => {
+ sinon.spy(GP, 'renderBasicTabContent')
+ sinon.spy(GP, 'renderAdvancedTabContent')
+ sinon.spy(GP, 'renderInfoRows')
+ })
+
+ afterEach(() => {
+ GP.renderBasicTabContent.restore()
+ GP.renderAdvancedTabContent.restore()
+ GP.renderInfoRows.restore()
+ })
+
+ it('should render a Tabs component with "Basic" and "Advanced" tabs', () => {
+ const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
+ gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
+ otherProps: 'mockAdvancedTabProps',
+ })
+ const renderedTabs = shallow(renderTabsResult)
+ assert.equal(renderedTabs.props().className, 'tabs')
+
+ const tabs = renderedTabs.find(Tab)
+ assert.equal(tabs.length, 2)
+
+ assert.equal(tabs.at(0).props().name, 'basic')
+ assert.equal(tabs.at(1).props().name, 'advanced')
+
+ assert.equal(tabs.at(0).childAt(0).props().className, 'gas-modal-content')
+ assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content')
+ })
+
+ it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => {
+ assert.equal(GP.renderBasicTabContent.callCount, 0)
+ assert.equal(GP.renderAdvancedTabContent.callCount, 0)
+
+ wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
+
+ assert.equal(GP.renderBasicTabContent.callCount, 1)
+ assert.equal(GP.renderAdvancedTabContent.callCount, 1)
+
+ assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps)
+ assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { transactionFee: 'mockTransactionFee', otherProps: 'mockAdvancedTabProps' })
+ })
+
+ it('should call renderInfoRows with the expected props', () => {
+ assert.equal(GP.renderInfoRows.callCount, 0)
+
+ wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
+
+ assert.equal(GP.renderInfoRows.callCount, 2)
+
+ assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
+ assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
+ })
+
+ it('should not render the basic tab if hideBasic is true', () => {
+ const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
+ gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
+ otherProps: 'mockAdvancedTabProps',
+ hideBasic: true,
+ })
+
+ const renderedTabs = shallow(renderTabsResult)
+ const tabs = renderedTabs.find(Tab)
+ assert.equal(tabs.length, 1)
+ assert.equal(tabs.at(0).props().name, 'advanced')
+ })
+ })
+
+ describe('renderBasicTabContent', () => {
+ it('should render', () => {
+ const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps)
+
+ assert.deepEqual(
+ renderBasicTabContentResult.props.gasPriceButtonGroupProps,
+ mockGasPriceButtonGroupProps
+ )
+ })
+ })
+
+ describe('renderAdvancedTabContent', () => {
+ it('should render with the correct props', () => {
+ const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({
+ convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice',
+ convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit',
+ customGasPrice: 123,
+ customGasLimit: 456,
+ newTotalFiat: '$0.30',
+ currentTimeEstimate: '1 min 31 sec',
+ gasEstimatesLoading: 'mockGasEstimatesLoading',
+ })
+ const advancedTabContentProps = renderAdvancedTabContentResult.props
+ assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice')
+ assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit')
+ assert.equal(advancedTabContentProps.customGasPrice, 123)
+ assert.equal(advancedTabContentProps.customGasLimit, 456)
+ assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec')
+ assert.equal(advancedTabContentProps.totalFee, '$0.30')
+ assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading')
+ })
+ })
+
+ describe('renderInfoRows', () => {
+ it('should render the info rows with the passed data', () => {
+ const baseClassName = 'gas-modal-content__info-row'
+ const renderedInfoRowsContainer = shallow(wrapper.instance().renderInfoRows(
+ 'mockNewTotalFiat',
+ ' mockNewTotalEth',
+ ' mockSendAmount',
+ ' mockTransactionFee'
+ ))
+
+ assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName))
+
+ const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children()
+ assert.equal(renderedInfoRows.length, 4)
+ assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`))
+ assert(renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`))
+ assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`))
+ assert(renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`))
+
+ assert.equal(renderedInfoRows.at(0).text(), 'sendAmount mockSendAmount')
+ assert.equal(renderedInfoRows.at(1).text(), 'transactionFee mockTransactionFee')
+ assert.equal(renderedInfoRows.at(2).text(), 'newTotal mockNewTotalEth')
+ assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat')
+ })
+ })
+})
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
new file mode 100644
index 000000000..b9eb67d2b
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
@@ -0,0 +1,431 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+let mergeProps
+
+const actionSpies = {
+ hideModal: sinon.spy(),
+ setGasLimit: sinon.spy(),
+ setGasPrice: sinon.spy(),
+}
+
+const gasActionSpies = {
+ setCustomGasPrice: sinon.spy(),
+ setCustomGasLimit: sinon.spy(),
+ resetCustomData: sinon.spy(),
+}
+
+const confirmTransactionActionSpies = {
+ updateGasAndCalculate: sinon.spy(),
+}
+
+const sendActionSpies = {
+ hideGasButtonGroup: sinon.spy(),
+}
+
+proxyquire('../gas-modal-page-container.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+ '../../../../selectors/custom-gas': {
+ getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`,
+ getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`,
+ getDefaultActiveButtonIndex: (a, b) => a + b,
+ },
+ '../../../../store/actions': actionSpies,
+ '../../../../ducks/gas/gas.duck': gasActionSpies,
+ '../../../../ducks/confirm-transaction/confirm-transaction.duck': confirmTransactionActionSpies,
+ '../../../../ducks/send/send.duck': sendActionSpies,
+ '../../../../selectors/selectors.js': {
+ getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
+ },
+})
+
+describe('gas-modal-page-container container', () => {
+
+ describe('mapStateToProps()', () => {
+ it('should map the correct properties to props', () => {
+ const baseMockState = {
+ appState: {
+ modal: {
+ modalState: {
+ props: {
+ hideBasic: true,
+ },
+ },
+ },
+ },
+ metamask: {
+ send: {
+ gasLimit: '16',
+ gasPrice: '32',
+ amount: '64',
+ },
+ currentCurrency: 'abc',
+ conversionRate: 50,
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 12,
+ safeLow: 2,
+ },
+ customData: {
+ limit: 'aaaaaaaa',
+ price: 'ffffffff',
+ },
+ gasEstimatesLoading: false,
+ priceAndTimeEstimates: [
+ { gasprice: 3, expectedTime: 31 },
+ { gasprice: 4, expectedTime: 62 },
+ { gasprice: 5, expectedTime: 93 },
+ { gasprice: 6, expectedTime: 124 },
+ ],
+ },
+ confirmTransaction: {
+ txData: {
+ txParams: {
+ gas: '0x1600000',
+ gasPrice: '0x3200000',
+ value: '0x640000000000000',
+ },
+ },
+ },
+ }
+ const baseExpectedResult = {
+ isConfirm: true,
+ customGasPrice: 4.294967295,
+ customGasLimit: 2863311530,
+ currentTimeEstimate: '~1 min 11 sec',
+ newTotalFiat: '637.41',
+ blockTime: 12,
+ customModalGasLimitInHex: 'aaaaaaaa',
+ customModalGasPriceInHex: 'ffffffff',
+ customPriceIsSafe: true,
+ gasChartProps: {
+ 'currentPrice': 4.294967295,
+ estimatedTimes: [31, 62, 93, 124],
+ estimatedTimesMax: 31,
+ gasPrices: [3, 4, 5, 6],
+ gasPricesMax: 6,
+ },
+ gasPriceButtonGroupProps: {
+ buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4',
+ defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff',
+ gasButtonInfo: 'mockRenderableBasicEstimateData:4',
+ },
+ gasEstimatesLoading: false,
+ hideBasic: true,
+ infoRowProps: {
+ originalTotalFiat: '637.41',
+ originalTotalEth: '12.748189 ETH',
+ newTotalFiat: '637.41',
+ newTotalEth: '12.748189 ETH',
+ sendAmount: '0.45036 ETH',
+ transactionFee: '12.297829 ETH',
+ },
+ insufficientBalance: true,
+ isSpeedUp: false,
+ txId: 34,
+ isEthereumNetwork: false,
+ isMainnet: true,
+ }
+ const baseMockOwnProps = { transaction: { id: 34 } }
+ const tests = [
+ { mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps },
+ {
+ mockState: Object.assign({}, baseMockState, {
+ metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' },
+ }),
+ expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }),
+ mockOwnProps: baseMockOwnProps,
+ },
+ {
+ mockState: baseMockState,
+ mockOwnProps: Object.assign({}, baseMockOwnProps, {
+ transaction: { id: 34, status: 'submitted' },
+ }),
+ expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }),
+ },
+ {
+ mockState: Object.assign({}, baseMockState, {
+ metamask: {
+ ...baseMockState.metamask,
+ preferences: {
+ ...baseMockState.metamask.preferences,
+ showFiatInTestnets: false,
+ },
+ provider: {
+ ...baseMockState.metamask.provider,
+ type: 'rinkeby',
+ },
+ },
+ }),
+ mockOwnProps: baseMockOwnProps,
+ expectedResult: {
+ ...baseExpectedResult,
+ infoRowProps: {
+ ...baseExpectedResult.infoRowProps,
+ newTotalFiat: '',
+ },
+ isMainnet: false,
+ },
+ },
+ {
+ mockState: Object.assign({}, baseMockState, {
+ metamask: {
+ ...baseMockState.metamask,
+ preferences: {
+ ...baseMockState.metamask.preferences,
+ showFiatInTestnets: true,
+ },
+ provider: {
+ ...baseMockState.metamask.provider,
+ type: 'rinkeby',
+ },
+ },
+ }),
+ mockOwnProps: baseMockOwnProps,
+ expectedResult: {
+ ...baseExpectedResult,
+ isMainnet: false,
+ },
+ },
+ {
+ mockState: Object.assign({}, baseMockState, {
+ metamask: {
+ ...baseMockState.metamask,
+ preferences: {
+ ...baseMockState.metamask.preferences,
+ showFiatInTestnets: true,
+ },
+ provider: {
+ ...baseMockState.metamask.provider,
+ type: 'mainnet',
+ },
+ },
+ }),
+ mockOwnProps: baseMockOwnProps,
+ expectedResult: baseExpectedResult,
+ },
+ ]
+
+ let result
+ tests.forEach(({ mockState, mockOwnProps, expectedResult}) => {
+ result = mapStateToProps(mockState, mockOwnProps)
+ assert.deepEqual(result, expectedResult)
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ afterEach(() => {
+ actionSpies.hideModal.resetHistory()
+ gasActionSpies.setCustomGasPrice.resetHistory()
+ gasActionSpies.setCustomGasLimit.resetHistory()
+ })
+
+ describe('hideGasButtonGroup()', () => {
+ it('should dispatch a hideGasButtonGroup action', () => {
+ mapDispatchToPropsObject.hideGasButtonGroup()
+ assert(dispatchSpy.calledOnce)
+ assert(sendActionSpies.hideGasButtonGroup.calledOnce)
+ })
+ })
+
+ describe('cancelAndClose()', () => {
+ it('should dispatch a hideModal action', () => {
+ mapDispatchToPropsObject.cancelAndClose()
+ assert(dispatchSpy.calledTwice)
+ assert(actionSpies.hideModal.calledOnce)
+ assert(gasActionSpies.resetCustomData.calledOnce)
+ })
+ })
+
+ describe('updateCustomGasPrice()', () => {
+ it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => {
+ mapDispatchToPropsObject.updateCustomGasPrice('ffff')
+ assert(dispatchSpy.calledOnce)
+ assert(gasActionSpies.setCustomGasPrice.calledOnce)
+ assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff')
+ })
+ })
+
+ describe('convertThenUpdateCustomGasPrice()', () => {
+ it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => {
+ mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff')
+ assert(dispatchSpy.calledOnce)
+ assert(gasActionSpies.setCustomGasPrice.calledOnce)
+ assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600')
+ })
+ })
+
+
+ describe('convertThenUpdateCustomGasLimit()', () => {
+ it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => {
+ mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16)
+ assert(dispatchSpy.calledOnce)
+ assert(gasActionSpies.setCustomGasLimit.calledOnce)
+ assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10')
+ })
+ })
+
+ describe('setGasData()', () => {
+ it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => {
+ mapDispatchToPropsObject.setGasData('ffff', 'aaaa')
+ assert(dispatchSpy.calledTwice)
+ assert(actionSpies.setGasPrice.calledOnce)
+ assert(actionSpies.setGasLimit.calledOnce)
+ assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff')
+ assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa')
+ })
+ })
+
+ describe('updateConfirmTxGasAndCalculate()', () => {
+ it('should dispatch a updateGasAndCalculate action with the correct props', () => {
+ mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa')
+ assert.equal(dispatchSpy.callCount, 3)
+ assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce)
+ assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' })
+ })
+ })
+
+ })
+
+ describe('mergeProps', () => {
+ let stateProps
+ let dispatchProps
+ let ownProps
+
+ beforeEach(() => {
+ stateProps = {
+ gasPriceButtonGroupProps: {
+ someGasPriceButtonGroupProp: 'foo',
+ anotherGasPriceButtonGroupProp: 'bar',
+ },
+ isConfirm: true,
+ someOtherStateProp: 'baz',
+ }
+ dispatchProps = {
+ updateCustomGasPrice: sinon.spy(),
+ hideGasButtonGroup: sinon.spy(),
+ setGasData: sinon.spy(),
+ updateConfirmTxGasAndCalculate: sinon.spy(),
+ someOtherDispatchProp: sinon.spy(),
+ createSpeedUpTransaction: sinon.spy(),
+ hideSidebar: sinon.spy(),
+ hideModal: sinon.spy(),
+ cancelAndClose: sinon.spy(),
+ }
+ ownProps = { someOwnProp: 123 }
+ })
+
+ afterEach(() => {
+ dispatchProps.updateCustomGasPrice.resetHistory()
+ dispatchProps.hideGasButtonGroup.resetHistory()
+ dispatchProps.setGasData.resetHistory()
+ dispatchProps.updateConfirmTxGasAndCalculate.resetHistory()
+ dispatchProps.someOtherDispatchProp.resetHistory()
+ dispatchProps.createSpeedUpTransaction.resetHistory()
+ dispatchProps.hideSidebar.resetHistory()
+ dispatchProps.hideModal.resetHistory()
+ })
+ it('should return the expected props when isConfirm is true', () => {
+ const result = mergeProps(stateProps, dispatchProps, ownProps)
+
+ assert.equal(result.isConfirm, true)
+ assert.equal(result.someOtherStateProp, 'baz')
+ assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
+ assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
+ assert.equal(result.someOwnProp, 123)
+
+ assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
+ assert.equal(dispatchProps.setGasData.callCount, 0)
+ assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
+ assert.equal(dispatchProps.hideModal.callCount, 0)
+
+ result.onSubmit()
+
+ assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1)
+ assert.equal(dispatchProps.setGasData.callCount, 0)
+ assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
+ assert.equal(dispatchProps.hideModal.callCount, 1)
+
+ assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
+ result.gasPriceButtonGroupProps.handleGasPriceSelection()
+ assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
+
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
+ result.someOtherDispatchProp()
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
+ })
+
+ it('should return the expected props when isConfirm is false', () => {
+ const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps)
+
+ assert.equal(result.isConfirm, false)
+ assert.equal(result.someOtherStateProp, 'baz')
+ assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
+ assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
+ assert.equal(result.someOwnProp, 123)
+
+ assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
+ assert.equal(dispatchProps.setGasData.callCount, 0)
+ assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
+ assert.equal(dispatchProps.cancelAndClose.callCount, 0)
+
+ result.onSubmit('mockNewLimit', 'mockNewPrice')
+
+ assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
+ assert.equal(dispatchProps.setGasData.callCount, 1)
+ assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice'])
+ assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1)
+ assert.equal(dispatchProps.cancelAndClose.callCount, 1)
+
+ assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
+ result.gasPriceButtonGroupProps.handleGasPriceSelection()
+ assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
+
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
+ result.someOtherDispatchProp()
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
+ })
+
+ it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => {
+ const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps)
+
+ result.onSubmit()
+
+ assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
+ assert.equal(dispatchProps.setGasData.callCount, 0)
+ assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
+ assert.equal(dispatchProps.cancelAndClose.callCount, 1)
+
+ assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1)
+ assert.equal(dispatchProps.hideSidebar.callCount, 1)
+ })
+ })
+
+})
diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js
new file mode 100644
index 000000000..0456f5262
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js
@@ -0,0 +1,89 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ButtonGroup from '../../../ui/button-group'
+import Button from '../../../ui/button'
+
+const GAS_OBJECT_PROPTYPES_SHAPE = {
+ label: PropTypes.string,
+ feeInPrimaryCurrency: PropTypes.string,
+ feeInSecondaryCurrency: PropTypes.string,
+ timeEstimate: PropTypes.string,
+ priceInHexWei: PropTypes.string,
+}
+
+export default class GasPriceButtonGroup extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ buttonDataLoading: PropTypes.bool,
+ className: PropTypes.string,
+ defaultActiveButtonIndex: PropTypes.number,
+ gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)),
+ handleGasPriceSelection: PropTypes.func,
+ newActiveButtonIndex: PropTypes.number,
+ noButtonActiveByDefault: PropTypes.bool,
+ showCheck: PropTypes.bool,
+ }
+
+ renderButtonContent ({
+ labelKey,
+ feeInPrimaryCurrency,
+ feeInSecondaryCurrency,
+ timeEstimate,
+ }, {
+ className,
+ showCheck,
+ }) {
+ return (<div>
+ { labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> }
+ { timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> }
+ { feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> }
+ { feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> }
+ { showCheck && <div className="button-check-wrapper"><i className="fa fa-check fa-sm" /></div> }
+ </div>)
+ }
+
+ renderButton ({
+ priceInHexWei,
+ ...renderableGasInfo
+ }, {
+ buttonDataLoading,
+ handleGasPriceSelection,
+ ...buttonContentPropsAndFlags
+ }, index) {
+ return (
+ <Button
+ onClick={() => handleGasPriceSelection(priceInHexWei)}
+ key={`gas-price-button-${index}`}
+ >
+ {this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)}
+ </Button>
+ )
+ }
+
+ render () {
+ const {
+ gasButtonInfo,
+ defaultActiveButtonIndex = 1,
+ newActiveButtonIndex,
+ noButtonActiveByDefault = false,
+ buttonDataLoading,
+ ...buttonPropsAndFlags
+ } = this.props
+
+ return (
+ !buttonDataLoading
+ ? <ButtonGroup
+ className={buttonPropsAndFlags.className}
+ defaultActiveButtonIndex={defaultActiveButtonIndex}
+ newActiveButtonIndex={newActiveButtonIndex}
+ noButtonActiveByDefault={noButtonActiveByDefault}
+ >
+ { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) }
+ </ButtonGroup>
+ : <div className={`${buttonPropsAndFlags.className}__loading-container`}>{ this.context.t('loading') }</div>
+ )
+ }
+}
diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.js b/ui/app/components/app/gas-customization/gas-price-button-group/index.js
index 775648330..775648330 100644
--- a/ui/app/components/gas-customization/gas-price-button-group/index.js
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.js
diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss
index cb2f3ecf1..cb2f3ecf1 100644
--- a/ui/app/components/gas-customization/gas-price-button-group/index.scss
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js
new file mode 100644
index 000000000..37840a8a5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js
@@ -0,0 +1,233 @@
+import React from 'react'
+import assert from 'assert'
+import shallow from '../../../../../../lib/shallow-with-context'
+import sinon from 'sinon'
+import GasPriceButtonGroup from '../gas-price-button-group.component'
+
+import ButtonGroup from '../../../../ui/button-group'
+
+const mockGasPriceButtonGroupProps = {
+ buttonDataLoading: false,
+ className: 'gas-price-button-group',
+ gasButtonInfo: [
+ {
+ feeInPrimaryCurrency: '$0.52',
+ feeInSecondaryCurrency: '0.0048 ETH',
+ timeEstimate: '~ 1 min 0 sec',
+ priceInHexWei: '0xa1b2c3f',
+ },
+ {
+ feeInPrimaryCurrency: '$0.39',
+ feeInSecondaryCurrency: '0.004 ETH',
+ timeEstimate: '~ 1 min 30 sec',
+ priceInHexWei: '0xa1b2c39',
+ },
+ {
+ feeInPrimaryCurrency: '$0.30',
+ feeInSecondaryCurrency: '0.00354 ETH',
+ timeEstimate: '~ 2 min 1 sec',
+ priceInHexWei: '0xa1b2c30',
+ },
+ ],
+ handleGasPriceSelection: sinon.spy(),
+ noButtonActiveByDefault: true,
+ defaultActiveButtonIndex: 2,
+ showCheck: true,
+}
+
+const mockButtonPropsAndFlags = Object.assign({}, {
+ className: mockGasPriceButtonGroupProps.className,
+ handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection,
+ showCheck: mockGasPriceButtonGroupProps.showCheck,
+})
+
+sinon.spy(GasPriceButtonGroup.prototype, 'renderButton')
+sinon.spy(GasPriceButtonGroup.prototype, 'renderButtonContent')
+
+describe('GasPriceButtonGroup Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasPriceButtonGroup
+ {...mockGasPriceButtonGroupProps}
+ />)
+ })
+
+ afterEach(() => {
+ GasPriceButtonGroup.prototype.renderButton.resetHistory()
+ GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
+ mockGasPriceButtonGroupProps.handleGasPriceSelection.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a ButtonGroup', () => {
+ assert(wrapper.is(ButtonGroup))
+ })
+
+ it('should render the correct props on the ButtonGroup', () => {
+ const {
+ className,
+ defaultActiveButtonIndex,
+ noButtonActiveByDefault,
+ } = wrapper.props()
+ assert.equal(className, 'gas-price-button-group')
+ assert.equal(defaultActiveButtonIndex, 2)
+ assert.equal(noButtonActiveByDefault, true)
+ })
+
+ function renderButtonArgsTest (i, mockButtonPropsAndFlags) {
+ assert.deepEqual(
+ GasPriceButtonGroup.prototype.renderButton.getCall(i).args,
+ [
+ Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[i]),
+ mockButtonPropsAndFlags,
+ i,
+ ]
+ )
+ }
+
+ it('should call this.renderButton 3 times, with the correct args', () => {
+ assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3)
+ renderButtonArgsTest(0, mockButtonPropsAndFlags)
+ renderButtonArgsTest(1, mockButtonPropsAndFlags)
+ renderButtonArgsTest(2, mockButtonPropsAndFlags)
+ })
+
+ it('should show loading if buttonDataLoading', () => {
+ wrapper.setProps({ buttonDataLoading: true })
+ assert(wrapper.is('div'))
+ assert(wrapper.hasClass('gas-price-button-group__loading-container'))
+ assert.equal(wrapper.text(), 'loading')
+ })
+ })
+
+ describe('renderButton', () => {
+ let wrappedRenderButtonResult
+
+ beforeEach(() => {
+ GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
+ const renderButtonResult = GasPriceButtonGroup.prototype.renderButton(
+ Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]),
+ mockButtonPropsAndFlags
+ )
+ wrappedRenderButtonResult = shallow(renderButtonResult)
+ })
+
+ it('should render a button', () => {
+ assert.equal(wrappedRenderButtonResult.type(), 'button')
+ })
+
+ it('should call the correct method when clicked', () => {
+ assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 0)
+ wrappedRenderButtonResult.props().onClick()
+ assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 1)
+ assert.deepEqual(
+ mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args,
+ [mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei]
+ )
+ })
+
+ it('should call this.renderButtonContent with the correct args', () => {
+ assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 1)
+ const {
+ feeInPrimaryCurrency,
+ feeInSecondaryCurrency,
+ timeEstimate,
+ } = mockGasPriceButtonGroupProps.gasButtonInfo[0]
+ const {
+ showCheck,
+ className,
+ } = mockGasPriceButtonGroupProps
+ assert.deepEqual(
+ GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args,
+ [
+ {
+ feeInPrimaryCurrency,
+ feeInSecondaryCurrency,
+ timeEstimate,
+ },
+ {
+ showCheck,
+ className,
+ },
+ ]
+ )
+ })
+ })
+
+ describe('renderButtonContent', () => {
+ it('should render a label if passed a labelKey', () => {
+ const renderButtonContentResult = wrapper.instance().renderButtonContent({
+ labelKey: 'mockLabelKey',
+ }, {
+ className: 'someClass',
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
+ assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey')
+ })
+
+ it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => {
+ const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
+ feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
+ }, {
+ className: 'someClass',
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
+ assert.equal(wrappedRenderButtonContentResult.find('.someClass__primary-currency').text(), 'mockFeeInPrimaryCurrency')
+ })
+
+ it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => {
+ const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
+ feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
+ }, {
+ className: 'someClass',
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
+ assert.equal(wrappedRenderButtonContentResult.find('.someClass__secondary-currency').text(), 'mockFeeInSecondaryCurrency')
+ })
+
+ it('should render a timeEstimate if passed a timeEstimate', () => {
+ const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
+ timeEstimate: 'mockTimeEstimate',
+ }, {
+ className: 'someClass',
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
+ assert.equal(wrappedRenderButtonContentResult.find('.someClass__time-estimate').text(), 'mockTimeEstimate')
+ })
+
+ it('should render a check if showCheck is true', () => {
+ const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {
+ className: 'someClass',
+ showCheck: true,
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.find('.fa-check').length, 1)
+ })
+
+ it('should render all elements if all args passed', () => {
+ const renderButtonContentResult = wrapper.instance().renderButtonContent({
+ labelKey: 'mockLabel',
+ feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
+ feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
+ timeEstimate: 'mockTimeEstimate',
+ }, {
+ className: 'someClass',
+ showCheck: true,
+ })
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.children().length, 5)
+ })
+
+
+ it('should render no elements if all args passed', () => {
+ const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {})
+ const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
+ assert.equal(wrappedRenderButtonContentResult.children().length, 0)
+ })
+ })
+})
diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js
index c0eaf4852..c0eaf4852 100644
--- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js
+++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js
diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js
index f19dafcc1..f19dafcc1 100644
--- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js
+++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js
diff --git a/ui/app/components/gas-customization/gas-price-chart/index.js b/ui/app/components/app/gas-customization/gas-price-chart/index.js
index 9895acb62..9895acb62 100644
--- a/ui/app/components/gas-customization/gas-price-chart/index.js
+++ b/ui/app/components/app/gas-customization/gas-price-chart/index.js
diff --git a/ui/app/components/gas-customization/gas-price-chart/index.scss b/ui/app/components/app/gas-customization/gas-price-chart/index.scss
index 097543104..097543104 100644
--- a/ui/app/components/gas-customization/gas-price-chart/index.scss
+++ b/ui/app/components/app/gas-customization/gas-price-chart/index.scss
diff --git a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
new file mode 100644
index 000000000..7dec7a85f
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
@@ -0,0 +1,218 @@
+import React from 'react'
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+import shallow from '../../../../../../lib/shallow-with-context'
+import * as d3 from 'd3'
+
+function timeout (time) {
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, time)
+ })
+}
+
+const propsMethodSpies = {
+ updateCustomGasPrice: sinon.spy(),
+}
+
+const selectReturnSpies = {
+ empty: sinon.spy(),
+ remove: sinon.spy(),
+ style: sinon.spy(),
+ select: d3.select,
+ attr: sinon.spy(),
+ on: sinon.spy(),
+ datum: sinon.stub().returns({ x: 'mockX' }),
+}
+
+const mockSelectReturn = {
+ ...d3.select('div'),
+ node: () => ({
+ getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }),
+ }),
+ ...selectReturnSpies,
+}
+
+const gasPriceChartUtilsSpies = {
+ appendOrUpdateCircle: sinon.spy(),
+ generateChart: sinon.stub().returns({ mockChart: true }),
+ generateDataUIObj: sinon.spy(),
+ getAdjacentGasPrices: sinon.spy(),
+ getCoordinateData: sinon.stub().returns({ x: 'mockCoordinateX', width: 'mockWidth' }),
+ getNewXandTimeEstimate: sinon.spy(),
+ handleChartUpdate: sinon.spy(),
+ hideDataUI: sinon.spy(),
+ setSelectedCircle: sinon.spy(),
+ setTickPosition: sinon.spy(),
+ handleMouseMove: sinon.spy(),
+}
+
+const testProps = {
+ gasPrices: [1.5, 2.5, 4, 8],
+ estimatedTimes: [100, 80, 40, 10],
+ gasPricesMax: 9,
+ estimatedTimesMax: '100',
+ currentPrice: 6,
+ updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
+}
+
+const GasPriceChart = proxyquire('../gas-price-chart.component.js', {
+ './gas-price-chart.utils.js': gasPriceChartUtilsSpies,
+ 'd3': {
+ ...d3,
+ select: function (...args) {
+ const result = d3.select(...args)
+ return result.empty()
+ ? mockSelectReturn
+ : result
+ },
+ event: {
+ clientX: 'mockClientX',
+ },
+ },
+}).default
+
+sinon.spy(GasPriceChart.prototype, 'renderChart')
+
+describe('GasPriceChart Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasPriceChart {...testProps} />)
+ })
+
+ describe('render()', () => {
+ it('should render', () => {
+ assert(wrapper.hasClass('gas-price-chart'))
+ })
+
+ it('should render the chart div', () => {
+ assert(wrapper.childAt(0).hasClass('gas-price-chart__root'))
+ assert.equal(wrapper.childAt(0).props().id, 'chart')
+ })
+ })
+
+ describe('componentDidMount', () => {
+ it('should call this.renderChart with the components props', () => {
+ assert(GasPriceChart.prototype.renderChart.callCount, 1)
+ wrapper.instance().componentDidMount()
+ assert(GasPriceChart.prototype.renderChart.callCount, 2)
+ assert.deepEqual(GasPriceChart.prototype.renderChart.getCall(1).args, [{...testProps}])
+ })
+ })
+
+ describe('componentDidUpdate', () => {
+ it('should call handleChartUpdate if props.currentPrice has changed', () => {
+ gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
+ wrapper.instance().componentDidUpdate({ currentPrice: 7 })
+ assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 1)
+ })
+
+ it('should call handleChartUpdate with the correct props', () => {
+ gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
+ wrapper.instance().componentDidUpdate({ currentPrice: 7 })
+ assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
+ chart: { mockChart: true },
+ gasPrices: [1.5, 2.5, 4, 8],
+ newPrice: 6,
+ cssId: '#set-circle',
+ }])
+ })
+
+ it('should not call handleChartUpdate if props.currentPrice has not changed', () => {
+ gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
+ wrapper.instance().componentDidUpdate({ currentPrice: 6 })
+ assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 0)
+ })
+ })
+
+ describe('renderChart', () => {
+ it('should call setTickPosition 4 times, with the expected props', async () => {
+ await timeout(0)
+ gasPriceChartUtilsSpies.setTickPosition.resetHistory()
+ assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 0)
+ wrapper.instance().renderChart(testProps)
+ await timeout(0)
+ assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 4)
+ assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(0).args, ['y', 0, -5, 8])
+ assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(1).args, ['y', 1, -3, -5])
+ assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3])
+ assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8])
+ })
+
+ it('should call handleChartUpdate with the correct props', async () => {
+ await timeout(0)
+ gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
+ wrapper.instance().renderChart(testProps)
+ await timeout(0)
+ assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
+ chart: { mockChart: true },
+ gasPrices: [1.5, 2.5, 4, 8],
+ newPrice: 6,
+ cssId: '#set-circle',
+ }])
+ })
+
+ it('should add three events to the chart', async () => {
+ await timeout(0)
+ selectReturnSpies.on.resetHistory()
+ assert.equal(selectReturnSpies.on.callCount, 0)
+ wrapper.instance().renderChart(testProps)
+ await timeout(0)
+ assert.equal(selectReturnSpies.on.callCount, 3)
+
+ const firstOnEventArgs = selectReturnSpies.on.getCall(0).args
+ assert.equal(firstOnEventArgs[0], 'mouseout')
+ const secondOnEventArgs = selectReturnSpies.on.getCall(1).args
+ assert.equal(secondOnEventArgs[0], 'click')
+ const thirdOnEventArgs = selectReturnSpies.on.getCall(2).args
+ assert.equal(thirdOnEventArgs[0], 'mousemove')
+ })
+
+ it('should hide the data UI on mouseout', async () => {
+ await timeout(0)
+ selectReturnSpies.on.resetHistory()
+ wrapper.instance().renderChart(testProps)
+ gasPriceChartUtilsSpies.hideDataUI.resetHistory()
+ await timeout(0)
+ const mouseoutEventArgs = selectReturnSpies.on.getCall(0).args
+ assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 0)
+ mouseoutEventArgs[1]()
+ assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 1)
+ assert.deepEqual(gasPriceChartUtilsSpies.hideDataUI.getCall(0).args, [{ mockChart: true }, '#overlayed-circle'])
+ })
+
+ it('should updateCustomGasPrice on click', async () => {
+ await timeout(0)
+ selectReturnSpies.on.resetHistory()
+ wrapper.instance().renderChart(testProps)
+ propsMethodSpies.updateCustomGasPrice.resetHistory()
+ await timeout(0)
+ const mouseoutEventArgs = selectReturnSpies.on.getCall(1).args
+ assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 0)
+ mouseoutEventArgs[1]()
+ assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 1)
+ assert.equal(propsMethodSpies.updateCustomGasPrice.getCall(0).args[0], 'mockX')
+ })
+
+ it('should handle mousemove', async () => {
+ await timeout(0)
+ selectReturnSpies.on.resetHistory()
+ wrapper.instance().renderChart(testProps)
+ gasPriceChartUtilsSpies.handleMouseMove.resetHistory()
+ await timeout(0)
+ const mouseoutEventArgs = selectReturnSpies.on.getCall(2).args
+ assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 0)
+ mouseoutEventArgs[1]()
+ assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 1)
+ assert.deepEqual(gasPriceChartUtilsSpies.handleMouseMove.getCall(0).args, [{
+ xMousePos: 'mockClientX',
+ chartXStart: 'mockCoordinateX',
+ chartWidth: 'mockWidth',
+ gasPrices: testProps.gasPrices,
+ estimatedTimes: testProps.estimatedTimes,
+ chart: { mockChart: true },
+ }])
+ })
+ })
+})
diff --git a/ui/app/components/gas-customization/gas-slider/gas-slider.component.js b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js
index 5836e7dfc..5836e7dfc 100644
--- a/ui/app/components/gas-customization/gas-slider/gas-slider.component.js
+++ b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js
diff --git a/ui/app/components/gas-customization/gas-slider/index.js b/ui/app/components/app/gas-customization/gas-slider/index.js
index f1752c93f..f1752c93f 100644
--- a/ui/app/components/gas-customization/gas-slider/index.js
+++ b/ui/app/components/app/gas-customization/gas-slider/index.js
diff --git a/ui/app/components/gas-customization/gas-slider/index.scss b/ui/app/components/app/gas-customization/gas-slider/index.scss
index e6c734367..e6c734367 100644
--- a/ui/app/components/gas-customization/gas-slider/index.scss
+++ b/ui/app/components/app/gas-customization/gas-slider/index.scss
diff --git a/ui/app/components/gas-customization/gas.selectors.js b/ui/app/components/app/gas-customization/gas.selectors.js
index 89374b5f1..89374b5f1 100644
--- a/ui/app/components/gas-customization/gas.selectors.js
+++ b/ui/app/components/app/gas-customization/gas.selectors.js
diff --git a/ui/app/components/gas-customization/index.scss b/ui/app/components/app/gas-customization/index.scss
index b06c1d044..b06c1d044 100644
--- a/ui/app/components/gas-customization/index.scss
+++ b/ui/app/components/app/gas-customization/index.scss
diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss
new file mode 100644
index 000000000..e9bb4ac9f
--- /dev/null
+++ b/ui/app/components/app/index.scss
@@ -0,0 +1,81 @@
+@import 'account-menu/index';
+
+@import 'add-token-button/index';
+
+@import 'app-header/index';
+
+@import '../ui/breadcrumbs/index';
+
+@import '../ui/button-group/index';
+
+@import '../ui/card/index';
+
+@import 'confirm-page-container/index';
+
+@import '../ui/currency-input/index';
+
+@import '../ui/currency-display/index';
+
+@import '../ui/error-message/index';
+
+@import '../ui/export-text-container/index';
+
+@import '../ui/identicon/index';
+
+@import 'info-box/index';
+
+@import 'menu-bar/index';
+
+@import 'modal/index';
+
+@import 'modals/index';
+
+@import 'network-display/index';
+
+@import '../ui/page-container/index';
+
+@import '../../pages/index';
+
+@import 'provider-page-container/index';
+
+@import 'selected-account/index';
+
+@import '../ui/sender-to-recipient/index';
+
+@import '../ui/tabs/index';
+
+@import '../ui/token-balance/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';
+
+@import '../ui/unit-input/index';
+
+@import 'gas-customization/gas-modal-page-container/index';
+
+@import 'gas-customization/gas-modal-page-container/index';
+
+@import 'gas-customization/gas-modal-page-container/index';
+
+@import 'gas-customization/index';
+
+@import 'gas-customization/gas-price-button-group/index';
+
+@import 'ui-migration-annoucement/index';
diff --git a/ui/app/components/info-box/index.js b/ui/app/components/app/info-box/index.js
index 6110422ed..6110422ed 100644
--- a/ui/app/components/info-box/index.js
+++ b/ui/app/components/app/info-box/index.js
diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/app/info-box/index.scss
index 8b5626d79..8b5626d79 100644
--- a/ui/app/components/info-box/index.scss
+++ b/ui/app/components/app/info-box/index.scss
diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/app/info-box/info-box.component.js
index 8688b8e8f..8688b8e8f 100644
--- a/ui/app/components/info-box/info-box.component.js
+++ b/ui/app/components/app/info-box/info-box.component.js
diff --git a/ui/app/components/app/input-number.js b/ui/app/components/app/input-number.js
new file mode 100644
index 000000000..8a6ec725c
--- /dev/null
+++ b/ui/app/components/app/input-number.js
@@ -0,0 +1,81 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const {
+ addCurrencies,
+ conversionGTE,
+ conversionLTE,
+ subtractCurrencies,
+} = require('../../helpers/utils/conversion-util')
+
+module.exports = InputNumber
+
+inherits(InputNumber, Component)
+function InputNumber () {
+ Component.call(this)
+
+ this.setValue = this.setValue.bind(this)
+}
+
+function isValidInput (text) {
+ const re = /^([1-9]\d*|0)(\.|\.\d*)?$/
+ return re.test(text)
+}
+
+function removeLeadingZeroes (str) {
+ return str.replace(/^0*(?=\d)/, '')
+}
+
+InputNumber.prototype.setValue = function (newValue) {
+ newValue = removeLeadingZeroes(newValue)
+ if (newValue && !isValidInput(newValue)) return
+ const { fixed, min = -1, max = Infinity, onChange } = this.props
+
+ newValue = fixed ? newValue.toFixed(4) : newValue
+ const newValueGreaterThanMin = conversionGTE(
+ { value: newValue || '0', fromNumericBase: 'dec' },
+ { value: min, fromNumericBase: 'hex' },
+ )
+
+ const newValueLessThanMax = conversionLTE(
+ { value: newValue || '0', fromNumericBase: 'dec' },
+ { value: max, fromNumericBase: 'hex' },
+ )
+ if (newValueGreaterThanMin && newValueLessThanMax) {
+ onChange(newValue)
+ } else if (!newValueGreaterThanMin) {
+ onChange(min)
+ } else if (!newValueLessThanMax) {
+ onChange(max)
+ }
+}
+
+InputNumber.prototype.render = function () {
+ const { unitLabel, step = 1, placeholder, value } = this.props
+
+ return h('div.customize-gas-input-wrapper', {}, [
+ h('input', {
+ className: 'customize-gas-input',
+ value,
+ placeholder,
+ type: 'number',
+ onChange: e => {
+ this.setValue(e.target.value)
+ },
+ min: 0,
+ }),
+ h('span.gas-tooltip-input-detail', {}, [unitLabel]),
+ h('div.gas-tooltip-input-arrows', {}, [
+ h('div.gas-tooltip-input-arrow-wrapper', {
+ onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
+ }, [
+ 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/loading-network-screen/index.js b/ui/app/components/app/loading-network-screen/index.js
index 726b4b530..726b4b530 100644
--- a/ui/app/components/loading-network-screen/index.js
+++ b/ui/app/components/app/loading-network-screen/index.js
diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js
new file mode 100644
index 000000000..348a997c8
--- /dev/null
+++ b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js
@@ -0,0 +1,138 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Spinner from '../../ui/spinner'
+import Button from '../../ui/button'
+
+export default class LoadingNetworkScreen extends PureComponent {
+ state = {
+ showErrorScreen: false,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ loadingMessage: PropTypes.string,
+ cancelTime: PropTypes.number,
+ provider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ providerId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ showNetworkDropdown: PropTypes.func,
+ setProviderArgs: PropTypes.array,
+ lastSelectedProvider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ setProviderType: PropTypes.func,
+ isLoadingNetwork: PropTypes.bool,
+ }
+
+ componentDidMount = () => {
+ this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
+ }
+
+ getConnectingLabel = function (loadingMessage) {
+ if (loadingMessage) {
+ return loadingMessage
+ }
+ const { provider, providerId } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('connectingToMainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('connectingToRopsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('connectingToKovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('connectingToRinkeby')
+ } else {
+ name = this.context.t('connectingTo', [providerId])
+ }
+
+ return name
+ }
+
+ renderMessage = () => {
+ return <span>{ this.getConnectingLabel(this.props.loadingMessage) }</span>
+ }
+
+ renderLoadingScreenContent = () => {
+ return <div className="loading-overlay__screen-content">
+ <Spinner color="#F7C06C" />
+ {this.renderMessage()}
+ </div>
+ }
+
+ renderErrorScreenContent = () => {
+ const { showNetworkDropdown, setProviderArgs, setProviderType } = this.props
+
+ return <div className="loading-overlay__error-screen">
+ <span className="loading-overlay__emoji">&#128542;</span>
+ <span>{ this.context.t('somethingWentWrong') }</span>
+ <div className="loading-overlay__error-buttons">
+ <Button
+ type="default"
+ onClick={() => {
+ window.clearTimeout(this.cancelCallTimeout)
+ showNetworkDropdown()
+ }}
+ >
+ { this.context.t('switchNetworks') }
+ </Button>
+
+ <Button
+ type="primary"
+ onClick={() => {
+ this.setState({ showErrorScreen: false })
+ setProviderType(...setProviderArgs)
+ window.clearTimeout(this.cancelCallTimeout)
+ this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
+ }}
+ >
+ { this.context.t('tryAgain') }
+ </Button>
+ </div>
+ </div>
+ }
+
+ cancelCall = () => {
+ const { isLoadingNetwork } = this.props
+
+ if (isLoadingNetwork) {
+ this.setState({ showErrorScreen: true })
+ }
+ }
+
+ componentDidUpdate = (prevProps) => {
+ const { provider } = this.props
+ const { provider: prevProvider } = prevProps
+ if (provider.type !== prevProvider.type) {
+ window.clearTimeout(this.cancelCallTimeout)
+ this.setState({ showErrorScreen: false })
+ this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
+ }
+ }
+
+ componentWillUnmount = () => {
+ window.clearTimeout(this.cancelCallTimeout)
+ }
+
+ render () {
+ const { lastSelectedProvider, setProviderType } = this.props
+
+ return (
+ <div className="loading-overlay">
+ <div
+ className="page-container__header-close"
+ onClick={() => setProviderType(lastSelectedProvider || 'ropsten')}
+ />
+ <div className="loading-overlay__container">
+ { this.state.showErrorScreen
+ ? this.renderErrorScreenContent()
+ : this.renderLoadingScreenContent()
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js
new file mode 100644
index 000000000..87f1397ce
--- /dev/null
+++ b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js
@@ -0,0 +1,41 @@
+import { connect } from 'react-redux'
+import LoadingNetworkScreen from './loading-network-screen.component'
+import actions from '../../../store/actions'
+import { getNetworkIdentifier } from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const {
+ loadingMessage,
+ currentView,
+ } = state.appState
+ const {
+ provider,
+ lastSelectedProvider,
+ network,
+ } = state.metamask
+ const { rpcTarget, chainId, ticker, nickname, type } = provider
+
+ const setProviderArgs = type === 'rpc'
+ ? [rpcTarget, chainId, ticker, nickname]
+ : [provider.type]
+
+ return {
+ isLoadingNetwork: network === 'loading' && currentView.name !== 'config',
+ loadingMessage,
+ lastSelectedProvider,
+ setProviderArgs,
+ provider,
+ providerId: getNetworkIdentifier(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setProviderType: (type) => {
+ dispatch(actions.setProviderType(type))
+ },
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(LoadingNetworkScreen)
diff --git a/ui/app/components/menu-bar/index.js b/ui/app/components/app/menu-bar/index.js
index c5760847f..c5760847f 100644
--- a/ui/app/components/menu-bar/index.js
+++ b/ui/app/components/app/menu-bar/index.js
diff --git a/ui/app/components/menu-bar/index.scss b/ui/app/components/app/menu-bar/index.scss
index f699f4090..f699f4090 100644
--- a/ui/app/components/menu-bar/index.scss
+++ b/ui/app/components/app/menu-bar/index.scss
diff --git a/ui/app/components/app/menu-bar/menu-bar.component.js b/ui/app/components/app/menu-bar/menu-bar.component.js
new file mode 100644
index 000000000..e37fddda4
--- /dev/null
+++ b/ui/app/components/app/menu-bar/menu-bar.component.js
@@ -0,0 +1,79 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Tooltip from '../../ui/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,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideSidebar: PropTypes.func,
+ sidebarOpen: PropTypes.bool,
+ showSidebar: PropTypes.func,
+ }
+
+ state = { accountDetailsMenuOpen: false }
+
+ render () {
+ const { t } = this.context
+ const { 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={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Opened Hamburger',
+ },
+ })
+ sidebarOpen ? hideSidebar() : showSidebar()
+ }}
+ />
+ </Tooltip>
+ <SelectedAccount />
+
+ <Tooltip
+ title={t('accountOptions')}
+ position="bottom"
+ >
+ <div
+ className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Opened Account Options',
+ },
+ })
+ 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/app/menu-bar/menu-bar.container.js b/ui/app/components/app/menu-bar/menu-bar.container.js
new file mode 100644
index 000000000..059263ff3
--- /dev/null
+++ b/ui/app/components/app/menu-bar/menu-bar.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import { WALLET_VIEW_SIDEBAR } from '../sidebars/sidebar.constants'
+import MenuBar from './menu-bar.component'
+import { showSidebar, hideSidebar } from '../../../store/actions'
+
+const mapStateToProps = state => {
+ const { appState: { sidebar: { isOpen } } } = state
+
+ return {
+ sidebarOpen: isOpen,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showSidebar: () => {
+ dispatch(showSidebar({
+ transitionName: 'sidebar-right',
+ type: WALLET_VIEW_SIDEBAR,
+ }))
+ },
+ hideSidebar: () => dispatch(hideSidebar()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/app/menu-droppo.js
index c80bee2be..c80bee2be 100644
--- a/ui/app/components/menu-droppo.js
+++ b/ui/app/components/app/menu-droppo.js
diff --git a/ui/app/components/modal/index.js b/ui/app/components/app/modal/index.js
index 58309abbe..58309abbe 100644
--- a/ui/app/components/modal/index.js
+++ b/ui/app/components/app/modal/index.js
diff --git a/ui/app/components/app/modal/index.scss b/ui/app/components/app/modal/index.scss
new file mode 100644
index 000000000..ec67d15fd
--- /dev/null
+++ b/ui/app/components/app/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/app/modal/modal-content/index.js
index 733cfb3b8..733cfb3b8 100644
--- a/ui/app/components/modal/modal-content/index.js
+++ b/ui/app/components/app/modal/modal-content/index.js
diff --git a/ui/app/components/modal/modal-content/index.scss b/ui/app/components/app/modal/modal-content/index.scss
index 560505b84..560505b84 100644
--- a/ui/app/components/modal/modal-content/index.scss
+++ b/ui/app/components/app/modal/modal-content/index.scss
diff --git a/ui/app/components/modal/modal-content/modal-content.component.js b/ui/app/components/app/modal/modal-content/modal-content.component.js
index ecec0ee5b..ecec0ee5b 100644
--- a/ui/app/components/modal/modal-content/modal-content.component.js
+++ b/ui/app/components/app/modal/modal-content/modal-content.component.js
diff --git a/ui/app/components/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js
index 17af09f45..17af09f45 100644
--- a/ui/app/components/modal/modal-content/tests/modal-content.component.test.js
+++ b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js
diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js
new file mode 100644
index 000000000..49e131b3c
--- /dev/null
+++ b/ui/app/components/app/modal/modal.component.js
@@ -0,0 +1,83 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../ui/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,
+ submitDisabled: PropTypes.bool,
+ // 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,
+ submitDisabled,
+ 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}
+ disabled={submitDisabled}
+ className="modal-container__footer-button"
+ >
+ { submitText }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/modal/tests/modal.component.test.js b/ui/app/components/app/modal/tests/modal.component.test.js
new file mode 100644
index 000000000..a13d7c06a
--- /dev/null
+++ b/ui/app/components/app/modal/tests/modal.component.test.js
@@ -0,0 +1,133 @@
+import React from 'react'
+import assert from 'assert'
+import { mount, shallow } from 'enzyme'
+import sinon from 'sinon'
+import Modal from '../modal.component'
+import Button from '../../../ui/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)
+ })
+
+ it('should disable the submit button if submitDisabled is true', () => {
+ const handleCancel = sinon.spy()
+ const handleSubmit = sinon.spy()
+ const wrapper = mount(
+ <Modal
+ onCancel={handleCancel}
+ cancelText="Cancel"
+ onSubmit={handleSubmit}
+ submitText="Submit"
+ submitDisabled={true}
+ headerText="My Header"
+ onClose={handleCancel}
+ />
+ )
+
+ const buttons = wrapper.find(Button)
+ assert.equal(buttons.length, 2)
+ const cancelButton = buttons.at(0)
+ const submitButton = buttons.at(1)
+
+ assert.equal(handleCancel.callCount, 0)
+ cancelButton.simulate('click')
+ assert.equal(handleCancel.callCount, 1)
+
+ assert.equal(submitButton.props().disabled, true)
+ assert.equal(handleSubmit.callCount, 0)
+ submitButton.simulate('click')
+ assert.equal(handleSubmit.callCount, 0)
+ })
+})
diff --git a/ui/app/components/app/modals/account-details-modal.js b/ui/app/components/app/modals/account-details-modal.js
new file mode 100644
index 000000000..94ed04df9
--- /dev/null
+++ b/ui/app/components/app/modals/account-details-modal.js
@@ -0,0 +1,101 @@
+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('../../../store/actions')
+const AccountModalContainer = require('./account-modal-container')
+const { getSelectedIdentity } = require('../../../selectors/selectors')
+const genAccountLink = require('../../../../lib/account-link.js')
+const QrView = require('../../ui/qr-code')
+const EditableLabel = require('../../ui/editable-label')
+
+import Button from '../../ui/button'
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ selectedIdentity: getSelectedIdentity(state),
+ keyrings: state.metamask.keyrings,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ // Is this supposed to be used somewhere?
+ showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
+ showExportPrivateKeyModal: () => {
+ dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
+ },
+ hideModal: () => dispatch(actions.hideModal()),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
+ }
+}
+
+inherits(AccountDetailsModal, Component)
+function AccountDetailsModal () {
+ Component.call(this)
+}
+
+AccountDetailsModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal)
+
+
+// Not yet pixel perfect todos:
+ // fonts of qr-header
+
+AccountDetailsModal.prototype.render = function () {
+ const {
+ selectedIdentity,
+ network,
+ showExportPrivateKeyModal,
+ setAccountLabel,
+ keyrings,
+ } = this.props
+ const { name, address } = selectedIdentity
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(address)
+ })
+
+ let exportPrivateKeyFeatureEnabled = true
+ // This feature is disabled for hardware wallets
+ if (keyring && keyring.type.search('Hardware') !== -1) {
+ exportPrivateKeyFeatureEnabled = false
+ }
+
+ return h(AccountModalContainer, {}, [
+ h(EditableLabel, {
+ className: 'account-modal__name',
+ defaultValue: name,
+ onSubmit: label => setAccountLabel(address, label),
+ }),
+
+ h(QrView, {
+ Qr: {
+ data: address,
+ network: network,
+ },
+ }),
+
+ h('div.account-modal-divider'),
+
+ 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, {
+ type: 'primary',
+ className: 'account-modal__button',
+ onClick: () => showExportPrivateKeyModal(),
+ }, this.context.t('exportPrivateKey')) : null,
+
+ ])
+}
diff --git a/ui/app/components/app/modals/account-modal-container.js b/ui/app/components/app/modals/account-modal-container.js
new file mode 100644
index 000000000..b7ae0b5b8
--- /dev/null
+++ b/ui/app/components/app/modals/account-modal-container.js
@@ -0,0 +1,80 @@
+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('../../../store/actions')
+const { getSelectedIdentity } = require('../../../selectors/selectors')
+import Identicon from '../../ui/identicon'
+
+function mapStateToProps (state, ownProps) {
+ return {
+ selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ }
+}
+
+inherits(AccountModalContainer, Component)
+function AccountModalContainer () {
+ Component.call(this)
+}
+
+AccountModalContainer.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContainer)
+
+
+AccountModalContainer.prototype.render = function () {
+ const {
+ selectedIdentity,
+ showBackButton = false,
+ backButtonAction,
+ } = this.props
+ let { children } = this.props
+
+ if (children.constructor !== Array) {
+ children = [children]
+ }
+
+ return h('div', { style: { borderRadius: '4px' }}, [
+ h('div.account-modal-container', [
+
+ h('div', [
+
+ // Needs a border; requires changes to svg
+ h(Identicon, {
+ address: selectedIdentity.address,
+ diameter: 64,
+ style: {},
+ }),
+
+ ]),
+
+ showBackButton && h('div.account-modal-back', {
+ onClick: backButtonAction,
+ }, [
+
+ h('i.fa.fa-angle-left.fa-lg'),
+
+ h('span.account-modal-back__text', ' ' + this.context.t('back')),
+
+ ]),
+
+ h('div.account-modal-close', {
+ onClick: this.props.hideModal,
+ }),
+
+ ...children,
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/app/modals/buy-options-modal.js b/ui/app/components/app/modals/buy-options-modal.js
new file mode 100644
index 000000000..2df20e65c
--- /dev/null
+++ b/ui/app/components/app/modals/buy-options-modal.js
@@ -0,0 +1,101 @@
+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('../../../store/actions')
+const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util')
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ address: state.metamask.selectedAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ toCoinbase: (address) => {
+ dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
+ },
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ toFaucet: network => dispatch(actions.buyEth({ network })),
+ }
+}
+
+inherits(BuyOptions, Component)
+function BuyOptions () {
+ Component.call(this)
+}
+
+BuyOptions.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions)
+
+
+BuyOptions.prototype.renderModalContentOption = function (title, header, onClick) {
+ return h('div.buy-modal-content-option', {
+ onClick,
+ }, [
+ h('div.buy-modal-content-option-title', {}, title),
+ h('div.buy-modal-content-option-subtitle', {}, header),
+ ])
+}
+
+BuyOptions.prototype.render = function () {
+ const { network, toCoinbase, address, toFaucet } = this.props
+ const isTestNetwork = ['3', '4', '42'].find(n => n === network)
+ const networkName = getNetworkDisplayName(network)
+
+ return h('div', {}, [
+ h('div.buy-modal-content.transfers-subview', {
+ }, [
+ h('div.buy-modal-content-title-wrapper.flex-column.flex-center', {
+ style: {},
+ }, [
+ h('div.buy-modal-content-title', {
+ style: {},
+ }, this.context.t('transfers')),
+ h('div', {}, this.context.t('howToDeposit')),
+ ]),
+
+ h('div.buy-modal-content-options.flex-column.flex-center', {}, [
+
+ isTestNetwork
+ ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network))
+ : this.renderModalContentOption('Coinbase', this.context.t('depositFiat'), () => toCoinbase(address)),
+
+ // h('div.buy-modal-content-option', {}, [
+ // h('div.buy-modal-content-option-title', {}, 'Shapeshift'),
+ // h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'),
+ // ]),,
+
+ this.renderModalContentOption(
+ this.context.t('directDeposit'),
+ this.context.t('depositFromAccount'),
+ () => this.goToAccountDetailsModal()
+ ),
+
+ ]),
+
+ h('button', {
+ style: {
+ background: 'white',
+ },
+ onClick: () => { this.props.hideModal() },
+ }, h('div.buy-modal-content-footer#buy-modal-content-footer-text', {}, this.context.t('cancel'))),
+ ]),
+ ])
+}
+
+BuyOptions.prototype.goToAccountDetailsModal = function () {
+ this.props.hideModal()
+ this.props.showAccountDetailModal()
+}
diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js
new file mode 100644
index 000000000..beebb7ed7
--- /dev/null
+++ b/ui/app/components/app/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 UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../../../helpers/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">
+ <UserPreferencedCurrencyDisplay
+ className="cancel-transaction-gas-fee__eth"
+ value={value}
+ type={PRIMARY}
+ />
+ <UserPreferencedCurrencyDisplay
+ className="cancel-transaction-gas-fee__fiat"
+ value={value}
+ type={SECONDARY}
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js
index 1a9ae2e07..1a9ae2e07 100644
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js
+++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss
index ce81dd448..ce81dd448 100644
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss
+++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss
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/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js
index 014815503..014815503 100644
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js
+++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js
diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js
new file mode 100644
index 000000000..6bab5ec1f
--- /dev/null
+++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js
@@ -0,0 +1,76 @@
+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 '../../../../helpers/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,
+ }
+
+ state = {
+ busy: false,
+ }
+
+ componentDidUpdate () {
+ const { transactionStatus, showTransactionConfirmedModal } = this.props
+
+ if (transactionStatus !== SUBMITTED_STATUS) {
+ showTransactionConfirmedModal()
+ return
+ }
+ }
+
+ handleSubmit = async () => {
+ const { createCancelTransaction, hideModal } = this.props
+
+ this.setState({ busy: true })
+
+ await createCancelTransaction()
+ this.setState({ busy: false }, () => hideModal())
+ }
+
+ handleCancel = () => {
+ this.props.hideModal()
+ }
+
+ render () {
+ const { t } = this.context
+ const { newGasFee } = this.props
+ const { busy } = this.state
+
+ return (
+ <Modal
+ headerText={t('attemptToCancel')}
+ onClose={this.handleCancel}
+ onSubmit={this.handleSubmit}
+ onCancel={this.handleCancel}
+ submitText={t('yesLetsTry')}
+ cancelText={t('nevermind')}
+ submitType="secondary"
+ submitDisabled={busy}
+ >
+ <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/app/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js
new file mode 100644
index 000000000..6959889d9
--- /dev/null
+++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js
@@ -0,0 +1,60 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ethUtil from 'ethereumjs-util'
+import { multiplyCurrencies } from '../../../../helpers/utils/conversion-util'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import CancelTransaction from './cancel-transaction.component'
+import { showModal, createCancelTransaction } from '../../../../store/actions'
+import { getHexGasTotal } from '../../../../helpers/utils/confirm-tx.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,
+ defaultNewGasPrice,
+ newGasFee,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ createCancelTransaction: (txId, customGasPrice) => {
+ return dispatch(createCancelTransaction(txId, customGasPrice))
+ },
+ showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps
+ const { createCancelTransaction, ...restDispatchProps } = dispatchProps
+
+ return {
+ ...restStateProps,
+ ...restDispatchProps,
+ ...ownProps,
+ createCancelTransaction: () => createCancelTransaction(transactionId, defaultNewGasPrice),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps),
+)(CancelTransaction)
diff --git a/ui/app/components/modals/cancel-transaction/index.js b/ui/app/components/app/modals/cancel-transaction/index.js
index 7abc871ee..7abc871ee 100644
--- a/ui/app/components/modals/cancel-transaction/index.js
+++ b/ui/app/components/app/modals/cancel-transaction/index.js
diff --git a/ui/app/components/app/modals/cancel-transaction/index.scss b/ui/app/components/app/modals/cancel-transaction/index.scss
new file mode 100644
index 000000000..4ffb5a0f8
--- /dev/null
+++ b/ui/app/components/app/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;
+ }
+}
diff --git a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js
index 345951b0f..345951b0f 100644
--- a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js
+++ b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js
diff --git a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js
index ceaa20a95..ceaa20a95 100644
--- a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js
+++ b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js
diff --git a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js
new file mode 100644
index 000000000..2276bc7e7
--- /dev/null
+++ b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import ClearApprovedOriginsComponent from './clear-approved-origins.component'
+import { clearApprovedOrigins } from '../../../../store/actions'
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearApprovedOrigins: () => dispatch(clearApprovedOrigins()),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(null, mapDispatchToProps)
+)(ClearApprovedOriginsComponent)
diff --git a/ui/app/components/modals/clear-approved-origins/index.js b/ui/app/components/app/modals/clear-approved-origins/index.js
index b3e321995..b3e321995 100644
--- a/ui/app/components/modals/clear-approved-origins/index.js
+++ b/ui/app/components/app/modals/clear-approved-origins/index.js
diff --git a/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js
new file mode 100644
index 000000000..f35fb85a0
--- /dev/null
+++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js
@@ -0,0 +1,89 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Modal from '../../modal'
+import { addressSummary } from '../../../../helpers/utils/util'
+import Identicon from '../../../ui/identicon'
+import genAccountLink from '../../../../../lib/account-link'
+
+export default class ConfirmRemoveAccount extends Component {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ removeAccount: PropTypes.func.isRequired,
+ identity: PropTypes.object.isRequired,
+ network: PropTypes.string.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ handleRemove = () => {
+ this.props.removeAccount(this.props.identity.address)
+ .then(() => this.props.hideModal())
+ }
+
+ handleCancel = () => {
+ this.props.hideModal()
+ }
+
+ renderSelectedAccount () {
+ const { identity } = this.props
+ return (
+ <div className="confirm-remove-account__account">
+ <div className="confirm-remove-account__account__identicon">
+ <Identicon
+ address={identity.address}
+ diameter={32}
+ />
+ </div>
+ <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="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="confirm-remove-account__account__link">
+ <a
+ className=""
+ href={genAccountLink(identity.address, this.props.network)}
+ target={'_blank'}
+ title={this.context.t('etherscanView')}
+ >
+ <img src="images/popout.svg" />
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <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="confirm-remove-account__link"
+ rel="noopener noreferrer"
+ target="_blank" href="https://metamask.zendesk.com/hc/en-us/articles/360015289932">
+ { t('learnMore') }
+ </a>
+ </div>
+ </div>
+ </Modal>
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js
new file mode 100644
index 000000000..0a3cda5b6
--- /dev/null
+++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ConfirmRemoveAccount from './confirm-remove-account.component'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import { removeAccount } from '../../../../store/actions'
+
+const mapStateToProps = state => {
+ return {
+ network: state.metamask.network,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ removeAccount: (address) => dispatch(removeAccount(address)),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmRemoveAccount)
diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/app/modals/confirm-remove-account/index.js
index ecb5f7790..ecb5f7790 100644
--- a/ui/app/components/modals/confirm-remove-account/index.js
+++ b/ui/app/components/app/modals/confirm-remove-account/index.js
diff --git a/ui/app/components/modals/confirm-remove-account/index.scss b/ui/app/components/app/modals/confirm-remove-account/index.scss
index 3be3a1967..3be3a1967 100644
--- a/ui/app/components/modals/confirm-remove-account/index.scss
+++ b/ui/app/components/app/modals/confirm-remove-account/index.scss
diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js
index f1a4542ac..f1a4542ac 100644
--- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js
+++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js
diff --git a/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js
new file mode 100644
index 000000000..ffbd40d9d
--- /dev/null
+++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import ConfirmResetAccount from './confirm-reset-account.component'
+import { resetAccount } from '../../../../store/actions'
+
+const mapDispatchToProps = dispatch => {
+ return {
+ resetAccount: () => dispatch(resetAccount()),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(null, mapDispatchToProps)
+)(ConfirmResetAccount)
diff --git a/ui/app/components/modals/confirm-reset-account/index.js b/ui/app/components/app/modals/confirm-reset-account/index.js
index ca4d9c5bf..ca4d9c5bf 100644
--- a/ui/app/components/modals/confirm-reset-account/index.js
+++ b/ui/app/components/app/modals/confirm-reset-account/index.js
diff --git a/ui/app/components/app/modals/customize-gas/customize-gas.component.js b/ui/app/components/app/modals/customize-gas/customize-gas.component.js
new file mode 100644
index 000000000..5db5c79e7
--- /dev/null
+++ b/ui/app/components/app/modals/customize-gas/customize-gas.component.js
@@ -0,0 +1,162 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import BigNumber from 'bignumber.js'
+import GasModalCard from '../../customize-gas-modal/gas-modal-card'
+import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
+import Button from '../../../ui/button'
+
+import {
+ getDecimalGasLimit,
+ getDecimalGasPrice,
+ getPrefixedHexGasLimit,
+ getPrefixedHexGasPrice,
+} from './customize-gas.util'
+
+export default class CustomizeGas extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object.isRequired,
+ hideModal: PropTypes.func,
+ validate: PropTypes.func,
+ onSubmit: PropTypes.func,
+ }
+
+ state = {
+ gasPrice: 0,
+ gasLimit: 0,
+ originalGasPrice: 0,
+ originalGasLimit: 0,
+ }
+
+ componentDidMount () {
+ const { txData = {} } = this.props
+ const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData
+
+ const gasLimit = getDecimalGasLimit(hexGasLimit)
+ const gasPrice = getDecimalGasPrice(hexGasPrice)
+
+ this.setState({
+ gasPrice,
+ gasLimit,
+ originalGasPrice: gasPrice,
+ originalGasLimit: gasLimit,
+ })
+ }
+
+ handleRevert () {
+ const { originalGasPrice, originalGasLimit } = this.state
+
+ this.setState({
+ gasPrice: originalGasPrice,
+ gasLimit: originalGasLimit,
+ })
+ }
+
+ handleSave () {
+ const { onSubmit, hideModal } = this.props
+ const { gasLimit, gasPrice } = this.state
+ const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice)
+ const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit)
+
+ Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit }))
+ .then(() => hideModal())
+ }
+
+ validate () {
+ const { gasLimit, gasPrice } = this.state
+ return this.props.validate({
+ gasPrice: getPrefixedHexGasPrice(gasPrice),
+ gasLimit: getPrefixedHexGasLimit(gasLimit),
+ })
+ }
+
+ render () {
+ const { t, metricsEvent } = this.context
+ const { hideModal } = this.props
+ const { gasPrice, gasLimit, originalGasPrice, originalGasLimit } = this.state
+ const { valid, errorKey } = this.validate()
+
+ return (
+ <div className="customize-gas">
+ <div className="customize-gas__content">
+ <div className="customize-gas__header">
+ <div className="customize-gas__title">
+ { this.context.t('customGas') }
+ </div>
+ <div
+ className="customize-gas__close"
+ onClick={() => hideModal()}
+ />
+ </div>
+ <div className="customize-gas__body">
+ <GasModalCard
+ value={gasPrice}
+ min={MIN_GAS_PRICE_GWEI}
+ step={1}
+ onChange={value => this.setState({ gasPrice: value })}
+ title={t('gasPrice')}
+ copy={t('gasPriceCalculation')}
+ />
+ <GasModalCard
+ value={gasLimit}
+ min={1}
+ step={1}
+ onChange={value => this.setState({ gasLimit: value })}
+ title={t('gasLimit')}
+ copy={t('gasLimitCalculation')}
+ />
+ </div>
+ <div className="customize-gas__footer">
+ { !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> }
+ <div
+ className="customize-gas__revert"
+ onClick={() => this.handleRevert()}
+ >
+ { t('revert') }
+ </div>
+ <div className="customize-gas__buttons">
+ <Button
+ type="default"
+ className="customize-gas__cancel"
+ onClick={() => hideModal()}
+ style={{ marginRight: '10px' }}
+ >
+ { t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ className="customize-gas__save"
+ onClick={() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Activation',
+ action: 'userCloses',
+ name: 'closeCustomizeGas',
+ },
+ pageOpts: {
+ section: 'customizeGasModal',
+ component: 'customizeGasSaveButton',
+ },
+ customVariables: {
+ gasPriceChange: (new BigNumber(gasPrice)).minus(new BigNumber(originalGasPrice)).toString(10),
+ gasLimitChange: (new BigNumber(gasLimit)).minus(new BigNumber(originalGasLimit)).toString(10),
+ },
+ })
+ this.handleSave()
+ }}
+ style={{ marginRight: '10px' }}
+ disabled={!valid}
+ >
+ { t('save') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/customize-gas/customize-gas.container.js b/ui/app/components/app/modals/customize-gas/customize-gas.container.js
new file mode 100644
index 000000000..221881a8a
--- /dev/null
+++ b/ui/app/components/app/modals/customize-gas/customize-gas.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import CustomizeGas from './customize-gas.component'
+import { hideModal } from '../../../../store/actions'
+
+const mapStateToProps = state => {
+ const { appState: { modal: { modalState: { props } } } } = state
+ const { txData, onSubmit, validate } = props
+
+ return {
+ txData,
+ onSubmit,
+ validate,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas)
diff --git a/ui/app/components/app/modals/customize-gas/customize-gas.util.js b/ui/app/components/app/modals/customize-gas/customize-gas.util.js
new file mode 100644
index 000000000..e686183bd
--- /dev/null
+++ b/ui/app/components/app/modals/customize-gas/customize-gas.util.js
@@ -0,0 +1,34 @@
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil } from '../../../../helpers/utils/conversion-util'
+
+export function getDecimalGasLimit (hexGasLimit) {
+ return conversionUtil(hexGasLimit, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+}
+
+export function getDecimalGasPrice (hexGasPrice) {
+ return conversionUtil(hexGasPrice, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+}
+
+export function getPrefixedHexGasLimit (gasLimit) {
+ return ethUtil.addHexPrefix(conversionUtil(gasLimit, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ }))
+}
+
+export function getPrefixedHexGasPrice (gasPrice) {
+ return ethUtil.addHexPrefix(conversionUtil(gasPrice, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ }))
+}
diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/app/modals/customize-gas/index.js
index 3a0ab7edc..3a0ab7edc 100644
--- a/ui/app/components/modals/customize-gas/index.js
+++ b/ui/app/components/app/modals/customize-gas/index.js
diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/app/modals/customize-gas/index.scss
index e10452691..e10452691 100644
--- a/ui/app/components/modals/customize-gas/index.scss
+++ b/ui/app/components/app/modals/customize-gas/index.scss
diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js
new file mode 100644
index 000000000..082ff76a9
--- /dev/null
+++ b/ui/app/components/app/modals/deposit-ether-modal.js
@@ -0,0 +1,220 @@
+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('../../../store/actions')
+const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util')
+const ShapeshiftForm = require('../shapeshift-form')
+
+import Button from '../../ui/button'
+
+let DIRECT_DEPOSIT_ROW_TITLE
+let DIRECT_DEPOSIT_ROW_TEXT
+let WYRE_ROW_TITLE
+let WYRE_ROW_TEXT
+let SHAPESHIFT_ROW_TITLE
+let SHAPESHIFT_ROW_TEXT
+let FAUCET_ROW_TITLE
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ address: state.metamask.selectedAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ toCoinbase: (address) => {
+ dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
+ },
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ hideWarning: () => {
+ dispatch(actions.hideWarning())
+ },
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ toFaucet: network => dispatch(actions.buyEth({ network })),
+ }
+}
+
+inherits(DepositEtherModal, Component)
+function DepositEtherModal (props, context) {
+ Component.call(this)
+
+ // need to set after i18n locale has loaded
+ DIRECT_DEPOSIT_ROW_TITLE = context.t('directDepositEther')
+ DIRECT_DEPOSIT_ROW_TEXT = context.t('directDepositEtherExplainer')
+ WYRE_ROW_TITLE = context.t('buyWithWyre')
+ WYRE_ROW_TEXT = context.t('buyWithWyreDescription')
+ SHAPESHIFT_ROW_TITLE = context.t('depositShapeShift')
+ SHAPESHIFT_ROW_TEXT = context.t('depositShapeShiftExplainer')
+ FAUCET_ROW_TITLE = context.t('testFaucet')
+
+ this.state = {
+ buyingWithShapeshift: false,
+ }
+}
+
+DepositEtherModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(DepositEtherModal)
+
+
+DepositEtherModal.prototype.facuetRowText = function (networkName) {
+ return this.context.t('getEtherFromFaucet', [networkName])
+}
+
+DepositEtherModal.prototype.renderRow = function ({
+ logo,
+ title,
+ text,
+ buttonLabel,
+ onButtonClick,
+ hide,
+ className,
+ hideButton,
+ hideTitle,
+ onBackClick,
+ showBackButton,
+}) {
+ if (hide) {
+ return null
+ }
+
+ return h('div', {
+ className: className || 'deposit-ether-modal__buy-row',
+ }, [
+
+ onBackClick && showBackButton && h('div.deposit-ether-modal__buy-row__back', {
+ onClick: onBackClick,
+ }, [
+
+ h('i.fa.fa-arrow-left.cursor-pointer'),
+
+ ]),
+
+ h('div.deposit-ether-modal__buy-row__logo-container', [logo]),
+
+ h('div.deposit-ether-modal__buy-row__description', [
+
+ !hideTitle && h('div.deposit-ether-modal__buy-row__description__title', [title]),
+
+ h('div.deposit-ether-modal__buy-row__description__text', [text]),
+
+ ]),
+
+ !hideButton && h('div.deposit-ether-modal__buy-row__button', [
+ h(Button, {
+ type: 'primary',
+ className: 'deposit-ether-modal__deposit-button',
+ large: true,
+ onClick: onButtonClick,
+ }, [buttonLabel]),
+ ]),
+
+ ])
+}
+
+DepositEtherModal.prototype.render = function () {
+ const { network, toCoinbase, address, toFaucet } = this.props
+ const { buyingWithShapeshift } = this.state
+
+ const isTestNetwork = ['3', '4', '42'].find(n => n === network)
+ const networkName = getNetworkDisplayName(network)
+
+ return h('div.page-container.page-container--full-width.page-container--full-height', {}, [
+
+ h('div.page-container__header', [
+
+ h('div.page-container__title', [this.context.t('depositEther')]),
+
+ h('div.page-container__subtitle', [
+ this.context.t('needEtherInWallet'),
+ ]),
+
+ h('div.page-container__header-close', {
+ onClick: () => {
+ this.setState({ buyingWithShapeshift: false })
+ this.props.hideWarning()
+ this.props.hideModal()
+ },
+ }),
+
+ ]),
+
+ h('.page-container__content', {}, [
+
+ h('div.deposit-ether-modal__buy-rows', [
+
+ this.renderRow({
+ logo: h('img.deposit-ether-modal__logo', {
+ src: './images/deposit-eth.svg',
+ }),
+ title: DIRECT_DEPOSIT_ROW_TITLE,
+ text: DIRECT_DEPOSIT_ROW_TEXT,
+ buttonLabel: this.context.t('viewAccount'),
+ onButtonClick: () => this.goToAccountDetailsModal(),
+ hide: buyingWithShapeshift,
+ }),
+
+ this.renderRow({
+ logo: h('i.fa.fa-tint.fa-2x'),
+ title: FAUCET_ROW_TITLE,
+ text: this.facuetRowText(networkName),
+ buttonLabel: this.context.t('getEther'),
+ onButtonClick: () => toFaucet(network),
+ hide: !isTestNetwork || buyingWithShapeshift,
+ }),
+
+ this.renderRow({
+ logo: h('div.deposit-ether-modal__logo', {
+ style: {
+ backgroundImage: 'url(\'./images/wyre.svg\')',
+ height: '40px',
+ },
+ }),
+ title: WYRE_ROW_TITLE,
+ text: WYRE_ROW_TEXT,
+ buttonLabel: this.context.t('continueToWyre'),
+ onButtonClick: () => toCoinbase(address),
+ hide: isTestNetwork || buyingWithShapeshift,
+ }),
+
+ this.renderRow({
+ logo: h('div.deposit-ether-modal__logo', {
+ style: {
+ backgroundImage: 'url(\'./images/shapeshift logo.png\')',
+ },
+ }),
+ title: SHAPESHIFT_ROW_TITLE,
+ text: SHAPESHIFT_ROW_TEXT,
+ buttonLabel: this.context.t('shapeshiftBuy'),
+ onButtonClick: () => this.setState({ buyingWithShapeshift: true }),
+ hide: isTestNetwork,
+ hideButton: buyingWithShapeshift,
+ hideTitle: buyingWithShapeshift,
+ onBackClick: () => this.setState({ buyingWithShapeshift: false }),
+ showBackButton: this.state.buyingWithShapeshift,
+ className: buyingWithShapeshift && 'deposit-ether-modal__buy-row__shapeshift-buy',
+ }),
+
+ buyingWithShapeshift && h(ShapeshiftForm),
+
+ ]),
+
+ ]),
+ ])
+}
+
+DepositEtherModal.prototype.goToAccountDetailsModal = function () {
+ this.props.hideWarning()
+ this.props.hideModal()
+ this.props.showAccountDetailModal()
+}
diff --git a/ui/app/components/app/modals/edit-account-name-modal.js b/ui/app/components/app/modals/edit-account-name-modal.js
new file mode 100644
index 000000000..41a9862e9
--- /dev/null
+++ b/ui/app/components/app/modals/edit-account-name-modal.js
@@ -0,0 +1,83 @@
+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('../../../store/actions')
+const { getSelectedAccount } = require('../../../selectors/selectors')
+
+function mapStateToProps (state) {
+ return {
+ selectedAccount: getSelectedAccount(state),
+ identity: state.appState.modal.modalState.props.identity,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ setAccountLabel: (account, label) => {
+ dispatch(actions.setAccountLabel(account, label))
+ },
+ }
+}
+
+inherits(EditAccountNameModal, Component)
+function EditAccountNameModal (props) {
+ Component.call(this)
+
+ this.state = {
+ inputText: props.identity.name,
+ }
+}
+
+EditAccountNameModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameModal)
+
+
+EditAccountNameModal.prototype.render = function () {
+ const { hideModal, setAccountLabel, identity } = this.props
+
+ return h('div', {}, [
+ h('div.flex-column.edit-account-name-modal-content', {
+ }, [
+
+ h('div.edit-account-name-modal-cancel', {
+ onClick: () => {
+ hideModal()
+ },
+ }, [
+ h('i.fa.fa-times'),
+ ]),
+
+ h('div.edit-account-name-modal-title', {
+ }, [this.context.t('editAccountName')]),
+
+ h('input.edit-account-name-modal-input', {
+ placeholder: identity.name,
+ onChange: (event) => {
+ this.setState({ inputText: event.target.value })
+ },
+ value: this.state.inputText,
+ }, []),
+
+ h('button.btn-clear.edit-account-name-modal-save-button.allcaps', {
+ onClick: () => {
+ if (this.state.inputText.length !== 0) {
+ setAccountLabel(identity.address, this.state.inputText)
+ hideModal()
+ }
+ },
+ disabled: this.state.inputText.length === 0,
+ }, [
+ this.context.t('save'),
+ ]),
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/app/modals/export-private-key-modal.js b/ui/app/components/app/modals/export-private-key-modal.js
new file mode 100644
index 000000000..639887d4c
--- /dev/null
+++ b/ui/app/components/app/modals/export-private-key-modal.js
@@ -0,0 +1,177 @@
+const log = require('loglevel')
+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 { stripHexPrefix } = require('ethereumjs-util')
+const actions = require('../../../store/actions')
+const AccountModalContainer = require('./account-modal-container')
+const { getSelectedIdentity } = require('../../../selectors/selectors')
+const ReadOnlyInput = require('../../ui/readonly-input')
+const copyToClipboard = require('copy-to-clipboard')
+const { checksumAddress } = require('../../../helpers/utils/util')
+import Button from '../../ui/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) => {
+ return dispatch(actions.exportAccount(password, address))
+ .then((res) => {
+ dispatch(actions.hideWarning())
+ return res
+ })
+ },
+ showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
+ hideModal: () => dispatch(actions.hideModal()),
+ }
+}
+
+inherits(ExportPrivateKeyModal, Component)
+function ExportPrivateKeyModal () {
+ Component.call(this)
+
+ this.state = {
+ password: '',
+ privateKey: null,
+ showWarning: true,
+ }
+}
+
+ExportPrivateKeyModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
+
+
+ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
+ const { exportAccount } = this.props
+
+ exportAccount(password, address)
+ .then(privateKey => this.setState({
+ privateKey,
+ showWarning: false,
+ }))
+ .catch((e) => log.error(e))
+}
+
+ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
+ return h('span.private-key-password-label', privateKey
+ ? this.context.t('copyPrivateKey')
+ : this.context.t('typePassword')
+ )
+}
+
+ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
+ const plainKey = privateKey && stripHexPrefix(privateKey)
+
+ return privateKey
+ ? h(ReadOnlyInput, {
+ wrapperClass: 'private-key-password-display-wrapper',
+ inputClass: 'private-key-password-display-textarea',
+ textarea: true,
+ value: plainKey,
+ onClick: () => copyToClipboard(plainKey),
+ })
+ : h('input.private-key-password-input', {
+ type: 'password',
+ onChange: event => this.setState({ password: event.target.value }),
+ })
+}
+
+ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
+ return h('div.export-private-key-buttons', {}, [
+ !privateKey && h(Button, {
+ type: 'default',
+ large: true,
+ className: 'export-private-key__button export-private-key__button--cancel',
+ onClick: () => hideModal(),
+ }, this.context.t('cancel')),
+
+ (privateKey
+ ? (
+ 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'))
+ )
+ ),
+
+ ])
+}
+
+ExportPrivateKeyModal.prototype.render = function () {
+ const {
+ selectedIdentity,
+ warning,
+ showAccountDetailModal,
+ hideModal,
+ previousModalState,
+ } = this.props
+ const { name, address } = selectedIdentity
+
+ const {
+ privateKey,
+ showWarning,
+ } = this.state
+
+ return h(AccountModalContainer, {
+ selectedIdentity,
+ showBackButton: previousModalState === 'ACCOUNT_DETAILS',
+ backButtonAction: () => showAccountDetailModal(),
+ }, [
+
+ h('span.account-name', name),
+
+ h(ReadOnlyInput, {
+ wrapperClass: 'ellip-address-wrapper',
+ inputClass: 'qr-ellip-address ellip-address',
+ value: checksumAddress(address),
+ }),
+
+ h('div.account-modal-divider'),
+
+ h('span.modal-body-title', this.context.t('showPrivateKeys')),
+
+ h('div.private-key-password', {}, [
+ this.renderPasswordLabel(privateKey),
+
+ this.renderPasswordInput(privateKey),
+
+ showWarning && warning ? h('span.private-key-password-error', warning) : null,
+ ]),
+
+ h('div.private-key-password-warning', this.context.t('privateKeyWarning')),
+
+ this.renderButtons(privateKey, this.state.password, address, hideModal),
+
+ ])
+}
diff --git a/ui/app/components/app/modals/hide-token-confirmation-modal.js b/ui/app/components/app/modals/hide-token-confirmation-modal.js
new file mode 100644
index 000000000..8a9a48fd2
--- /dev/null
+++ b/ui/app/components/app/modals/hide-token-confirmation-modal.js
@@ -0,0 +1,83 @@
+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('../../../store/actions')
+import Identicon from '../../ui/identicon'
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ token: state.appState.modal.modalState.props.token,
+ assetImages: state.metamask.assetImages,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => dispatch(actions.hideModal()),
+ hideToken: address => {
+ dispatch(actions.removeToken(address))
+ .then(() => {
+ dispatch(actions.hideModal())
+ })
+ },
+ }
+}
+
+inherits(HideTokenConfirmationModal, Component)
+function HideTokenConfirmationModal () {
+ Component.call(this)
+
+ this.state = {}
+}
+
+HideTokenConfirmationModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmationModal)
+
+
+HideTokenConfirmationModal.prototype.render = function () {
+ 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', {
+ }, [
+ h('div.hide-token-confirmation__title', {}, [
+ this.context.t('hideTokenPrompt'),
+ ]),
+
+ h(Identicon, {
+ className: 'hide-token-confirmation__identicon',
+ diameter: 45,
+ address,
+ network,
+ image,
+ }),
+
+ h('div.hide-token-confirmation__symbol', {}, symbol),
+
+ h('div.hide-token-confirmation__copy', {}, [
+ this.context.t('readdToken'),
+ ]),
+
+ h('div.hide-token-confirmation__buttons', {}, [
+ h('button.btn-cancel.hide-token-confirmation__button.allcaps', {
+ onClick: () => hideModal(),
+ }, [
+ this.context.t('cancel'),
+ ]),
+ h('button.btn-clear.hide-token-confirmation__button.allcaps', {
+ onClick: () => hideToken(address),
+ }, [
+ this.context.t('hide'),
+ ]),
+ ]),
+ ]),
+ ])
+}
diff --git a/ui/app/components/modals/index.js b/ui/app/components/app/modals/index.js
index 1db1d33d4..1db1d33d4 100644
--- a/ui/app/components/modals/index.js
+++ b/ui/app/components/app/modals/index.js
diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss
new file mode 100644
index 000000000..09b0bb73c
--- /dev/null
+++ b/ui/app/components/app/modals/index.scss
@@ -0,0 +1,11 @@
+@import 'cancel-transaction/index';
+
+@import 'confirm-remove-account/index';
+
+@import 'customize-gas/index';
+
+@import 'qr-scanner/index';
+
+@import 'transaction-confirmed/index';
+
+@import 'metametrics-opt-in-modal/index';
diff --git a/ui/app/components/modals/loading-network-error/index.js b/ui/app/components/app/modals/loading-network-error/index.js
index b3737458a..b3737458a 100644
--- a/ui/app/components/modals/loading-network-error/index.js
+++ b/ui/app/components/app/modals/loading-network-error/index.js
diff --git a/ui/app/components/modals/loading-network-error/loading-network-error.component.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js
index 44f71e4b2..44f71e4b2 100644
--- a/ui/app/components/modals/loading-network-error/loading-network-error.component.js
+++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js
diff --git a/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js
new file mode 100644
index 000000000..38ea9b2ab
--- /dev/null
+++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js
@@ -0,0 +1,4 @@
+import LoadingNetworkError from './loading-network-error.component'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+
+export default withModalProps(LoadingNetworkError)
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/index.js b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js
index 47f946757..47f946757 100644
--- a/ui/app/components/modals/metametrics-opt-in-modal/index.js
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/index.scss b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss
index 88b6d7a4d..88b6d7a4d 100644
--- a/ui/app/components/modals/metametrics-opt-in-modal/index.scss
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss
diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
new file mode 100644
index 000000000..0335991fc
--- /dev/null
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
@@ -0,0 +1,141 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../../ui/page-container/page-container-footer'
+
+export default class MetaMetricsOptInModal extends Component {
+ static propTypes = {
+ setParticipateInMetaMetrics: PropTypes.func,
+ hideModal: PropTypes.func,
+ }
+
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ render () {
+ const { metricsEvent } = this.context
+ const { setParticipateInMetaMetrics, hideModal } = this.props
+
+ return (
+ <div className="metametrics-opt-in metametrics-opt-in-modal">
+ <div className="metametrics-opt-in__main">
+ <div className="metametrics-opt-in__content">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <div className="metametrics-opt-in__body-graphic">
+ <img src="images/metrics-chart.svg" />
+ </div>
+ <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
+ <div className="metametrics-opt-in__body">
+ <div className="metametrics-opt-in__description">
+ MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
+ will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
+ </div>
+ <div className="metametrics-opt-in__description">
+ MetaMask will..
+ </div>
+
+ <div className="metametrics-opt-in__committments">
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Always allow you to opt-out via Settings
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Send anonymized click & pageview events
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Maintain a public aggregate dashboard to educate the community
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="metametrics-opt-in__bottom-text">
+ This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Privacy Policy here
+ </a>.
+ </div>
+ </div>
+ <div className="metametrics-opt-in__footer">
+ <PageContainerFooter
+ onCancel={() => {
+ setParticipateInMetaMetrics(false)
+ .then(() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt Out',
+ },
+ isOptIn: true,
+ }, {
+ excludeMetaMetricsId: true,
+ })
+ hideModal()
+ })
+ }}
+ cancelText={'No Thanks'}
+ hideCancel={false}
+ onSubmit={() => {
+ setParticipateInMetaMetrics(true)
+ .then(() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt In',
+ },
+ isOptIn: true,
+ })
+ hideModal()
+ })
+ }}
+ submitText={'I agree'}
+ submitButtonType={'confirm'}
+ disabled={false}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
new file mode 100644
index 000000000..83595281f
--- /dev/null
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import MetaMetricsOptInModal from './metametrics-opt-in-modal.component'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import { setParticipateInMetaMetrics } from '../../../../store/actions'
+
+const mapStateToProps = (state, ownProps) => {
+ const { unapprovedTxCount } = ownProps
+
+ return {
+ unapprovedTxCount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps),
+)(MetaMetricsOptInModal)
diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js
new file mode 100644
index 000000000..717f623af
--- /dev/null
+++ b/ui/app/components/app/modals/modal.js
@@ -0,0 +1,511 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const FadeModal = require('boron').FadeModal
+const actions = require('../../../store/actions')
+const { resetCustomData: resetCustomGasData } = require('../../../ducks/gas/gas.duck')
+const isMobileView = require('../../../../lib/is-mobile-view')
+const { getEnvironmentType } = require('../../../../../app/scripts/lib/util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
+
+// Modal Components
+const BuyOptions = require('./buy-options-modal')
+const DepositEtherModal = require('./deposit-ether-modal')
+const AccountDetailsModal = require('./account-details-modal')
+const EditAccountNameModal = require('./edit-account-name-modal')
+const ExportPrivateKeyModal = require('./export-private-key-modal')
+const NewAccountModal = require('./new-account-modal')
+const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
+const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
+const NotifcationModal = require('./notification-modal')
+const QRScanner = require('./qr-scanner')
+
+import ConfirmRemoveAccount from './confirm-remove-account'
+import ConfirmResetAccount from './confirm-reset-account'
+import TransactionConfirmed from './transaction-confirmed'
+import CancelTransaction from './cancel-transaction'
+
+import MetaMetricsOptInModal from './metametrics-opt-in-modal'
+import RejectTransactions from './reject-transactions'
+import ClearApprovedOrigins from './clear-approved-origins'
+import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
+
+const modalContainerBaseStyle = {
+ transform: 'translate3d(-50%, 0, 0px)',
+ border: '1px solid #CCCFD1',
+ borderRadius: '8px',
+ backgroundColor: '#FFFFFF',
+ boxShadow: '0 2px 22px 0 rgba(0,0,0,0.2)',
+}
+
+const modalContainerLaptopStyle = {
+ ...modalContainerBaseStyle,
+ width: '344px',
+ top: '15%',
+}
+
+const modalContainerMobileStyle = {
+ ...modalContainerBaseStyle,
+ width: '309px',
+ top: '12.5%',
+}
+
+const accountModalStyle = {
+ mobileModalStyle: {
+ width: '95%',
+ // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ borderRadius: '4px',
+ top: '10%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '360px',
+ // top: 'calc(33% + 45px)',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ borderRadius: '4px',
+ top: '10%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ contentStyle: {
+ borderRadius: '4px',
+ },
+}
+
+const MODALS = {
+ BUY: {
+ contents: [
+ h(BuyOptions, {}, []),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
+ top: '10%',
+ },
+ laptopModalStyle: {
+ width: '66%',
+ maxWidth: '550px',
+ top: 'calc(10% + 10px)',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
+ transform: 'none',
+ },
+ },
+
+ DEPOSIT_ETHER: {
+ contents: [
+ h(DepositEtherModal, {}, []),
+ ],
+ onHide: (props) => props.hideWarning(),
+ mobileModalStyle: {
+ width: '100%',
+ height: '100%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
+ top: '0',
+ display: 'flex',
+ },
+ laptopModalStyle: {
+ width: 'initial',
+ maxWidth: '850px',
+ top: 'calc(10% + 10px)',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ boxShadow: '0 0 6px 0 rgba(0,0,0,0.3)',
+ borderRadius: '7px',
+ transform: 'none',
+ height: 'calc(80% - 20px)',
+ overflowY: 'hidden',
+ },
+ contentStyle: {
+ borderRadius: '7px',
+ height: '100%',
+ },
+ },
+
+ EDIT_ACCOUNT_NAME: {
+ contents: [
+ h(EditAccountNameModal, {}, []),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '375px',
+ // top: 'calc(30% + 10px)',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ },
+
+ ACCOUNT_DETAILS: {
+ contents: [
+ h(AccountDetailsModal, {}, []),
+ ],
+ ...accountModalStyle,
+ },
+
+ EXPORT_PRIVATE_KEY: {
+ contents: [
+ h(ExportPrivateKeyModal, {}, []),
+ ],
+ ...accountModalStyle,
+ },
+
+ SHAPESHIFT_DEPOSIT_TX: {
+ contents: [
+ h(ShapeshiftDepositTxModal),
+ ],
+ ...accountModalStyle,
+ },
+
+ HIDE_TOKEN_CONFIRMATION: {
+ contents: [
+ h(HideTokenConfirmationModal, {}, []),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
+ },
+ laptopModalStyle: {
+ width: '449px',
+ top: 'calc(33% + 45px)',
+ },
+ },
+
+ CLEAR_APPROVED_ORIGINS: {
+ contents: h(ClearApprovedOrigins),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ METAMETRICS_OPT_IN_MODAL: {
+ contents: h(MetaMetricsOptInModal),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ width: '100%',
+ height: '100%',
+ top: '0px',
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ top: '10%',
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ OLD_UI_NOTIFICATION_MODAL: {
+ contents: [
+ h(NotifcationModal, {
+ header: 'oldUI',
+ message: 'oldUIMessage',
+ }),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
+ },
+ laptopModalStyle: {
+ width: '449px',
+ top: 'calc(33% + 45px)',
+ },
+ },
+
+ GAS_PRICE_INFO_MODAL: {
+ contents: [
+ h(NotifcationModal, {
+ header: 'gasPriceNoDenom',
+ message: 'gasPriceInfoModalContent',
+ }),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
+ },
+ laptopModalStyle: {
+ width: '449px',
+ top: 'calc(33% + 45px)',
+ },
+ },
+
+ GAS_LIMIT_INFO_MODAL: {
+ contents: [
+ h(NotifcationModal, {
+ header: 'gasLimit',
+ message: 'gasLimitInfoModalContent',
+ }),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
+ },
+ laptopModalStyle: {
+ width: '449px',
+ top: 'calc(33% + 45px)',
+ },
+ },
+
+ CONFIRM_RESET_ACCOUNT: {
+ contents: h(ConfirmResetAccount),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ CONFIRM_REMOVE_ACCOUNT: {
+ contents: h(ConfirmRemoveAccount),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ NEW_ACCOUNT: {
+ contents: [
+ h(NewAccountModal, {}, []),
+ ],
+ mobileModalStyle: {
+ width: '95%',
+ // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
+ top: '10%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '449px',
+ // top: 'calc(33% + 45px)',
+ top: '10%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ },
+
+ CUSTOMIZE_GAS: {
+ contents: [
+ h(ConfirmCustomizeGasModal),
+ ],
+ mobileModalStyle: {
+ width: '100vw',
+ height: '100vh',
+ top: '0',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: 'auto',
+ height: '0px',
+ top: '80px',
+ left: '0px',
+ transform: 'none',
+ margin: '0 auto',
+ position: 'relative',
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ customOnHideOpts: {
+ action: resetCustomGasData,
+ args: [],
+ },
+ },
+
+ TRANSACTION_CONFIRMED: {
+ disableBackdropClick: true,
+ contents: h(TransactionConfirmed),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ QR_SCANNER: {
+ contents: h(QRScanner),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ CANCEL_TRANSACTION: {
+ contents: h(CancelTransaction),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ REJECT_TRANSACTIONS: {
+ contents: h(RejectTransactions),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
+ DEFAULT: {
+ contents: [],
+ mobileModalStyle: {},
+ laptopModalStyle: {},
+ },
+}
+
+const BACKDROPSTYLE = {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+}
+
+function mapStateToProps (state) {
+ return {
+ active: state.appState.modal.open,
+ modalState: state.appState.modal.modalState,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: (customOnHideOpts) => {
+ dispatch(actions.hideModal())
+ if (customOnHideOpts && customOnHideOpts.action) {
+ dispatch(customOnHideOpts.action(...customOnHideOpts.args))
+ }
+ },
+ hideWarning: () => {
+ dispatch(actions.hideWarning())
+ },
+
+ }
+}
+
+// Global Modal Component
+inherits(Modal, Component)
+function Modal () {
+ Component.call(this)
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal)
+
+Modal.prototype.render = function () {
+ const modal = MODALS[this.props.modalState.name || 'DEFAULT']
+
+ const { contents: children, disableBackdropClick = false } = modal
+ const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle']
+ const contentStyle = modal.contentStyle || {}
+
+ return h(FadeModal,
+ {
+ className: 'modal',
+ keyboard: false,
+ onHide: () => {
+ if (modal.onHide) {
+ modal.onHide(this.props)
+ }
+ this.onHide(modal.customOnHideOpts)
+ },
+ ref: (ref) => {
+ this.modalRef = ref
+ },
+ modalStyle,
+ contentStyle,
+ backdropStyle: BACKDROPSTYLE,
+ closeOnClick: !disableBackdropClick,
+ },
+ children,
+ )
+}
+
+Modal.prototype.componentWillReceiveProps = function (nextProps) {
+ if (nextProps.active) {
+ this.show()
+ } else if (this.props.active) {
+ this.hide()
+ }
+}
+
+Modal.prototype.onHide = function (customOnHideOpts) {
+ if (this.props.onHideCallback) {
+ this.props.onHideCallback()
+ }
+ this.props.hideModal(customOnHideOpts)
+}
+
+Modal.prototype.hide = function () {
+ this.modalRef.hide()
+}
+
+Modal.prototype.show = function () {
+ this.modalRef.show()
+}
diff --git a/ui/app/components/app/modals/new-account-modal.js b/ui/app/components/app/modals/new-account-modal.js
new file mode 100644
index 000000000..27c81a701
--- /dev/null
+++ b/ui/app/components/app/modals/new-account-modal.js
@@ -0,0 +1,112 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+
+class NewAccountModal extends Component {
+ constructor (props) {
+ super(props)
+ const { numberOfExistingAccounts = 0 } = props
+ const newAccountNumber = numberOfExistingAccounts + 1
+
+ this.state = {
+ newAccountName: `${this.context.t('account')} ${newAccountNumber}`,
+ }
+ }
+
+ render () {
+ const { newAccountName } = this.state
+
+ return h('div', [
+ h('div.new-account-modal-wrapper', {
+ }, [
+ h('div.new-account-modal-header', {}, [
+ this.context.t('newAccount'),
+ ]),
+
+ h('div.modal-close-x', {
+ onClick: this.props.hideModal,
+ }),
+
+ h('div.new-account-modal-content', {}, [
+ this.context.t('accountName'),
+ ]),
+
+ h('div.new-account-input-wrapper', {}, [
+ h('input.new-account-input', {
+ value: this.state.newAccountName,
+ placeholder: this.context.t('sampleAccountName'),
+ onChange: event => this.setState({ newAccountName: event.target.value }),
+ }, []),
+ ]),
+
+ h('div.new-account-modal-content.after-input', {}, [
+ this.context.t('or'),
+ ]),
+
+ h('div.new-account-modal-content.after-input.pointer', {
+ onClick: () => {
+ this.props.hideModal()
+ this.props.showImportPage()
+ },
+ }, this.context.t('importAnAccount')),
+
+ h('div.new-account-modal-content.button.allcaps', {}, [
+ h('button.btn-clear', {
+ onClick: () => this.props.createAccount(newAccountName),
+ }, [
+ this.context.t('save'),
+ ]),
+ ]),
+ ]),
+ ])
+ }
+}
+
+NewAccountModal.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ createAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { network, selectedAddress, identities = {} } } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ toCoinbase: (address) => {
+ dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
+ },
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ createAccount: (newAccountName) => {
+ dispatch(actions.addNewAccount())
+ .then((newAccountAddress) => {
+ if (newAccountName) {
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
+ }
+ dispatch(actions.hideModal())
+ })
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ }
+}
+
+NewAccountModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal)
+
diff --git a/ui/app/components/app/modals/notification-modal.js b/ui/app/components/app/modals/notification-modal.js
new file mode 100644
index 000000000..2d73b2cfa
--- /dev/null
+++ b/ui/app/components/app/modals/notification-modal.js
@@ -0,0 +1,81 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+
+class NotificationModal extends Component {
+ render () {
+ const {
+ header,
+ message,
+ showCancelButton = false,
+ showConfirmButton = false,
+ hideModal,
+ onConfirm,
+ } = this.props
+
+ const showButtons = showCancelButton || showConfirmButton
+
+ return h('div', [
+ h('div.notification-modal__wrapper', {
+ }, [
+
+ h('div.notification-modal__header', {}, [
+ this.context.t(header),
+ ]),
+
+ h('div.notification-modal__message-wrapper', {}, [
+ h('div.notification-modal__message', {}, [
+ this.context.t(message),
+ ]),
+ ]),
+
+ h('div.modal-close-x', {
+ onClick: hideModal,
+ }),
+
+ showButtons && h('div.notification-modal__buttons', [
+
+ showCancelButton && h('div.btn-cancel.notification-modal__buttons__btn', {
+ onClick: hideModal,
+ }, 'Cancel'),
+
+ showConfirmButton && h('div.btn-clear.notification-modal__buttons__btn', {
+ onClick: () => {
+ onConfirm()
+ hideModal()
+ },
+ }, 'Confirm'),
+
+ ]),
+
+ ]),
+ ])
+ }
+}
+
+NotificationModal.propTypes = {
+ hideModal: PropTypes.func,
+ header: PropTypes.string,
+ message: PropTypes.node,
+ showCancelButton: PropTypes.bool,
+ showConfirmButton: PropTypes.bool,
+ onConfirm: PropTypes.func,
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ }
+}
+
+NotificationModal.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(null, mapDispatchToProps)(NotificationModal)
+
diff --git a/ui/app/components/modals/qr-scanner/index.js b/ui/app/components/app/modals/qr-scanner/index.js
index 470dee1f4..470dee1f4 100644
--- a/ui/app/components/modals/qr-scanner/index.js
+++ b/ui/app/components/app/modals/qr-scanner/index.js
diff --git a/ui/app/components/modals/qr-scanner/index.scss b/ui/app/components/app/modals/qr-scanner/index.scss
index 6fa81d51f..6fa81d51f 100644
--- a/ui/app/components/modals/qr-scanner/index.scss
+++ b/ui/app/components/app/modals/qr-scanner/index.scss
diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js
new file mode 100644
index 000000000..20915b5f9
--- /dev/null
+++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js
@@ -0,0 +1,216 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { BrowserQRCodeReader } from '@zxing/library'
+import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars
+import Spinner from '../../../ui/spinner'
+import WebcamUtils from '../../../../../lib/webcam-utils'
+import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component'
+
+export default class QrScanner extends Component {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ qrCodeDetected: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ error: PropTypes.bool,
+ errorType: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props, context) {
+ super(props)
+
+ this.state = {
+ ready: false,
+ msg: context.t('accessingYourCamera'),
+ }
+ this.codeReader = null
+ this.permissionChecker = null
+ this.needsToReinit = false
+
+ // Clear pre-existing qr code data before scanning
+ this.props.qrCodeDetected(null)
+ }
+
+ componentDidMount () {
+ this.initCamera()
+ }
+
+ async checkPermisisions () {
+ const { permissions } = await WebcamUtils.checkStatus()
+ if (permissions) {
+ clearTimeout(this.permissionChecker)
+ // Let the video stream load first...
+ setTimeout(_ => {
+ this.setState({
+ ready: true,
+ msg: this.context.t('scanInstructions'),
+ })
+ if (this.needsToReinit) {
+ this.initCamera()
+ this.needsToReinit = false
+ }
+ }, 2000)
+ } else {
+ // Keep checking for permissions
+ this.permissionChecker = setTimeout(_ => {
+ this.checkPermisisions()
+ }, 1000)
+ }
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.permissionChecker)
+ if (this.codeReader) {
+ this.codeReader.reset()
+ }
+ }
+
+ initCamera () {
+ this.codeReader = new BrowserQRCodeReader()
+ this.codeReader.getVideoInputDevices()
+ .then(videoInputDevices => {
+ clearTimeout(this.permissionChecker)
+ this.checkPermisisions()
+ this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
+ .then(content => {
+ const result = this.parseContent(content.text)
+ if (result.type !== 'unknown') {
+ this.props.qrCodeDetected(result)
+ this.stopAndClose()
+ } else {
+ this.setState({msg: this.context.t('unknownQrCode')})
+ }
+ })
+ .catch(err => {
+ if (err && err.name === 'NotAllowedError') {
+ this.setState({msg: this.context.t('youNeedToAllowCameraAccess')})
+ clearTimeout(this.permissionChecker)
+ this.needsToReinit = true
+ this.checkPermisisions()
+ }
+ })
+ }).catch(err => {
+ console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err)
+ })
+ }
+
+ parseContent (content) {
+ let type = 'unknown'
+ let values = {}
+
+ // Here we could add more cases
+ // To parse other type of links
+ // For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
+
+
+ // Ethereum address links - fox ex. ethereum:0x.....1111
+ if (content.split('ethereum:').length > 1) {
+
+ type = 'address'
+ values = {'address': content.split('ethereum:')[1] }
+
+ // Regular ethereum addresses - fox ex. 0x.....1111
+ } else if (content.substring(0, 2).toLowerCase() === '0x') {
+
+ type = 'address'
+ values = {'address': content }
+
+ }
+ return {type, values}
+ }
+
+
+ stopAndClose = () => {
+ if (this.codeReader) {
+ this.codeReader.reset()
+ }
+ this.setState({ ready: false })
+ this.props.hideModal()
+ }
+
+ tryAgain = () => {
+ // close the modal
+ this.stopAndClose()
+ // wait for the animation and try again
+ setTimeout(_ => {
+ this.props.scanQrCode()
+ }, 1000)
+ }
+
+ renderVideo () {
+ return (
+ <div className={'qr-scanner__content__video-wrapper'}>
+ <video
+ id="video"
+ style={{
+ display: this.state.ready ? 'block' : 'none',
+ }}
+ />
+ { !this.state.ready ? <Spinner color={'#F7C06C'} /> : null}
+ </div>
+ )
+ }
+
+ renderErrorModal () {
+ let title, msg
+
+ if (this.props.error) {
+ if (this.props.errorType === 'NO_WEBCAM_FOUND') {
+ title = this.context.t('noWebcamFoundTitle')
+ msg = this.context.t('noWebcamFound')
+ } else {
+ title = this.context.t('unknownCameraErrorTitle')
+ msg = this.context.t('unknownCameraError')
+ }
+ }
+
+ return (
+ <div className="qr-scanner">
+ <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
+
+ <div className="qr-scanner__image">
+ <img src={'images/webcam.svg'} width={70} height={70} />
+ </div>
+ <div className="qr-scanner__title">
+ { title }
+ </div>
+ <div className={'qr-scanner__error'}>
+ {msg}
+ </div>
+ <PageContainerFooter
+ onCancel={this.stopAndClose}
+ onSubmit={this.tryAgain}
+ cancelText={this.context.t('cancel')}
+ submitText={this.context.t('tryAgain')}
+ submitButtonType="confirm"
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ if (this.props.error) {
+ return this.renderErrorModal()
+ }
+
+ return (
+ <div className="qr-scanner">
+ <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
+ <div className="qr-scanner__title">
+ { `${t('scanQrCode')}` }
+ </div>
+ <div className="qr-scanner__content">
+ { this.renderVideo() }
+ </div>
+ <div className={'qr-scanner__status'}>
+ {this.state.msg}
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.container.js
new file mode 100644
index 000000000..2210fbed2
--- /dev/null
+++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.container.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux'
+import QrScanner from './qr-scanner.component'
+
+const { hideModal, qrCodeDetected, showQrScanner } = require('../../../../store/actions')
+import {
+ SEND_ROUTE,
+} from '../../../../helpers/constants/routes'
+
+const mapStateToProps = state => {
+ return {
+ error: state.appState.modal.modalState.props.error,
+ errorType: state.appState.modal.modalState.props.errorType,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
+ scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(QrScanner)
diff --git a/ui/app/components/modals/reject-transactions/index.js b/ui/app/components/app/modals/reject-transactions/index.js
index fcdc372b6..fcdc372b6 100644
--- a/ui/app/components/modals/reject-transactions/index.js
+++ b/ui/app/components/app/modals/reject-transactions/index.js
diff --git a/ui/app/components/modals/reject-transactions/index.scss b/ui/app/components/app/modals/reject-transactions/index.scss
index 753466883..753466883 100644
--- a/ui/app/components/modals/reject-transactions/index.scss
+++ b/ui/app/components/app/modals/reject-transactions/index.scss
diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.component.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js
index 60b259bdc..60b259bdc 100644
--- a/ui/app/components/modals/reject-transactions/reject-transactions.component.js
+++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js
diff --git a/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js
new file mode 100644
index 000000000..d2af05573
--- /dev/null
+++ b/ui/app/components/app/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 '../../../../helpers/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/app/modals/shapeshift-deposit-tx-modal.js b/ui/app/components/app/modals/shapeshift-deposit-tx-modal.js
new file mode 100644
index 000000000..ada9430f7
--- /dev/null
+++ b/ui/app/components/app/modals/shapeshift-deposit-tx-modal.js
@@ -0,0 +1,40 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const QrView = require('../../ui/qr-code')
+const AccountModalContainer = require('./account-modal-container')
+
+function mapStateToProps (state) {
+ return {
+ Qr: state.appState.modal.modalState.props.Qr,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => {
+ dispatch(actions.hideModal())
+ },
+ }
+}
+
+inherits(ShapeshiftDepositTxModal, Component)
+function ShapeshiftDepositTxModal () {
+ Component.call(this)
+
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftDepositTxModal)
+
+ShapeshiftDepositTxModal.prototype.render = function () {
+ const { Qr } = this.props
+
+ return h(AccountModalContainer, {
+ }, [
+ h('div', {}, [
+ h(QrView, {key: 'qr', Qr}),
+ ]),
+ ])
+}
diff --git a/ui/app/components/modals/transaction-confirmed/index.js b/ui/app/components/app/modals/transaction-confirmed/index.js
index 7776b969e..7776b969e 100644
--- a/ui/app/components/modals/transaction-confirmed/index.js
+++ b/ui/app/components/app/modals/transaction-confirmed/index.js
diff --git a/ui/app/components/modals/transaction-confirmed/index.scss b/ui/app/components/app/modals/transaction-confirmed/index.scss
index c97371fb6..c97371fb6 100644
--- a/ui/app/components/modals/transaction-confirmed/index.scss
+++ b/ui/app/components/app/modals/transaction-confirmed/index.scss
diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js
index 0a98eb1a1..0a98eb1a1 100644
--- a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js
+++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js
diff --git a/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js
new file mode 100644
index 000000000..9089ec158
--- /dev/null
+++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js
@@ -0,0 +1,4 @@
+import TransactionConfirmed from './transaction-confirmed.component'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+
+export default withModalProps(TransactionConfirmed)
diff --git a/ui/app/components/network-display/index.js b/ui/app/components/app/network-display/index.js
index f6878ae5b..f6878ae5b 100644
--- a/ui/app/components/network-display/index.js
+++ b/ui/app/components/app/network-display/index.js
diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/app/network-display/index.scss
index e9f2f2057..e9f2f2057 100644
--- a/ui/app/components/network-display/index.scss
+++ b/ui/app/components/app/network-display/index.scss
diff --git a/ui/app/components/app/network-display/network-display.component.js b/ui/app/components/app/network-display/network-display.component.js
new file mode 100644
index 000000000..1142e8606
--- /dev/null
+++ b/ui/app/components/app/network-display/network-display.component.js
@@ -0,0 +1,76 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import {
+ MAINNET_CODE,
+ ROPSTEN_CODE,
+ RINKEYBY_CODE,
+ KOVAN_CODE,
+} from '../../../../../app/scripts/controllers/network/enums'
+
+const networkToClassHash = {
+ [MAINNET_CODE]: 'mainnet',
+ [ROPSTEN_CODE]: 'ropsten',
+ [RINKEYBY_CODE]: 'rinkeby',
+ [KOVAN_CODE]: 'kovan',
+}
+
+export default class NetworkDisplay extends Component {
+ static defaultProps = {
+ colored: true,
+ }
+
+ static propTypes = {
+ colored: PropTypes.bool,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderNetworkIcon () {
+ const { network } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+
+ render () {
+ const { colored, network, provider: { type, nickname } } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return (
+ <div
+ className={classnames('network-display__container', {
+ 'network-display__container--colored': colored,
+ ['network-display__container--' + networkClass]: colored && networkClass,
+ })}
+ >
+ {
+ networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+ <div className="network-display__name">
+ { type === 'rpc' && nickname ? nickname : this.context.t(type) }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/app/network-display/network-display.container.js
index 99a14fff4..99a14fff4 100644
--- a/ui/app/components/network-display/network-display.container.js
+++ b/ui/app/components/app/network-display/network-display.container.js
diff --git a/ui/app/components/network.js b/ui/app/components/app/network.js
index e18404f42..e18404f42 100644
--- a/ui/app/components/network.js
+++ b/ui/app/components/app/network.js
diff --git a/ui/app/components/notice.js b/ui/app/components/app/notice.js
index bb7e0814c..bb7e0814c 100644
--- a/ui/app/components/notice.js
+++ b/ui/app/components/app/notice.js
diff --git a/ui/app/components/provider-page-container/index.js b/ui/app/components/app/provider-page-container/index.js
index 927c35940..927c35940 100644
--- a/ui/app/components/provider-page-container/index.js
+++ b/ui/app/components/app/provider-page-container/index.js
diff --git a/ui/app/components/provider-page-container/index.scss b/ui/app/components/app/provider-page-container/index.scss
index 8d35ac179..8d35ac179 100644
--- a/ui/app/components/provider-page-container/index.scss
+++ b/ui/app/components/app/provider-page-container/index.scss
diff --git a/ui/app/components/provider-page-container/provider-page-container-content/index.js b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js
index 73e491adc..73e491adc 100644
--- a/ui/app/components/provider-page-container/provider-page-container-content/index.js
+++ b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
new file mode 100644
index 000000000..0eb1d616a
--- /dev/null
+++ b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
@@ -0,0 +1,77 @@
+import PropTypes from 'prop-types'
+import React, {PureComponent} from 'react'
+import Identicon from '../../../ui/identicon'
+
+export default class ProviderPageContainerContent extends PureComponent {
+ static propTypes = {
+ origin: PropTypes.string.isRequired,
+ selectedIdentity: PropTypes.string.isRequired,
+ siteImage: PropTypes.string,
+ siteTitle: PropTypes.string.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ renderConnectVisual = () => {
+ const { origin, selectedIdentity, siteImage, siteTitle } = this.props
+
+ return (
+ <div className="provider-approval-visual">
+ <section>
+ {siteImage ? (
+ <img
+ className="provider-approval-visual__identicon"
+ src={siteImage}
+ />
+ ) : (
+ <i className="provider-approval-visual__identicon--default">
+ {siteTitle.charAt(0).toUpperCase()}
+ </i>
+ )}
+ <h1>{siteTitle}</h1>
+ <h2>{origin}</h2>
+ </section>
+ <span className="provider-approval-visual__check" />
+ <section>
+ <Identicon
+ className="provider-approval-visual__identicon"
+ address={selectedIdentity.address}
+ diameter={64}
+ />
+ <h1>{selectedIdentity.name}</h1>
+ </section>
+ </div>
+ )
+ }
+
+ render () {
+ const { siteTitle } = this.props
+ const { t } = this.context
+
+ return (
+ <div className="provider-approval-container__content">
+ <section>
+ <h2>{t('connectRequest')}</h2>
+ {this.renderConnectVisual()}
+ <h1>{t('providerRequest', [siteTitle])}</h1>
+ <p>
+ {t('providerRequestInfo')}
+ <br/>
+ <a
+ href="https://medium.com/metamask/introducing-privacy-mode-42549d4870fa"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {t('learnMore')}.
+ </a>
+ </p>
+ </section>
+ <section className="secure-badge">
+ <img src="/images/mm-secure.svg" />
+ </section>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
new file mode 100644
index 000000000..4dbdddd16
--- /dev/null
+++ b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import ProviderPageContainerContent from './provider-page-container-content.component'
+import { getSelectedIdentity } from '../../../../selectors/selectors'
+
+const mapStateToProps = (state) => {
+ return {
+ selectedIdentity: getSelectedIdentity(state),
+ }
+}
+
+export default connect(mapStateToProps)(ProviderPageContainerContent)
diff --git a/ui/app/components/provider-page-container/provider-page-container-header/index.js b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js
index 430627d3a..430627d3a 100644
--- a/ui/app/components/provider-page-container/provider-page-container-header/index.js
+++ b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js
diff --git a/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js b/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
index 41bf6c3dd..41bf6c3dd 100644
--- a/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
+++ b/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js
new file mode 100644
index 000000000..910def2a3
--- /dev/null
+++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types'
+import React, {PureComponent} from 'react'
+import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.'
+import { PageContainerFooter } from '../../ui/page-container'
+
+export default class ProviderPageContainer extends PureComponent {
+ static propTypes = {
+ approveProviderRequest: PropTypes.func.isRequired,
+ origin: PropTypes.string.isRequired,
+ rejectProviderRequest: PropTypes.func.isRequired,
+ siteImage: PropTypes.string,
+ siteTitle: PropTypes.string.isRequired,
+ tabID: PropTypes.string.isRequired,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ };
+
+ componentDidMount () {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Auth',
+ action: 'Connect',
+ name: 'Popup Opened',
+ },
+ })
+ }
+
+ onCancel = () => {
+ const { tabID, rejectProviderRequest } = this.props
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Auth',
+ action: 'Connect',
+ name: 'Canceled',
+ },
+ })
+ rejectProviderRequest(tabID)
+ }
+
+ onSubmit = () => {
+ const { approveProviderRequest, tabID } = this.props
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Auth',
+ action: 'Connect',
+ name: 'Confirmed',
+ },
+ })
+ approveProviderRequest(tabID)
+ }
+
+ render () {
+ const {origin, siteImage, siteTitle} = this.props
+
+ return (
+ <div className="page-container provider-approval-container">
+ <ProviderPageContainerHeader />
+ <ProviderPageContainerContent
+ origin={origin}
+ siteImage={siteImage}
+ siteTitle={siteTitle}
+ />
+ <PageContainerFooter
+ onCancel={() => this.onCancel()}
+ cancelText={this.context.t('cancel')}
+ onSubmit={() => this.onSubmit()}
+ submitText={this.context.t('connect')}
+ submitButtonType="confirm"
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/selected-account/index.js b/ui/app/components/app/selected-account/index.js
index eb342181f..eb342181f 100644
--- a/ui/app/components/selected-account/index.js
+++ b/ui/app/components/app/selected-account/index.js
diff --git a/ui/app/components/selected-account/index.scss b/ui/app/components/app/selected-account/index.scss
index 5339a228b..5339a228b 100644
--- a/ui/app/components/selected-account/index.scss
+++ b/ui/app/components/app/selected-account/index.scss
diff --git a/ui/app/components/app/selected-account/selected-account.component.js b/ui/app/components/app/selected-account/selected-account.component.js
new file mode 100644
index 000000000..5a3fa815f
--- /dev/null
+++ b/ui/app/components/app/selected-account/selected-account.component.js
@@ -0,0 +1,55 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import copyToClipboard from 'copy-to-clipboard'
+import { addressSlicer, checksumAddress } from '../../../helpers/utils/util'
+
+const Tooltip = require('../../ui/tooltip-v2.js').default
+
+class SelectedAccount extends Component {
+ state = {
+ copied: false,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ selectedAddress: PropTypes.string,
+ selectedIdentity: PropTypes.object,
+ network: PropTypes.string,
+ }
+
+ render () {
+ const { t } = this.context
+ const { selectedAddress, selectedIdentity, network } = this.props
+ const checksummedAddress = checksumAddress(selectedAddress, network)
+
+ return (
+ <div className="selected-account">
+ <Tooltip
+ position="bottom"
+ title={this.state.copied ? t('copiedExclamation') : t('copyToClipboard')}
+ >
+ <div
+ className="selected-account__clickable"
+ onClick={() => {
+ this.setState({ copied: true })
+ setTimeout(() => this.setState({ copied: false }), 3000)
+ copyToClipboard(checksummedAddress)
+ }}
+ >
+ <div className="selected-account__name">
+ { selectedIdentity.name }
+ </div>
+ <div className="selected-account__address">
+ { addressSlicer(checksummedAddress) }
+ </div>
+ </div>
+ </Tooltip>
+ </div>
+ )
+ }
+}
+
+export default SelectedAccount
diff --git a/ui/app/components/app/selected-account/selected-account.container.js b/ui/app/components/app/selected-account/selected-account.container.js
new file mode 100644
index 000000000..b5dbe74f3
--- /dev/null
+++ b/ui/app/components/app/selected-account/selected-account.container.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux'
+import SelectedAccount from './selected-account.component'
+
+const selectors = require('../../../selectors/selectors')
+
+const mapStateToProps = state => {
+ return {
+ selectedAddress: selectors.getSelectedAddress(state),
+ selectedIdentity: selectors.getSelectedIdentity(state),
+ network: state.metamask.network,
+ }
+}
+
+export default connect(mapStateToProps)(SelectedAccount)
diff --git a/ui/app/components/selected-account/tests/selected-account-component.test.js b/ui/app/components/app/selected-account/tests/selected-account-component.test.js
index 78a94d1c8..78a94d1c8 100644
--- a/ui/app/components/selected-account/tests/selected-account-component.test.js
+++ b/ui/app/components/app/selected-account/tests/selected-account-component.test.js
diff --git a/ui/app/components/send/README.md b/ui/app/components/app/send/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/README.md
+++ b/ui/app/components/app/send/README.md
diff --git a/ui/app/components/send/account-list-item/account-list-item-README.md b/ui/app/components/app/send/account-list-item/account-list-item-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/account-list-item/account-list-item-README.md
+++ b/ui/app/components/app/send/account-list-item/account-list-item-README.md
diff --git a/ui/app/components/app/send/account-list-item/account-list-item.component.js b/ui/app/components/app/send/account-list-item/account-list-item.component.js
new file mode 100644
index 000000000..18e77b4f9
--- /dev/null
+++ b/ui/app/components/app/send/account-list-item/account-list-item.component.js
@@ -0,0 +1,108 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { checksumAddress } from '../../../../helpers/utils/util'
+import Identicon from '../../../ui/identicon'
+import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'
+import Tooltip from '../../../ui/tooltip-v2'
+
+export default class AccountListItem extends Component {
+
+ static propTypes = {
+ account: PropTypes.object,
+ className: PropTypes.string,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ displayAddress: PropTypes.bool,
+ displayBalance: PropTypes.bool,
+ handleClick: PropTypes.func,
+ icon: PropTypes.node,
+ balanceIsCached: PropTypes.bool,
+ showFiat: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ showFiat: true,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const {
+ account,
+ className,
+ displayAddress = false,
+ displayBalance = true,
+ handleClick,
+ icon = null,
+ balanceIsCached,
+ showFiat,
+ } = this.props
+
+ const { name, address, balance } = account || {}
+
+ return (<div
+ className={`account-list-item ${className}`}
+ onClick={() => handleClick && handleClick({ name, address, balance })}
+ >
+
+ <div className="account-list-item__top-row">
+ <Identicon
+ address={address}
+ className="account-list-item__identicon"
+ diameter={18}
+ />
+
+ <div className="account-list-item__account-name">{ name || address }</div>
+
+ {icon && <div className="account-list-item__icon">{ icon }</div>}
+
+ </div>
+
+ {displayAddress && name && <div className="account-list-item__account-address">
+ { checksumAddress(address) }
+ </div>}
+
+ {
+ displayBalance && (
+ <Tooltip
+ position="left"
+ title={this.context.t('balanceOutdated')}
+ disabled={!balanceIsCached}
+ style={{
+ left: '-20px !important',
+ }}
+ >
+ <div className={classnames('account-list-item__account-balances', {
+ 'account-list-item__cached-balances': balanceIsCached,
+ })}>
+ <div className="account-list-item__primary-cached-container">
+ <UserPreferencedCurrencyDisplay
+ type={PRIMARY}
+ value={balance}
+ hideTitle={true}
+ />
+ {
+ balanceIsCached ? <span className="account-list-item__cached-star">*</span> : null
+ }
+ </div>
+ {
+ showFiat && (
+ <UserPreferencedCurrencyDisplay
+ type={SECONDARY}
+ value={balance}
+ hideTitle={true}
+ />
+ )
+ }
+ </div>
+ </Tooltip>
+ )
+ }
+
+ </div>)
+ }
+}
diff --git a/ui/app/components/app/send/account-list-item/account-list-item.container.js b/ui/app/components/app/send/account-list-item/account-list-item.container.js
new file mode 100644
index 000000000..bc9a60f49
--- /dev/null
+++ b/ui/app/components/app/send/account-list-item/account-list-item.container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import {
+ getConversionRate,
+ getCurrentCurrency,
+ getNativeCurrency,
+} from '../send.selectors.js'
+import {
+ getIsMainnet,
+ isBalanceCached,
+ preferencesSelector,
+} from '../../../../selectors/selectors'
+import AccountListItem from './account-list-item.component'
+
+export default connect(mapStateToProps)(AccountListItem)
+
+function mapStateToProps (state) {
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+
+ return {
+ conversionRate: getConversionRate(state),
+ currentCurrency: getCurrentCurrency(state),
+ nativeCurrency: getNativeCurrency(state),
+ balanceIsCached: isBalanceCached(state),
+ showFiat: (isMainnet || !!showFiatInTestnets),
+ }
+}
diff --git a/ui/app/components/send/account-list-item/index.js b/ui/app/components/app/send/account-list-item/index.js
index 907485cf7..907485cf7 100644
--- a/ui/app/components/send/account-list-item/index.js
+++ b/ui/app/components/app/send/account-list-item/index.js
diff --git a/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js
new file mode 100644
index 000000000..5df9f77d6
--- /dev/null
+++ b/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js
@@ -0,0 +1,148 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import proxyquire from 'proxyquire'
+import Identicon from '../../../../ui/identicon'
+import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
+
+const utilsMethodStubs = {
+ checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
+}
+
+const AccountListItem = proxyquire('../account-list-item.component.js', {
+ '../../../../helpers/utils/util': utilsMethodStubs,
+}).default
+
+
+const propsMethodSpies = {
+ handleClick: sinon.spy(),
+}
+
+describe('AccountListItem Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<AccountListItem
+ account={ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' } }
+ className={'mockClassName'}
+ conversionRate={4}
+ currentCurrency={'mockCurrentyCurrency'}
+ nativeCurrency={'ETH'}
+ displayAddress={false}
+ displayBalance={false}
+ handleClick={propsMethodSpies.handleClick}
+ icon={<i className="mockIcon" />}
+ />, { context: { t: str => str + '_t' } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.handleClick.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a div with the passed className', () => {
+ assert.equal(wrapper.find('.mockClassName').length, 1)
+ assert(wrapper.find('.mockClassName').is('div'))
+ assert(wrapper.find('.mockClassName').hasClass('account-list-item'))
+ })
+
+ it('should call handleClick with the expected props when the root div is clicked', () => {
+ const { onClick } = wrapper.find('.mockClassName').props()
+ assert.equal(propsMethodSpies.handleClick.callCount, 0)
+ onClick()
+ assert.equal(propsMethodSpies.handleClick.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.handleClick.getCall(0).args,
+ [{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }]
+ )
+ })
+
+ it('should have a top row div', () => {
+ assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1)
+ assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div'))
+ })
+
+ it('should have an identicon, name and icon in the top row', () => {
+ const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
+ assert.equal(topRow.find(Identicon).length, 1)
+ assert.equal(topRow.find('.account-list-item__account-name').length, 1)
+ assert.equal(topRow.find('.account-list-item__icon').length, 1)
+ })
+
+ it('should show the account name if it exists', () => {
+ const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
+ assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName')
+ })
+
+ it('should show the account address if there is no name', () => {
+ wrapper.setProps({ account: { address: 'addressButNoName' } })
+ const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
+ assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName')
+ })
+
+ it('should render the passed icon', () => {
+ const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
+ assert(topRow.find('.account-list-item__icon').childAt(0).is('i'))
+ assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon'))
+ })
+
+ it('should not render an icon if none is passed', () => {
+ wrapper.setProps({ icon: null })
+ const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
+ assert.equal(topRow.find('.account-list-item__icon').length, 0)
+ })
+
+ it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => {
+ wrapper.setProps({ displayAddress: true })
+ assert.equal(wrapper.find('.account-list-item__account-address').length, 1)
+ assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress')
+ assert.deepEqual(
+ utilsMethodStubs.checksumAddress.getCall(0).args,
+ ['mockAddress']
+ )
+ })
+
+ it('should not render the account address as a checksumAddress if displayAddress is false', () => {
+ wrapper.setProps({ displayAddress: false })
+ assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
+ })
+
+ it('should not render the account address as a checksumAddress if name is not provided', () => {
+ wrapper.setProps({ account: { address: 'someAddressButNoName' } })
+ assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
+ })
+
+ it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => {
+ wrapper.setProps({ displayBalance: true })
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
+ assert.deepEqual(
+ wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(),
+ {
+ type: 'PRIMARY',
+ value: 'mockBalance',
+ hideTitle: true,
+ }
+ )
+ })
+
+ it('should only render one CurrencyDisplay if showFiat is false', () => {
+ wrapper.setProps({ showFiat: false, displayBalance: true })
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 1)
+ assert.deepEqual(
+ wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(),
+ {
+ type: 'PRIMARY',
+ value: 'mockBalance',
+ hideTitle: true,
+ }
+ )
+ })
+
+ it('should not render a CurrencyDisplay if displayBalance is false', () => {
+ wrapper.setProps({ displayBalance: false })
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0)
+ })
+
+ })
+})
diff --git a/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js
new file mode 100644
index 000000000..19a9a02d0
--- /dev/null
+++ b/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js
@@ -0,0 +1,73 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../account-list-item.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+ '../send.selectors.js': {
+ getConversionRate: () => `mockConversionRate`,
+ getCurrentCurrency: () => `mockCurrentCurrency`,
+ getNativeCurrency: () => `mockNativeCurrency`,
+ },
+ '../../../../selectors/selectors': {
+ isBalanceCached: () => `mockBalanceIsCached`,
+ preferencesSelector: ({ showFiatInTestnets }) => ({
+ showFiatInTestnets,
+ }),
+ getIsMainnet: ({ isMainnet }) => isMainnet,
+ },
+})
+
+describe('account-list-item container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: false }), {
+ conversionRate: 'mockConversionRate',
+ currentCurrency: 'mockCurrentCurrency',
+ nativeCurrency: 'mockNativeCurrency',
+ balanceIsCached: 'mockBalanceIsCached',
+ showFiat: true,
+ })
+ })
+
+ it('should map the correct properties to props when in mainnet and showFiatInTestnet is true', () => {
+ assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: true }), {
+ conversionRate: 'mockConversionRate',
+ currentCurrency: 'mockCurrentCurrency',
+ nativeCurrency: 'mockNativeCurrency',
+ balanceIsCached: 'mockBalanceIsCached',
+ showFiat: true,
+ })
+ })
+
+ it('should map the correct properties to props when not in mainnet and showFiatInTestnet is true', () => {
+ assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: true }), {
+ conversionRate: 'mockConversionRate',
+ currentCurrency: 'mockCurrentCurrency',
+ nativeCurrency: 'mockNativeCurrency',
+ balanceIsCached: 'mockBalanceIsCached',
+ showFiat: true,
+ })
+ })
+
+ it('should map the correct properties to props when not in mainnet and showFiatInTestnet is false', () => {
+ assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: false }), {
+ conversionRate: 'mockConversionRate',
+ currentCurrency: 'mockCurrentCurrency',
+ nativeCurrency: 'mockNativeCurrency',
+ balanceIsCached: 'mockBalanceIsCached',
+ showFiat: false,
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/index.js b/ui/app/components/app/send/index.js
index b5114babc..b5114babc 100644
--- a/ui/app/components/send/index.js
+++ b/ui/app/components/app/send/index.js
diff --git a/ui/app/components/send/send-content/index.js b/ui/app/components/app/send/send-content/index.js
index 891c17e6a..891c17e6a 100644
--- a/ui/app/components/send/send-content/index.js
+++ b/ui/app/components/app/send/send-content/index.js
diff --git a/ui/app/components/send/send-content/send-amount-row/README.md b/ui/app/components/app/send/send-content/send-amount-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-amount-row/README.md
+++ b/ui/app/components/app/send/send-content/send-amount-row/README.md
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index f17137c1e..f17137c1e 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
new file mode 100644
index 000000000..16c5a0db5
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
@@ -0,0 +1,40 @@
+import { connect } from 'react-redux'
+import {
+ getGasTotal,
+ getSelectedToken,
+ getSendFromBalance,
+ getTokenBalance,
+} from '../../../send.selectors.js'
+import { getMaxModeOn } from './amount-max-button.selectors.js'
+import { calcMaxAmount } from './amount-max-button.utils.js'
+import {
+ updateSendAmount,
+ setMaxModeTo,
+} from '../../../../../../store/actions'
+import AmountMaxButton from './amount-max-button.component'
+import {
+ updateSendErrors,
+} from '../../../../../../ducks/send/send.duck'
+
+export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
+
+function mapStateToProps (state) {
+
+ return {
+ balance: getSendFromBalance(state),
+ gasTotal: getGasTotal(state),
+ maxModeOn: getMaxModeOn(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setAmountToMax: maxAmountDataObject => {
+ dispatch(updateSendErrors({ amount: null }))
+ dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
+ },
+ setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
+ }
+}
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
index 69fec1994..69fec1994 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
new file mode 100644
index 000000000..f4c8fad8a
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
@@ -0,0 +1,29 @@
+const {
+ multiplyCurrencies,
+ subtractCurrencies,
+} = require('../../../../../../helpers/utils/conversion-util')
+const ethUtil = require('ethereumjs-util')
+
+function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) {
+ const { decimals } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+
+ return selectedToken
+ ? multiplyCurrencies(
+ tokenBalance,
+ multiplier,
+ {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ }
+ )
+ : subtractCurrencies(
+ ethUtil.addHexPrefix(balance),
+ ethUtil.addHexPrefix(gasTotal),
+ { toNumericBase: 'hex' }
+ )
+}
+
+module.exports = {
+ calcMaxAmount,
+}
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js
index ee8271494..ee8271494 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
index b04d3897f..b04d3897f 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
new file mode 100644
index 000000000..f446e330c
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
@@ -0,0 +1,91 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ setMaxModeTo: sinon.spy(),
+ updateSendAmount: sinon.spy(),
+}
+const duckActionSpies = {
+ updateSendErrors: sinon.spy(),
+}
+
+proxyquire('../amount-max-button.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../../send.selectors.js': {
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendFromBalance: (s) => `mockBalance:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ },
+ './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` },
+ './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
+ '../../../../../../store/actions': actionSpies,
+ '../../../../../../ducks/send/send.duck': duckActionSpies,
+})
+
+describe('amount-max-button container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ balance: 'mockBalance:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ maxModeOn: 'mockMaxModeOn:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('setAmountToMax()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' })
+ assert(dispatchSpy.calledTwice)
+ assert(duckActionSpies.updateSendErrors.calledOnce)
+ assert.deepEqual(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ { amount: null }
+ )
+ assert(actionSpies.updateSendAmount.calledOnce)
+ assert.equal(
+ actionSpies.updateSendAmount.getCall(0).args[0],
+ 12
+ )
+ })
+ })
+
+ describe('setMaxModeTo()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setMaxModeTo('mockVal')
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ actionSpies.setMaxModeTo.getCall(0).args[0],
+ 'mockVal'
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
index 655fe1969..655fe1969 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
index 1ee858f67..1ee858f67 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
diff --git a/ui/app/components/send/send-content/send-amount-row/index.js b/ui/app/components/app/send/send-content/send-amount-row/index.js
index abc6852fe..abc6852fe 100644
--- a/ui/app/components/send/send-content/send-amount-row/index.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/index.js
diff --git a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js
new file mode 100644
index 000000000..e725e7eda
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import AmountMaxButton from './amount-max-button'
+import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input'
+import UserPreferencedTokenInput from '../../../user-preferenced-token-input'
+
+export default class SendAmountRow extends Component {
+
+ static propTypes = {
+ amount: PropTypes.string,
+ amountConversionRate: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ balance: PropTypes.string,
+ conversionRate: PropTypes.number,
+ convertedCurrency: PropTypes.string,
+ gasTotal: PropTypes.string,
+ inError: PropTypes.bool,
+ primaryCurrency: PropTypes.string,
+ selectedToken: PropTypes.object,
+ setMaxModeTo: PropTypes.func,
+ tokenBalance: PropTypes.string,
+ updateGasFeeError: PropTypes.func,
+ updateSendAmount: PropTypes.func,
+ updateSendAmountError: PropTypes.func,
+ updateGas: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ validateAmount (amount) {
+ const {
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ updateGasFeeError,
+ updateSendAmountError,
+ } = this.props
+
+ updateSendAmountError({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+
+ if (selectedToken) {
+ updateGasFeeError({
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ }
+ }
+
+ updateAmount (amount) {
+ const { updateSendAmount, setMaxModeTo } = this.props
+
+ setMaxModeTo(false)
+ updateSendAmount(amount)
+ }
+
+ updateGas (amount) {
+ const { selectedToken, updateGas } = this.props
+
+ if (selectedToken) {
+ updateGas({ amount })
+ }
+ }
+
+ renderInput () {
+ const { amount, inError, selectedToken } = this.props
+ const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput
+
+ return (
+ <Component
+ onChange={newAmount => this.validateAmount(newAmount)}
+ onBlur={newAmount => {
+ this.updateGas(newAmount)
+ this.updateAmount(newAmount)
+ }}
+ error={inError}
+ value={amount}
+ />
+ )
+ }
+
+ render () {
+ const { gasTotal, inError } = this.props
+
+ return (
+ <SendRowWrapper
+ label={`${this.context.t('amount')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ {!inError && gasTotal && <AmountMaxButton />}
+ { this.renderInput() }
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js
new file mode 100644
index 000000000..0646355ab
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js
@@ -0,0 +1,54 @@
+import { connect } from 'react-redux'
+import {
+ getAmountConversionRate,
+ getConversionRate,
+ getCurrentCurrency,
+ getGasTotal,
+ getPrimaryCurrency,
+ getSelectedToken,
+ getSendAmount,
+ getSendFromBalance,
+ getTokenBalance,
+} from '../../send.selectors'
+import {
+ sendAmountIsInError,
+} from './send-amount-row.selectors'
+import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils'
+import {
+ setMaxModeTo,
+ updateSendAmount,
+} from '../../../../../store/actions'
+import {
+ updateSendErrors,
+} from '../../../../../ducks/send/send.duck'
+import SendAmountRow from './send-amount-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
+
+function mapStateToProps (state) {
+ return {
+ amount: getSendAmount(state),
+ amountConversionRate: getAmountConversionRate(state),
+ balance: getSendFromBalance(state),
+ conversionRate: getConversionRate(state),
+ convertedCurrency: getCurrentCurrency(state),
+ gasTotal: getGasTotal(state),
+ inError: sendAmountIsInError(state),
+ primaryCurrency: getPrimaryCurrency(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
+ updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
+ updateGasFeeError: (amountDataObject) => {
+ dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject)))
+ },
+ updateSendAmountError: (amountDataObject) => {
+ dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
+ },
+ }
+}
diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss
+++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss
diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js
index fb08c7ed7..fb08c7ed7 100644
--- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js
diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
index 14a71129f..14a71129f 100644
--- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
diff --git a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
new file mode 100644
index 000000000..6d20202b0
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
@@ -0,0 +1,125 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ setMaxModeTo: sinon.spy(),
+ updateSendAmount: sinon.spy(),
+}
+const duckActionSpies = {
+ updateSendErrors: sinon.spy(),
+}
+
+proxyquire('../send-amount-row.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../send.selectors': {
+ getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
+ getConversionRate: (s) => `mockConversionRate:${s}`,
+ getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendAmount: (s) => `mockAmount:${s}`,
+ getSendFromBalance: (s) => `mockBalance:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ },
+ './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
+ '../../send.utils': {
+ getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }),
+ getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }),
+ },
+ '../../../../../store/actions': actionSpies,
+ '../../../../../ducks/send/send.duck': duckActionSpies,
+})
+
+describe('send-amount-row container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ amount: 'mockAmount:mockState',
+ amountConversionRate: 'mockAmountConversionRate:mockState',
+ balance: 'mockBalance:mockState',
+ conversionRate: 'mockConversionRate:mockState',
+ convertedCurrency: 'mockConvertedCurrency:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ inError: 'mockInError:mockState',
+ primaryCurrency: 'mockPrimaryCurrency:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ duckActionSpies.updateSendErrors.resetHistory()
+ })
+
+ describe('setMaxModeTo()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setMaxModeTo('mockBool')
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.setMaxModeTo.calledOnce)
+ assert.equal(
+ actionSpies.setMaxModeTo.getCall(0).args[0],
+ 'mockBool'
+ )
+ })
+ })
+
+ describe('updateSendAmount()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendAmount('mockAmount')
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.updateSendAmount.calledOnce)
+ assert.equal(
+ actionSpies.updateSendAmount.getCall(0).args[0],
+ 'mockAmount'
+ )
+ })
+ })
+
+ describe('updateGasFeeError()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateGasFeeError({ some: 'data' })
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendErrors.calledOnce)
+ assert.deepEqual(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ { some: 'data', mockGasFeeErrorChange: true }
+ )
+ })
+ })
+
+ describe('updateSendAmountError()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendAmountError({ some: 'data' })
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendErrors.calledOnce)
+ assert.deepEqual(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ { some: 'data', mockChange: true }
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
index 4672cb8a7..4672cb8a7 100644
--- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
+++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
diff --git a/ui/app/components/app/send/send-content/send-content.component.js b/ui/app/components/app/send/send-content/send-content.component.js
new file mode 100644
index 000000000..2c09ceb19
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-content.component.js
@@ -0,0 +1,41 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerContent from '../../../ui/page-container/page-container-content.component'
+import SendAmountRow from './send-amount-row'
+import SendFromRow from './send-from-row'
+import SendGasRow from './send-gas-row'
+import SendHexDataRow from './send-hex-data-row'
+import SendToRow from './send-to-row'
+
+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={this.updateGas}
+ scanQrCode={ _ => this.props.scanQrCode()}
+ />
+ <SendAmountRow updateGas={this.updateGas} />
+ <SendGasRow />
+ {(this.props.showHexData && (
+ <SendHexDataRow
+ updateGas={this.updateGas}
+ />
+ ))}
+ </div>
+ </PageContainerContent>
+ )
+ }
+
+}
diff --git a/ui/app/components/send/send-content/send-dropdown-list/index.js b/ui/app/components/app/send/send-content/send-dropdown-list/index.js
index 04af6536c..04af6536c 100644
--- a/ui/app/components/send/send-content/send-dropdown-list/index.js
+++ b/ui/app/components/app/send/send-content/send-dropdown-list/index.js
diff --git a/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js
new file mode 100644
index 000000000..0d026bc69
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js
@@ -0,0 +1,52 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import AccountListItem from '../../account-list-item'
+
+export default class SendDropdownList extends Component {
+
+ static propTypes = {
+ accounts: PropTypes.array,
+ closeDropdown: PropTypes.func,
+ onSelect: PropTypes.func,
+ activeAddress: PropTypes.string,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ getListItemIcon (accountAddress, activeAddress) {
+ return accountAddress === activeAddress
+ ? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
+ : null
+ }
+
+ render () {
+ const {
+ accounts,
+ closeDropdown,
+ onSelect,
+ activeAddress,
+ } = this.props
+
+ return (<div>
+ <div
+ className="send-v2__from-dropdown__close-area"
+ onClick={() => closeDropdown()}
+ />
+ <div className="send-v2__from-dropdown__list">
+ {accounts.map((account, index) => <AccountListItem
+ account={account}
+ className="account-list-item__dropdown"
+ handleClick={() => {
+ onSelect(account)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account.address, activeAddress)}
+ key={`send-dropdown-account-#${index}`}
+ />)}
+ </div>
+ </div>)
+ }
+
+}
diff --git a/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
index b92dd4dfe..b92dd4dfe 100644
--- a/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
+++ b/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
diff --git a/ui/app/components/send/send-content/send-from-row/index.js b/ui/app/components/app/send/send-content/send-from-row/index.js
index 0a79726b2..0a79726b2 100644
--- a/ui/app/components/send/send-content/send-from-row/index.js
+++ b/ui/app/components/app/send/send-content/send-from-row/index.js
diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js
new file mode 100644
index 000000000..dfa53e970
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import AccountListItem from '../../account-list-item'
+
+export default class SendFromRow extends Component {
+ static propTypes = {
+ from: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { t } = this.context
+ const { from } = this.props
+
+ return (
+ <SendRowWrapper label={`${t('from')}:`}>
+ <div className="send-v2__from-dropdown">
+ <AccountListItem account={from} />
+ </div>
+ </SendRowWrapper>
+ )
+ }
+}
diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js
index fe3ac9aa1..fe3ac9aa1 100644
--- a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
+++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js
diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js
index 03ef4806b..03ef4806b 100644
--- a/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js
+++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js
diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js
index 18811c57e..18811c57e 100644
--- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
+++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js
diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js
index fd771ea77..fd771ea77 100644
--- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
+++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js
diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
index ecb57bbc3..ecb57bbc3 100644
--- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
+++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
diff --git a/ui/app/components/send/send-content/send-gas-row/README.md b/ui/app/components/app/send/send-content/send-gas-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-gas-row/README.md
+++ b/ui/app/components/app/send/send-content/send-gas-row/README.md
diff --git a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
new file mode 100644
index 000000000..48088607a
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
@@ -0,0 +1,57 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../../../../helpers/constants/common'
+
+export default class GasFeeDisplay extends Component {
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ primaryCurrency: PropTypes.string,
+ convertedCurrency: PropTypes.string,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ onReset: PropTypes.func,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { gasTotal, gasLoadingError, onReset } = this.props
+
+ return (
+ <div className="send-v2__gas-fee-display">
+ {gasTotal
+ ? (
+ <div className="currency-display">
+ <UserPreferencedCurrencyDisplay
+ value={gasTotal}
+ type={PRIMARY}
+ />
+ <UserPreferencedCurrencyDisplay
+ className="currency-display__converted-value"
+ value={gasTotal}
+ type={SECONDARY}
+ />
+ </div>
+ )
+ : gasLoadingError
+ ? <div className="currency-display.currency-display--message">
+ {this.context.t('setGasPrice')}
+ </div>
+ : <div className="currency-display">
+ {this.context.t('loading')}
+ </div>
+ }
+ <button
+ className="gas-fee-reset"
+ onClick={onReset}
+ >
+ { this.context.t('reset') }
+ </button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js
index dba0edb7b..dba0edb7b 100644
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
index cb4180508..cb4180508 100644
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
diff --git a/ui/app/components/send/send-content/send-gas-row/index.js b/ui/app/components/app/send/send-content/send-gas-row/index.js
index 3c7ff1d5f..3c7ff1d5f 100644
--- a/ui/app/components/send/send-content/send-gas-row/index.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/index.js
diff --git a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js
new file mode 100644
index 000000000..424a65b20
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js
@@ -0,0 +1,131 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
+import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group'
+import AdvancedGasInputs from '../../../gas-customization/advanced-gas-inputs'
+
+export default class SendGasRow extends Component {
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ convertedCurrency: PropTypes.string,
+ gasFeeError: PropTypes.bool,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ showCustomizeGasModal: PropTypes.func,
+ setGasPrice: PropTypes.func,
+ setGasLimit: PropTypes.func,
+ gasPriceButtonGroupProps: PropTypes.object,
+ gasButtonGroupShown: PropTypes.bool,
+ advancedInlineGasShown: PropTypes.bool,
+ resetGasButtons: PropTypes.func,
+ gasPrice: PropTypes.number,
+ gasLimit: PropTypes.number,
+ insufficientBalance: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ };
+
+ renderAdvancedOptionsButton () {
+ const { metricsEvent } = this.context
+ const { showCustomizeGasModal } = this.props
+ return <div className="advanced-gas-options-btn" onClick={() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Clicked "Advanced Options"',
+ },
+ })
+ showCustomizeGasModal()
+ }}>
+ { this.context.t('advancedOptions') }
+ </div>
+ }
+
+ renderContent () {
+ const {
+ conversionRate,
+ convertedCurrency,
+ gasLoadingError,
+ gasTotal,
+ showCustomizeGasModal,
+ gasPriceButtonGroupProps,
+ gasButtonGroupShown,
+ advancedInlineGasShown,
+ resetGasButtons,
+ setGasPrice,
+ setGasLimit,
+ gasPrice,
+ gasLimit,
+ insufficientBalance,
+ } = this.props
+ const { metricsEvent } = this.context
+
+ const gasPriceButtonGroup = <div>
+ <GasPriceButtonGroup
+ className="gas-price-button-group--small"
+ showCheck={false}
+ {...gasPriceButtonGroupProps}
+ handleGasPriceSelection={(...args) => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Changed Gas Button',
+ },
+ })
+ gasPriceButtonGroupProps.handleGasPriceSelection(...args)
+ }}
+ />
+ { this.renderAdvancedOptionsButton() }
+ </div>
+ const gasFeeDisplay = <GasFeeDisplay
+ conversionRate={conversionRate}
+ convertedCurrency={convertedCurrency}
+ gasLoadingError={gasLoadingError}
+ gasTotal={gasTotal}
+ onReset={resetGasButtons}
+ onClick={() => showCustomizeGasModal()}
+ />
+ const advancedGasInputs = <div>
+ <AdvancedGasInputs
+ updateCustomGasPrice={newGasPrice => setGasPrice(newGasPrice, gasLimit)}
+ updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)}
+ customGasPrice={gasPrice}
+ customGasLimit={gasLimit}
+ insufficientBalance={insufficientBalance}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ />
+ { this.renderAdvancedOptionsButton() }
+ </div>
+
+ if (advancedInlineGasShown) {
+ return advancedGasInputs
+ } else if (gasButtonGroupShown) {
+ return gasPriceButtonGroup
+ } else {
+ return gasFeeDisplay
+ }
+ }
+
+ render () {
+ const { gasFeeError } = this.props
+
+ return (
+ <SendRowWrapper
+ label={`${this.context.t('transactionFee')}:`}
+ showError={gasFeeError}
+ errorType={'gasFee'}
+ >
+ { this.renderContent() }
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js
new file mode 100644
index 000000000..f81670c02
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js
@@ -0,0 +1,118 @@
+import { connect } from 'react-redux'
+import {
+ getConversionRate,
+ getCurrentCurrency,
+ getGasTotal,
+ getGasPrice,
+ getGasLimit,
+ getSendAmount,
+} from '../../send.selectors.js'
+import {
+ isBalanceSufficient,
+ calcGasTotal,
+} from '../../send.utils.js'
+import {
+ getBasicGasEstimateLoadingStatus,
+ getRenderableEstimateDataForSmallButtonsFromGWEI,
+ getDefaultActiveButtonIndex,
+} from '../../../../../selectors/custom-gas'
+import {
+ showGasButtonGroup,
+} from '../../../../../ducks/send/send.duck'
+import {
+ resetCustomData,
+ setCustomGasPrice,
+ setCustomGasLimit,
+} from '../../../../../ducks/gas/gas.duck'
+import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
+import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../../store/actions'
+import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../../selectors/selectors'
+import SendGasRow from './send-gas-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
+
+function mapStateToProps (state) {
+ const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
+ const gasPrice = getGasPrice(state)
+ const gasLimit = getGasLimit(state)
+ const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice)
+
+ const gasTotal = getGasTotal(state)
+ const conversionRate = getConversionRate(state)
+ const balance = getCurrentEthBalance(state)
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount: getSelectedToken(state) ? '0x0' : getSendAmount(state),
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ return {
+ conversionRate,
+ convertedCurrency: getCurrentCurrency(state),
+ gasTotal,
+ gasFeeError: gasFeeIsInError(state),
+ gasLoadingError: getGasLoadingError(state),
+ gasPriceButtonGroupProps: {
+ buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
+ defaultActiveButtonIndex: 1,
+ newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null,
+ gasButtonInfo,
+ },
+ gasButtonGroupShown: getGasButtonGroupShown(state),
+ advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ gasPrice,
+ gasLimit,
+ insufficientBalance,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
+ setGasPrice: (newPrice, gasLimit) => {
+ dispatch(setGasPrice(newPrice))
+ dispatch(setCustomGasPrice(newPrice))
+ if (gasLimit) {
+ dispatch(setGasTotal(calcGasTotal(gasLimit, newPrice)))
+ }
+ },
+ setGasLimit: (newLimit, gasPrice) => {
+ dispatch(setGasLimit(newLimit))
+ dispatch(setCustomGasLimit(newLimit))
+ if (gasPrice) {
+ dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)))
+ }
+ },
+ showGasButtonGroup: () => dispatch(showGasButtonGroup()),
+ resetCustomData: () => dispatch(resetCustomData()),
+ }
+}
+
+function mergeProps (stateProps, dispatchProps, ownProps) {
+ const { gasPriceButtonGroupProps } = stateProps
+ const { gasButtonInfo } = gasPriceButtonGroupProps
+ const {
+ setGasPrice: dispatchSetGasPrice,
+ showGasButtonGroup: dispatchShowGasButtonGroup,
+ resetCustomData: dispatchResetCustomData,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ gasPriceButtonGroupProps: {
+ ...gasPriceButtonGroupProps,
+ handleGasPriceSelection: dispatchSetGasPrice,
+ },
+ resetGasButtons: () => {
+ dispatchResetCustomData()
+ dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei)
+ dispatchShowGasButtonGroup()
+ },
+ setGasPrice: dispatchSetGasPrice,
+ }
+}
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss
+++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js
index 79c838543..79c838543 100644
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
index 08f26854e..08f26854e 100644
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
diff --git a/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
new file mode 100644
index 000000000..d1f753639
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
@@ -0,0 +1,200 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+let mergeProps
+
+const actionSpies = {
+ showModal: sinon.spy(),
+ setGasPrice: sinon.spy(),
+ setGasTotal: sinon.spy(),
+ setGasLimit: sinon.spy(),
+}
+
+const sendDuckSpies = {
+ showGasButtonGroup: sinon.spy(),
+}
+
+const gasDuckSpies = {
+ resetCustomData: sinon.spy(),
+ setCustomGasPrice: sinon.spy(),
+ setCustomGasLimit: sinon.spy(),
+}
+
+proxyquire('../send-gas-row.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+ '../../../../../selectors/selectors': {
+ getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
+ getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
+ getSelectedToken: () => false,
+ },
+ '../../send.selectors.js': {
+ getConversionRate: (s) => `mockConversionRate:${s}`,
+ getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getSendAmount: (s) => `mockSendAmount:${s}`,
+ },
+ '../../send.utils.js': {
+ isBalanceSufficient: ({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ }) => `${amount}:${gasTotal}:${balance}:${conversionRate}`,
+ calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
+ },
+ './send-gas-row.selectors.js': {
+ getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
+ gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
+ getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`,
+ },
+ '../../../../../store/actions': actionSpies,
+ '../../../../../selectors/custom-gas': {
+ getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`,
+ getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`,
+ getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length,
+ },
+ '../../../../../ducks/send/send.duck': sendDuckSpies,
+ '../../../../../ducks/gas/gas.duck': gasDuckSpies,
+})
+
+describe('send-gas-row container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ conversionRate: 'mockConversionRate:mockState',
+ convertedCurrency: 'mockConvertedCurrency:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ gasFeeError: 'mockGasFeeError:mockState',
+ gasLoadingError: 'mockGasLoadingError:mockState',
+ gasPriceButtonGroupProps: {
+ buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`,
+ defaultActiveButtonIndex: 1,
+ newActiveButtonIndex: 49,
+ gasButtonInfo: `mockGasButtonInfo:mockState`,
+ },
+ gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`,
+ advancedInlineGasShown: 'mockAdvancedInlineGasShown:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ insufficientBalance: false,
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ actionSpies.setGasTotal.resetHistory()
+ })
+
+ describe('showCustomizeGasModal()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.showCustomizeGasModal()
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.showModal.getCall(0).args[0],
+ { name: 'CUSTOMIZE_GAS', hideBasic: true }
+ )
+ })
+ })
+
+ describe('setGasPrice()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit')
+ assert(dispatchSpy.calledThrice)
+ assert(actionSpies.setGasPrice.calledOnce)
+ assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice')
+ assert.equal(gasDuckSpies.setCustomGasPrice.getCall(0).args[0], 'mockNewPrice')
+ assert(actionSpies.setGasTotal.calledOnce)
+ assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockLimitmockNewPrice')
+ })
+ })
+
+ describe('setGasLimit()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice')
+ assert(dispatchSpy.calledThrice)
+ assert(actionSpies.setGasLimit.calledOnce)
+ assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'mockNewLimit')
+ assert.equal(gasDuckSpies.setCustomGasLimit.getCall(0).args[0], 'mockNewLimit')
+ assert(actionSpies.setGasTotal.calledOnce)
+ assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockNewLimitmockPrice')
+ })
+ })
+
+ describe('showGasButtonGroup()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.showGasButtonGroup()
+ assert(dispatchSpy.calledOnce)
+ assert(sendDuckSpies.showGasButtonGroup.calledOnce)
+ })
+ })
+
+ describe('resetCustomData()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.resetCustomData()
+ assert(dispatchSpy.calledOnce)
+ assert(gasDuckSpies.resetCustomData.calledOnce)
+ })
+ })
+
+ })
+
+ describe('mergeProps', () => {
+ let stateProps
+ let dispatchProps
+ let ownProps
+
+ beforeEach(() => {
+ stateProps = {
+ gasPriceButtonGroupProps: {
+ someGasPriceButtonGroupProp: 'foo',
+ anotherGasPriceButtonGroupProp: 'bar',
+ },
+ someOtherStateProp: 'baz',
+ }
+ dispatchProps = {
+ setGasPrice: sinon.spy(),
+ someOtherDispatchProp: sinon.spy(),
+ }
+ ownProps = { someOwnProp: 123 }
+ })
+
+ it('should return the expected props when isConfirm is true', () => {
+ const result = mergeProps(stateProps, dispatchProps, ownProps)
+
+ assert.equal(result.someOtherStateProp, 'baz')
+ assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
+ assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
+ assert.equal(result.someOwnProp, 123)
+
+ assert.equal(dispatchProps.setGasPrice.callCount, 0)
+ result.gasPriceButtonGroupProps.handleGasPriceSelection()
+ assert.equal(dispatchProps.setGasPrice.callCount, 1)
+
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
+ result.someOtherDispatchProp()
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
+ })
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
index bd3c9a257..bd3c9a257 100644
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
+++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/app/send/send-content/send-hex-data-row/index.js
index 08c341067..08c341067 100644
--- a/ui/app/components/send/send-content/send-hex-data-row/index.js
+++ b/ui/app/components/app/send/send-content/send-hex-data-row/index.js
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js
index 62a74a77b..62a74a77b 100644
--- a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
+++ b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js
diff --git a/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js
new file mode 100644
index 000000000..76c929d08
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import {
+ updateSendHexData,
+} from '../../../../../store/actions'
+import SendHexDataRow from './send-hex-data-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow)
+
+function mapStateToProps (state) {
+ return {
+ data: state.metamask.send.data,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateSendHexData (data) {
+ return dispatch(updateSendHexData(data))
+ },
+ }
+}
diff --git a/ui/app/components/send/send-content/send-row-wrapper/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/index.js
index d17545dcc..d17545dcc 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/index.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/index.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js
index c00617f83..c00617f83 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
index 61bc7bab7..61bc7bab7 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
index 59622047f..59622047f 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
index 2304a43d2..2304a43d2 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
index eecff165d..eecff165d 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js
index fd4d19ef7..fd4d19ef7 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/index.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
index f1caa8f99..f1caa8f99 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
index 7df14fd96..7df14fd96 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
index bd803d833..bd803d833 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
index 225bf056c..225bf056c 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md
diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js
new file mode 100644
index 000000000..94309bd96
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowErrorMessage from './send-row-error-message'
+import SendRowWarningMessage from './send-row-warning-message'
+
+export default class SendRowWrapper extends Component {
+
+ static propTypes = {
+ children: PropTypes.node,
+ errorType: PropTypes.string,
+ label: PropTypes.string,
+ showError: PropTypes.bool,
+ showWarning: PropTypes.bool,
+ warningType: PropTypes.string,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const {
+ children,
+ errorType = '',
+ label,
+ showError = false,
+ showWarning = false,
+ warningType = '',
+ } = this.props
+ const formField = Array.isArray(children) ? children[1] || children[0] : children
+ const customLabelContent = children.length > 1 ? children[0] : null
+
+ return (
+ <div className="send-v2__form-row">
+ <div className="send-v2__form-label">
+ {label}
+ {showError && <SendRowErrorMessage errorType={errorType}/>}
+ {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />}
+ {customLabelContent}
+ </div>
+ <div className="send-v2__form-field">
+ {formField}
+ </div>
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss
diff --git a/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
index 30280e1d0..30280e1d0 100644
--- a/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
diff --git a/ui/app/components/send/send-content/send-to-row/index.js b/ui/app/components/app/send/send-content/send-to-row/index.js
index 121f15148..121f15148 100644
--- a/ui/app/components/send/send-content/send-to-row/index.js
+++ b/ui/app/components/app/send/send-content/send-to-row/index.js
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row-README.md b/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row-README.md
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md
diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js
new file mode 100644
index 000000000..e8a55cb2a
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js
@@ -0,0 +1,91 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import EnsInput from '../../../ens-input'
+import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js'
+
+export default class SendToRow extends Component {
+
+ static propTypes = {
+ closeToDropdown: PropTypes.func,
+ hasHexData: PropTypes.bool.isRequired,
+ inError: PropTypes.bool,
+ inWarning: PropTypes.bool,
+ network: PropTypes.string,
+ openToDropdown: PropTypes.func,
+ selectedToken: PropTypes.object,
+ to: PropTypes.string,
+ toAccounts: PropTypes.array,
+ toDropdownOpen: PropTypes.bool,
+ tokens: PropTypes.array,
+ updateGas: PropTypes.func,
+ updateSendTo: PropTypes.func,
+ updateSendToError: PropTypes.func,
+ updateSendToWarning: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ handleToChange (to, nickname = '', toError, toWarning, network) {
+ const { hasHexData, updateSendTo, updateSendToError, updateGas, tokens, selectedToken, updateSendToWarning } = this.props
+ const toErrorObject = getToErrorObject(to, toError, hasHexData, tokens, selectedToken, network)
+ const toWarningObject = getToWarningObject(to, toWarning, tokens, selectedToken)
+ updateSendTo(to, nickname)
+ updateSendToError(toErrorObject)
+ updateSendToWarning(toWarningObject)
+ if (toErrorObject.to === null) {
+ updateGas({ to })
+ }
+ }
+
+ render () {
+ const {
+ closeToDropdown,
+ inError,
+ inWarning,
+ network,
+ openToDropdown,
+ to,
+ toAccounts,
+ toDropdownOpen,
+ } = this.props
+
+ return (
+ <SendRowWrapper
+ errorType={'to'}
+ label={`${this.context.t('to')}: `}
+ showError={inError}
+ showWarning={inWarning}
+ warningType={'to'}
+ >
+ <EnsInput
+ scanQrCode={_ => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Used QR scanner',
+ },
+ })
+ this.props.scanQrCode()
+ }}
+ accounts={toAccounts}
+ closeDropdown={() => closeToDropdown()}
+ dropdownOpen={toDropdownOpen}
+ inError={inError}
+ name={'address'}
+ network={network}
+ onChange={({ toAddress, nickname, toError, toWarning }) => this.handleToChange(toAddress, nickname, toError, toWarning, this.props.network)}
+ openDropdown={() => openToDropdown()}
+ placeholder={this.context.t('recipientAddress')}
+ to={to}
+ />
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js
new file mode 100644
index 000000000..30865d295
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js
@@ -0,0 +1,54 @@
+import { connect } from 'react-redux'
+import {
+ getCurrentNetwork,
+ getSelectedToken,
+ getSendTo,
+ getSendToAccounts,
+ getSendHexData,
+} from '../../send.selectors.js'
+import {
+ getToDropdownOpen,
+ getTokens,
+ sendToIsInError,
+ sendToIsInWarning,
+} from './send-to-row.selectors.js'
+import {
+ updateSendTo,
+} from '../../../../../store/actions'
+import {
+ updateSendErrors,
+ updateSendWarnings,
+ openToDropdown,
+ closeToDropdown,
+} from '../../../../../ducks/send/send.duck'
+import SendToRow from './send-to-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
+
+function mapStateToProps (state) {
+ return {
+ hasHexData: Boolean(getSendHexData(state)),
+ inError: sendToIsInError(state),
+ inWarning: sendToIsInWarning(state),
+ network: getCurrentNetwork(state),
+ selectedToken: getSelectedToken(state),
+ to: getSendTo(state),
+ toAccounts: getSendToAccounts(state),
+ toDropdownOpen: getToDropdownOpen(state),
+ tokens: getTokens(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ closeToDropdown: () => dispatch(closeToDropdown()),
+ openToDropdown: () => dispatch(openToDropdown()),
+ updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
+ updateSendToError: (toErrorObject) => {
+ dispatch(updateSendErrors(toErrorObject))
+ },
+ updateSendToWarning: (toWarningObject) => {
+ dispatch(updateSendWarnings(toWarningObject))
+ },
+ }
+}
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js
index a6160d335..a6160d335 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js
diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js
new file mode 100644
index 000000000..60e75d34c
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js
@@ -0,0 +1,36 @@
+const {
+ REQUIRED_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_RECIPIENT_ADDRESS_ERROR,
+ INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
+} = require('../../send.constants')
+const { isValidAddress, isEthNetwork } = require('../../../../../helpers/utils/util')
+import { checkExistingAddresses } from '../../../../../pages/add-token/util'
+
+const ethUtil = require('ethereumjs-util')
+const contractMap = require('eth-contract-metadata')
+
+function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) {
+ if (!to) {
+ if (!hasHexData) {
+ toError = REQUIRED_ERROR
+ }
+ } else if (!isValidAddress(to, network) && !toError) {
+ toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR
+ } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
+ toError = KNOWN_RECIPIENT_ADDRESS_ERROR
+ }
+ return { to: toError }
+}
+
+function getToWarningObject (to, toWarning = null, tokens = [], selectedToken = null) {
+ if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
+ toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR
+ }
+ return { to: toWarning }
+}
+
+module.exports = {
+ getToErrorObject,
+ getToWarningObject,
+}
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js
index d4d054057..d4d054057 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js
diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js
new file mode 100644
index 000000000..94b4f1024
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js
@@ -0,0 +1,134 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ updateSendTo: sinon.spy(),
+}
+const duckActionSpies = {
+ closeToDropdown: sinon.spy(),
+ openToDropdown: sinon.spy(),
+ updateSendErrors: sinon.spy(),
+ updateSendWarnings: sinon.spy(),
+}
+
+proxyquire('../send-to-row.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../send.selectors.js': {
+ getCurrentNetwork: (s) => `mockNetwork:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendHexData: (s) => s,
+ getSendTo: (s) => `mockTo:${s}`,
+ getSendToAccounts: (s) => `mockToAccounts:${s}`,
+ },
+ './send-to-row.selectors.js': {
+ getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`,
+ sendToIsInError: (s) => `mockInError:${s}`,
+ sendToIsInWarning: (s) => `mockInWarning:${s}`,
+ getTokens: (s) => `mockTokens:${s}`,
+ },
+ '../../../../../store/actions': actionSpies,
+ '../../../../../ducks/send/send.duck': duckActionSpies,
+})
+
+describe('send-to-row container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ hasHexData: true,
+ inError: 'mockInError:mockState',
+ inWarning: 'mockInWarning:mockState',
+ network: 'mockNetwork:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ to: 'mockTo:mockState',
+ toAccounts: 'mockToAccounts:mockState',
+ toDropdownOpen: 'mockToDropdownOpen:mockState',
+ tokens: 'mockTokens:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('closeToDropdown()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.closeToDropdown()
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.closeToDropdown.calledOnce)
+ assert.equal(
+ duckActionSpies.closeToDropdown.getCall(0).args[0],
+ undefined
+ )
+ })
+ })
+
+ describe('openToDropdown()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.openToDropdown()
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.openToDropdown.calledOnce)
+ assert.equal(
+ duckActionSpies.openToDropdown.getCall(0).args[0],
+ undefined
+ )
+ })
+ })
+
+ describe('updateSendTo()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname')
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.updateSendTo.calledOnce)
+ assert.deepEqual(
+ actionSpies.updateSendTo.getCall(0).args,
+ ['mockTo', 'mockNickname']
+ )
+ })
+ })
+
+ describe('updateSendToError()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendToError('mockToErrorObject')
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendErrors.calledOnce)
+ assert.equal(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ 'mockToErrorObject'
+ )
+ })
+ })
+
+ describe('updateSendToWarning()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendToWarning('mockToWarningObject')
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendWarnings.calledOnce)
+ assert.equal(
+ duckActionSpies.updateSendWarnings.getCall(0).args[0],
+ 'mockToWarningObject'
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
index 0fa342d1e..0fa342d1e 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js
new file mode 100644
index 000000000..95882d640
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js
@@ -0,0 +1,107 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+import {
+ REQUIRED_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_RECIPIENT_ADDRESS_ERROR,
+} from '../../../send.constants'
+
+const stubs = {
+ isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))),
+}
+
+const toRowUtils = proxyquire('../send-to-row.utils.js', {
+ '../../../../../helpers/utils/util': {
+ isValidAddress: stubs.isValidAddress,
+ },
+})
+const {
+ getToErrorObject,
+ getToWarningObject,
+} = toRowUtils
+
+describe('send-to-row utils', () => {
+
+ describe('getToErrorObject()', () => {
+ it('should return a required error if to is falsy', () => {
+ assert.deepEqual(getToErrorObject(null), {
+ to: REQUIRED_ERROR,
+ })
+ })
+
+ 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,
+ })
+ })
+
+ it('should return null if to is truthy and valid', () => {
+ assert.deepEqual(getToErrorObject('0xabc123'), {
+ to: null,
+ })
+ })
+
+ it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => {
+ assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), {
+ to: 'someExplicitError',
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of state tokens', () => {
+ assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+
+ it('should null if to is truthy part of tokens but selectedToken falsy', () => {
+ assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}]), {
+ to: null,
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of contract metadata', () => {
+ assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
+ assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ })
+
+ describe('getToWarningObject()', () => {
+ it('should return a known address recipient if to is truthy but part of state tokens', () => {
+ assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+
+ it('should null if to is truthy part of tokens but selectedToken falsy', () => {
+ assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}]), {
+ to: null,
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of contract metadata', () => {
+ assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
+ assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ })
+
+})
diff --git a/ui/app/components/app/send/send-content/tests/send-content-component.test.js b/ui/app/components/app/send/send-content/tests/send-content-component.test.js
new file mode 100644
index 000000000..7d102c930
--- /dev/null
+++ b/ui/app/components/app/send/send-content/tests/send-content-component.test.js
@@ -0,0 +1,50 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendContent from '../send-content.component.js'
+
+import PageContainerContent from '../../../../ui/page-container/page-container-content.component'
+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 showHexData={true} />)
+ })
+
+ describe('render', () => {
+ it('should render a PageContainerContent component', () => {
+ assert.equal(wrapper.find(PageContainerContent).length, 1)
+ })
+
+ it('should render a div with a .send-v2__form class as a child of PageContainerContent', () => {
+ const PageContainerContentChild = wrapper.find(PageContainerContent).children()
+ PageContainerContentChild.is('div')
+ PageContainerContentChild.is('.send-v2__form')
+ })
+
+ it('should render the correct row components as grandchildren of the PageContainerContent component', () => {
+ 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(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/README.md b/ui/app/components/app/send/send-footer/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-footer/README.md
+++ b/ui/app/components/app/send/send-footer/README.md
diff --git a/ui/app/components/send/send-footer/index.js b/ui/app/components/app/send/send-footer/index.js
index 58e91d622..58e91d622 100644
--- a/ui/app/components/send/send-footer/index.js
+++ b/ui/app/components/app/send/send-footer/index.js
diff --git a/ui/app/components/app/send/send-footer/send-footer.component.js b/ui/app/components/app/send/send-footer/send-footer.component.js
new file mode 100644
index 000000000..cc891a9b3
--- /dev/null
+++ b/ui/app/components/app/send/send-footer/send-footer.component.js
@@ -0,0 +1,137 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../../ui/page-container/page-container-footer'
+import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class SendFooter extends Component {
+
+ static propTypes = {
+ addToAddressBookIfNew: PropTypes.func,
+ amount: PropTypes.string,
+ data: PropTypes.string,
+ clearSend: PropTypes.func,
+ disabled: PropTypes.bool,
+ editingTransactionId: PropTypes.string,
+ errors: PropTypes.object,
+ from: PropTypes.object,
+ gasLimit: PropTypes.string,
+ gasPrice: PropTypes.string,
+ gasTotal: PropTypes.string,
+ history: PropTypes.object,
+ inError: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ sign: PropTypes.func,
+ to: PropTypes.string,
+ toAccounts: PropTypes.array,
+ tokenBalance: PropTypes.string,
+ unapprovedTxs: PropTypes.object,
+ update: PropTypes.func,
+ sendErrors: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ };
+
+ onCancel () {
+ this.props.clearSend()
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ onSubmit (event) {
+ event.preventDefault()
+ const {
+ addToAddressBookIfNew,
+ amount,
+ data,
+ editingTransactionId,
+ from: {address: from},
+ gasLimit: gas,
+ gasPrice,
+ selectedToken,
+ sign,
+ to,
+ unapprovedTxs,
+ // updateTx,
+ update,
+ toAccounts,
+ history,
+ } = this.props
+ const { metricsEvent } = this.context
+
+ // Should not be needed because submit should be disabled if there are errors.
+ // const noErrors = !amountError && toError === null
+
+ // if (!noErrors) {
+ // return
+ // }
+
+ // TODO: add nickname functionality
+ addToAddressBookIfNew(to, toAccounts)
+ const promise = editingTransactionId
+ ? update({
+ amount,
+ data,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+ : sign({ data, selectedToken, to, amount, from, gas, gasPrice })
+
+ Promise.resolve(promise)
+ .then(() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Complete',
+ },
+ })
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ })
+ }
+
+ formShouldBeDisabled () {
+ const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
+ const missingTokenBalance = selectedToken && !tokenBalance
+ const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to)
+ return shouldBeDisabled
+ }
+
+ componentDidUpdate (prevProps) {
+ const { inError, sendErrors } = this.props
+ const { metricsEvent } = this.context
+ if (!prevProps.inError && inError) {
+ const errorField = Object.keys(sendErrors).find(key => sendErrors[key])
+ const errorMessage = sendErrors[errorField]
+
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Error',
+ },
+ customVariables: {
+ errorField,
+ errorMessage,
+ },
+ })
+ }
+ }
+
+ render () {
+ return (
+ <PageContainerFooter
+ onCancel={() => this.onCancel()}
+ onSubmit={e => this.onSubmit(e)}
+ disabled={this.formShouldBeDisabled()}
+ />
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send-footer/send-footer.container.js b/ui/app/components/app/send/send-footer/send-footer.container.js
new file mode 100644
index 000000000..502159a81
--- /dev/null
+++ b/ui/app/components/app/send/send-footer/send-footer.container.js
@@ -0,0 +1,107 @@
+import { connect } from 'react-redux'
+import ethUtil from 'ethereumjs-util'
+import {
+ addToAddressBook,
+ clearSend,
+ signTokenTx,
+ signTx,
+ updateTransaction,
+} from '../../../../store/actions'
+import SendFooter from './send-footer.component'
+import {
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getSelectedToken,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendFromObject,
+ getSendTo,
+ getSendToAccounts,
+ getSendHexData,
+ getTokenBalance,
+ getUnapprovedTxs,
+ getSendErrors,
+} from '../send.selectors'
+import {
+ isSendFormInError,
+} from './send-footer.selectors'
+import {
+ addressIsNew,
+ constructTxParams,
+ constructUpdatedTx,
+} from './send-footer.utils'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
+
+function mapStateToProps (state) {
+ return {
+ amount: getSendAmount(state),
+ data: getSendHexData(state),
+ editingTransactionId: getSendEditingTransactionId(state),
+ from: getSendFromObject(state),
+ gasLimit: getGasLimit(state),
+ gasPrice: getGasPrice(state),
+ gasTotal: getGasTotal(state),
+ inError: isSendFormInError(state),
+ selectedToken: getSelectedToken(state),
+ to: getSendTo(state),
+ toAccounts: getSendToAccounts(state),
+ tokenBalance: getTokenBalance(state),
+ unapprovedTxs: getUnapprovedTxs(state),
+ sendErrors: getSendErrors(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ clearSend: () => dispatch(clearSend()),
+ sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => {
+ const txParams = constructTxParams({
+ amount,
+ data,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ })
+
+ selectedToken
+ ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams))
+ : dispatch(signTx(txParams))
+ },
+ update: ({
+ amount,
+ data,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ }) => {
+ const editingTx = constructUpdatedTx({
+ amount,
+ data,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+
+ return dispatch(updateTransaction(editingTx))
+ },
+ addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
+ const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
+ if (addressIsNew(toAccounts)) {
+ // TODO: nickname, i.e. addToAddressBook(recipient, nickname)
+ dispatch(addToAddressBook(hexPrefixedAddress, nickname))
+ }
+ },
+ }
+}
diff --git a/ui/app/components/send/send-footer/send-footer.scss b/ui/app/components/app/send/send-footer/send-footer.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-footer/send-footer.scss
+++ b/ui/app/components/app/send/send-footer/send-footer.scss
diff --git a/ui/app/components/send/send-footer/send-footer.selectors.js b/ui/app/components/app/send/send-footer/send-footer.selectors.js
index e20addfdc..e20addfdc 100644
--- a/ui/app/components/send/send-footer/send-footer.selectors.js
+++ b/ui/app/components/app/send/send-footer/send-footer.selectors.js
diff --git a/ui/app/components/send/send-footer/send-footer.utils.js b/ui/app/components/app/send/send-footer/send-footer.utils.js
index f82ff1e9b..f82ff1e9b 100644
--- a/ui/app/components/send/send-footer/send-footer.utils.js
+++ b/ui/app/components/app/send/send-footer/send-footer.utils.js
diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js
new file mode 100644
index 000000000..6683ca8c0
--- /dev/null
+++ b/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js
@@ -0,0 +1,233 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../../helpers/constants/routes'
+import SendFooter from '../send-footer.component.js'
+
+import PageContainerFooter from '../../../../ui/page-container/page-container-footer'
+
+const propsMethodSpies = {
+ addToAddressBookIfNew: sinon.spy(),
+ clearSend: sinon.spy(),
+ sign: sinon.spy(),
+ update: sinon.spy(),
+}
+const historySpies = {
+ push: sinon.spy(),
+}
+const MOCK_EVENT = { preventDefault: () => {} }
+
+sinon.spy(SendFooter.prototype, 'onCancel')
+sinon.spy(SendFooter.prototype, 'onSubmit')
+
+describe('SendFooter Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendFooter
+ addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
+ amount={'mockAmount'}
+ clearSend={propsMethodSpies.clearSend}
+ disabled={true}
+ editingTransactionId={'mockEditingTransactionId'}
+ errors={{}}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={historySpies}
+ inError={false}
+ selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
+ sign={propsMethodSpies.sign}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ tokenBalance={'mockTokenBalance'}
+ unapprovedTxs={['mockTx']}
+ update={propsMethodSpies.update}
+ sendErrors={{}}
+ />, { context: { t: str => str, metricsEvent: () => ({}) } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.clearSend.resetHistory()
+ propsMethodSpies.addToAddressBookIfNew.resetHistory()
+ propsMethodSpies.clearSend.resetHistory()
+ propsMethodSpies.sign.resetHistory()
+ propsMethodSpies.update.resetHistory()
+ historySpies.push.resetHistory()
+ SendFooter.prototype.onCancel.resetHistory()
+ SendFooter.prototype.onSubmit.resetHistory()
+ })
+
+ describe('onCancel', () => {
+ it('should call clearSend', () => {
+ assert.equal(propsMethodSpies.clearSend.callCount, 0)
+ wrapper.instance().onCancel()
+ assert.equal(propsMethodSpies.clearSend.callCount, 1)
+ })
+
+ it('should call history.push', () => {
+ assert.equal(historySpies.push.callCount, 0)
+ wrapper.instance().onCancel()
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE)
+ })
+ })
+
+
+ describe('formShouldBeDisabled()', () => {
+ const config = {
+ 'should return true if inError is truthy': {
+ inError: true,
+ expectedResult: true,
+ },
+ 'should return true if gasTotal is falsy': {
+ inError: false,
+ gasTotal: false,
+ expectedResult: true,
+ },
+ 'should return true if to is truthy': {
+ to: '0xsomevalidAddress',
+ inError: false,
+ gasTotal: false,
+ expectedResult: true,
+ },
+ 'should return true if selectedToken is truthy and tokenBalance is falsy': {
+ selectedToken: true,
+ tokenBalance: null,
+ expectedResult: true,
+ },
+ 'should return false if inError is false and all other params are truthy': {
+ inError: false,
+ gasTotal: '0x123',
+ selectedToken: true,
+ tokenBalance: 123,
+ expectedResult: false,
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ wrapper.setProps(obj)
+ assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult)
+ })
+ })
+ })
+
+ describe('onSubmit', () => {
+ it('should call addToAddressBookIfNew with the correct params', () => {
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.addToAddressBookIfNew.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.addToAddressBookIfNew.getCall(0).args,
+ ['mockTo', ['mockAccount']]
+ )
+ })
+
+ it('should call props.update if editingTransactionId is truthy', () => {
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.update.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.update.getCall(0).args[0],
+ {
+ data: undefined,
+ amount: 'mockAmount',
+ editingTransactionId: 'mockEditingTransactionId',
+ from: 'mockAddress',
+ gas: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ selectedToken: { mockProp: 'mockSelectedTokenProp' },
+ to: 'mockTo',
+ unapprovedTxs: ['mockTx'],
+ }
+ )
+ })
+
+ it('should not call props.sign if editingTransactionId is truthy', () => {
+ assert.equal(propsMethodSpies.sign.callCount, 0)
+ })
+
+ it('should call props.sign if editingTransactionId is falsy', () => {
+ wrapper.setProps({ editingTransactionId: null })
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.sign.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.sign.getCall(0).args[0],
+ {
+ data: undefined,
+ amount: 'mockAmount',
+ from: 'mockAddress',
+ gas: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ selectedToken: { mockProp: 'mockSelectedTokenProp' },
+ to: 'mockTo',
+ }
+ )
+ })
+
+ it('should not call props.update if editingTransactionId is falsy', () => {
+ assert.equal(propsMethodSpies.update.callCount, 0)
+ })
+
+ it('should call history.push', done => {
+ Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT))
+ .then(() => {
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ done()
+ })
+ })
+ })
+
+ describe('render', () => {
+ beforeEach(() => {
+ sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn')
+ wrapper = shallow(<SendFooter
+ addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
+ amount={'mockAmount'}
+ clearSend={propsMethodSpies.clearSend}
+ disabled={true}
+ editingTransactionId={'mockEditingTransactionId'}
+ errors={{}}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={historySpies}
+ inError={false}
+ selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
+ sign={propsMethodSpies.sign}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ tokenBalance={'mockTokenBalance'}
+ unapprovedTxs={['mockTx']}
+ update={propsMethodSpies.update}
+ />, { context: { t: str => str, metricsEvent: () => ({}) } })
+ })
+
+ afterEach(() => {
+ SendFooter.prototype.formShouldBeDisabled.restore()
+ })
+
+ it('should render a PageContainerFooter component', () => {
+ assert.equal(wrapper.find(PageContainerFooter).length, 1)
+ })
+
+ it('should pass the correct props to PageContainerFooter', () => {
+ const {
+ onCancel,
+ onSubmit,
+ disabled,
+ } = wrapper.find(PageContainerFooter).props()
+ assert.equal(disabled, 'formShouldBeDisabledReturn')
+
+ assert.equal(SendFooter.prototype.onSubmit.callCount, 0)
+ onSubmit(MOCK_EVENT)
+ assert.equal(SendFooter.prototype.onSubmit.callCount, 1)
+
+ assert.equal(SendFooter.prototype.onCancel.callCount, 0)
+ onCancel()
+ assert.equal(SendFooter.prototype.onCancel.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js
new file mode 100644
index 000000000..878b0aa19
--- /dev/null
+++ b/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js
@@ -0,0 +1,200 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ addToAddressBook: sinon.spy(),
+ clearSend: sinon.spy(),
+ signTokenTx: sinon.spy(),
+ signTx: sinon.spy(),
+ updateTransaction: sinon.spy(),
+}
+const utilsStubs = {
+ addressIsNew: sinon.stub().returns(true),
+ constructTxParams: sinon.stub().returns({
+ value: 'mockAmount',
+ }),
+ constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'),
+}
+
+proxyquire('../send-footer.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../../../store/actions': actionSpies,
+ '../send.selectors': {
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendAmount: (s) => `mockAmount:${s}`,
+ getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
+ getSendFromObject: (s) => `mockFromObject:${s}`,
+ getSendTo: (s) => `mockTo:${s}`,
+ getSendToAccounts: (s) => `mockToAccounts:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getSendHexData: (s) => `mockHexData:${s}`,
+ getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
+ getSendErrors: (s) => `mockSendErrors:${s}`,
+ },
+ './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
+ './send-footer.utils': utilsStubs,
+})
+
+describe('send-footer container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ amount: 'mockAmount:mockState',
+ data: 'mockHexData:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ editingTransactionId: 'mockEditingTransactionId:mockState',
+ from: 'mockFromObject:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ inError: 'mockInError:mockState',
+ to: 'mockTo:mockState',
+ toAccounts: 'mockToAccounts:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ unapprovedTxs: 'mockUnapprovedTxs:mockState',
+ sendErrors: 'mockSendErrors:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('clearSend()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.clearSend()
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.clearSend.calledOnce)
+ })
+ })
+
+ describe('sign()', () => {
+ it('should dispatch a signTokenTx action if selectedToken is defined', () => {
+ mapDispatchToPropsObject.sign({
+ selectedToken: {
+ address: '0xabc',
+ },
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructTxParams.getCall(0).args[0],
+ {
+ data: undefined,
+ selectedToken: {
+ address: '0xabc',
+ },
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }
+ )
+ assert.deepEqual(
+ actionSpies.signTokenTx.getCall(0).args,
+ [ '0xabc', 'mockTo', 'mockAmount', { value: 'mockAmount' } ]
+ )
+ })
+
+ it('should dispatch a sign action if selectedToken is not defined', () => {
+ utilsStubs.constructTxParams.resetHistory()
+ mapDispatchToPropsObject.sign({
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructTxParams.getCall(0).args[0],
+ {
+ data: undefined,
+ selectedToken: undefined,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }
+ )
+ assert.deepEqual(
+ actionSpies.signTx.getCall(0).args,
+ [ { value: 'mockAmount' } ]
+ )
+ })
+ })
+
+ describe('update()', () => {
+ it('should dispatch an updateTransaction action', () => {
+ mapDispatchToPropsObject.update({
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ editingTransactionId: 'mockEditingTransactionId',
+ selectedToken: 'mockSelectedToken',
+ unapprovedTxs: 'mockUnapprovedTxs',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructUpdatedTx.getCall(0).args[0],
+ {
+ data: undefined,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ editingTransactionId: 'mockEditingTransactionId',
+ selectedToken: 'mockSelectedToken',
+ unapprovedTxs: 'mockUnapprovedTxs',
+ }
+ )
+ assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams')
+ })
+ })
+
+ describe('addToAddressBookIfNew()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname')
+ assert(dispatchSpy.calledOnce)
+ assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts')
+ assert.deepEqual(
+ actionSpies.addToAddressBook.getCall(0).args,
+ [ '0xmockNewAddress', 'mockNickname' ]
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js
index 8de032f57..8de032f57 100644
--- a/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js
+++ b/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js
diff --git a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js
index 28ff0c891..28ff0c891 100644
--- a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js
+++ b/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js
diff --git a/ui/app/components/send/send-header/README.md b/ui/app/components/app/send/send-header/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send-header/README.md
+++ b/ui/app/components/app/send/send-header/README.md
diff --git a/ui/app/components/send/send-header/index.js b/ui/app/components/app/send/send-header/index.js
index 0b17f0b7d..0b17f0b7d 100644
--- a/ui/app/components/send/send-header/index.js
+++ b/ui/app/components/app/send/send-header/index.js
diff --git a/ui/app/components/app/send/send-header/send-header.component.js b/ui/app/components/app/send/send-header/send-header.component.js
new file mode 100644
index 000000000..f216954ef
--- /dev/null
+++ b/ui/app/components/app/send/send-header/send-header.component.js
@@ -0,0 +1,34 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerHeader from '../../../ui/page-container/page-container-header'
+import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class SendHeader extends Component {
+
+ static propTypes = {
+ clearSend: PropTypes.func,
+ history: PropTypes.object,
+ titleKey: PropTypes.string,
+ subtitleParams: PropTypes.array,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ onClose () {
+ this.props.clearSend()
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ render () {
+ return (
+ <PageContainerHeader
+ onClose={() => this.onClose()}
+ subtitle={this.context.t(...this.props.subtitleParams)}
+ title={this.context.t(this.props.titleKey)}
+ />
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send-header/send-header.container.js b/ui/app/components/app/send/send-header/send-header.container.js
new file mode 100644
index 000000000..ce53fba9a
--- /dev/null
+++ b/ui/app/components/app/send/send-header/send-header.container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { clearSend } from '../../../../store/actions'
+import SendHeader from './send-header.component'
+import { getSubtitleParams, getTitleKey } from './send-header.selectors'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendHeader)
+
+function mapStateToProps (state) {
+ return {
+ titleKey: getTitleKey(state),
+ subtitleParams: getSubtitleParams(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ clearSend: () => dispatch(clearSend()),
+ }
+}
diff --git a/ui/app/components/send/send-header/send-header.selectors.js b/ui/app/components/app/send/send-header/send-header.selectors.js
index d7c9d3766..d7c9d3766 100644
--- a/ui/app/components/send/send-header/send-header.selectors.js
+++ b/ui/app/components/app/send/send-header/send-header.selectors.js
diff --git a/ui/app/components/app/send/send-header/tests/send-header-component.test.js b/ui/app/components/app/send/send-header/tests/send-header-component.test.js
new file mode 100644
index 000000000..db2ee8967
--- /dev/null
+++ b/ui/app/components/app/send/send-header/tests/send-header-component.test.js
@@ -0,0 +1,70 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import { DEFAULT_ROUTE } from '../../../../../helpers/constants/routes'
+import SendHeader from '../send-header.component.js'
+
+import PageContainerHeader from '../../../../ui/page-container/page-container-header'
+
+const propsMethodSpies = {
+ clearSend: sinon.spy(),
+}
+const historySpies = {
+ push: sinon.spy(),
+}
+
+sinon.spy(SendHeader.prototype, 'onClose')
+
+describe('SendHeader Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendHeader
+ clearSend={propsMethodSpies.clearSend}
+ history={historySpies}
+ titleKey={'mockTitleKey'}
+ subtitleParams={[ 'mockSubtitleKey', 'mockVal']}
+ />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.clearSend.resetHistory()
+ historySpies.push.resetHistory()
+ SendHeader.prototype.onClose.resetHistory()
+ })
+
+ describe('onClose', () => {
+ it('should call clearSend', () => {
+ assert.equal(propsMethodSpies.clearSend.callCount, 0)
+ wrapper.instance().onClose()
+ assert.equal(propsMethodSpies.clearSend.callCount, 1)
+ })
+
+ it('should call history.push', () => {
+ assert.equal(historySpies.push.callCount, 0)
+ wrapper.instance().onClose()
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a PageContainerHeader compenent', () => {
+ assert.equal(wrapper.find(PageContainerHeader).length, 1)
+ })
+
+ it('should pass the correct props to PageContainerHeader', () => {
+ const {
+ onClose,
+ subtitle,
+ title,
+ } = wrapper.find(PageContainerHeader).props()
+ assert.equal(subtitle, 'mockSubtitleKeymockVal')
+ assert.equal(title, 'mockTitleKey')
+ assert.equal(SendHeader.prototype.onClose.callCount, 0)
+ onClose()
+ assert.equal(SendHeader.prototype.onClose.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/components/app/send/send-header/tests/send-header-container.test.js b/ui/app/components/app/send/send-header/tests/send-header-container.test.js
new file mode 100644
index 000000000..634c3424b
--- /dev/null
+++ b/ui/app/components/app/send/send-header/tests/send-header-container.test.js
@@ -0,0 +1,59 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ clearSend: sinon.spy(),
+}
+
+proxyquire('../send-header.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../../../store/actions': actionSpies,
+ './send-header.selectors': {
+ getTitleKey: (s) => `mockTitleKey:${s}`,
+ getSubtitleParams: (s) => `mockSubtitleParams:${s}`,
+ },
+})
+
+describe('send-header container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ titleKey: 'mockTitleKey:mockState',
+ subtitleParams: 'mockSubtitleParams:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('clearSend()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.clearSend()
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.clearSend.calledOnce)
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/send-header/tests/send-header-selectors.test.js b/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js
index e0c6a3ab3..e0c6a3ab3 100644
--- a/ui/app/components/send/send-header/tests/send-header-selectors.test.js
+++ b/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js
diff --git a/ui/app/components/app/send/send.component.js b/ui/app/components/app/send/send.component.js
new file mode 100644
index 000000000..a38b681b0
--- /dev/null
+++ b/ui/app/components/app/send/send.component.js
@@ -0,0 +1,218 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import PersistentForm from '../../../../lib/persistent-form'
+import {
+ getAmountErrorObject,
+ getGasFeeErrorObject,
+ getToAddressForGasUpdate,
+ doesAmountErrorRequireUpdate,
+} from './send.utils'
+
+import SendHeader from './send-header'
+import SendContent from './send-content'
+import SendFooter from './send-footer'
+
+export default class SendTransactionScreen extends PersistentForm {
+
+ static propTypes = {
+ amount: PropTypes.string,
+ amountConversionRate: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ blockGasLimit: PropTypes.string,
+ conversionRate: PropTypes.number,
+ editingTransactionId: PropTypes.string,
+ from: PropTypes.object,
+ gasLimit: PropTypes.string,
+ gasPrice: PropTypes.string,
+ gasTotal: PropTypes.string,
+ history: PropTypes.object,
+ network: PropTypes.string,
+ primaryCurrency: PropTypes.string,
+ recentBlocks: PropTypes.array,
+ selectedAddress: PropTypes.string,
+ selectedToken: PropTypes.object,
+ tokenBalance: PropTypes.string,
+ tokenContract: PropTypes.object,
+ fetchBasicGasEstimates: PropTypes.func,
+ updateAndSetGasTotal: PropTypes.func,
+ updateSendErrors: PropTypes.func,
+ updateSendTokenBalance: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ qrCodeDetected: PropTypes.func,
+ qrCodeData: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.qrCodeData) {
+ if (nextProps.qrCodeData.type === 'address') {
+ const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase()
+ const currentAddress = this.props.to && this.props.to.toLowerCase()
+ if (currentAddress !== scannedAddress) {
+ this.props.updateSendTo(scannedAddress)
+ this.updateGas({ to: scannedAddress })
+ // Clean up QR code data after handling
+ this.props.qrCodeDetected(null)
+ }
+ }
+ }
+ }
+
+ updateGas ({ to: updatedToAddress, amount: value, data } = {}) {
+ const {
+ amount,
+ blockGasLimit,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ recentBlocks,
+ selectedAddress,
+ selectedToken = {},
+ to: currentToAddress,
+ updateAndSetGasLimit,
+ } = this.props
+
+ updateAndSetGasLimit({
+ blockGasLimit,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ recentBlocks,
+ selectedAddress,
+ selectedToken,
+ to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
+ value: value || amount,
+ data,
+ })
+ }
+
+ componentDidUpdate (prevProps) {
+ const {
+ amount,
+ amountConversionRate,
+ conversionRate,
+ from: { address, balance },
+ gasTotal,
+ network,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ updateSendErrors,
+ updateSendTokenBalance,
+ tokenContract,
+ } = this.props
+
+ const {
+ from: { balance: prevBalance },
+ gasTotal: prevGasTotal,
+ tokenBalance: prevTokenBalance,
+ network: prevNetwork,
+ } = prevProps
+
+ const uninitialized = [prevBalance, prevGasTotal].every(n => n === null)
+
+ const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({
+ balance,
+ gasTotal,
+ prevBalance,
+ prevGasTotal,
+ prevTokenBalance,
+ selectedToken,
+ tokenBalance,
+ })
+
+ if (amountErrorRequiresUpdate) {
+ const amountErrorObject = getAmountErrorObject({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ const gasFeeErrorObject = selectedToken
+ ? getGasFeeErrorObject({
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ })
+ : { gasFee: null }
+ updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject))
+ }
+
+ if (!uninitialized) {
+
+ if (network !== prevNetwork && network !== 'loading') {
+ updateSendTokenBalance({
+ selectedToken,
+ tokenContract,
+ address,
+ })
+ this.updateGas()
+ }
+ }
+ }
+
+ componentDidMount () {
+ this.props.fetchBasicGasEstimates()
+ .then(() => {
+ this.updateGas()
+ })
+ }
+
+ componentWillMount () {
+ const {
+ from: { address },
+ selectedToken,
+ tokenContract,
+ updateSendTokenBalance,
+ } = this.props
+
+ updateSendTokenBalance({
+ selectedToken,
+ tokenContract,
+ address,
+ })
+
+ // Show QR Scanner modal if ?scan=true
+ if (window.location.search === '?scan=true') {
+ this.props.scanQrCode()
+
+ // Clear the queryString param after showing the modal
+ const cleanUrl = location.href.split('?')[0]
+ history.pushState({}, null, `${cleanUrl}`)
+ window.location.hash = '#send'
+ }
+ }
+
+ componentWillUnmount () {
+ this.props.resetSendState()
+ }
+
+ render () {
+ const { history, showHexData } = this.props
+
+ return (
+ <div className="page-container">
+ <SendHeader history={history}/>
+ <SendContent
+ updateGas={(updateData) => this.updateGas(updateData)}
+ scanQrCode={_ => this.props.scanQrCode()}
+ showHexData={showHexData}
+ />
+ <SendFooter history={history}/>
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/app/send/send.constants.js b/ui/app/components/app/send/send.constants.js
new file mode 100644
index 000000000..36549038e
--- /dev/null
+++ b/ui/app/components/app/send/send.constants.js
@@ -0,0 +1,61 @@
+const ethUtil = require('ethereumjs-util')
+const { conversionUtil, multiplyCurrencies } = require('../../../helpers/utils/conversion-util')
+
+const MIN_GAS_PRICE_DEC = '0'
+const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16)
+const MIN_GAS_LIMIT_DEC = '21000'
+const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
+
+const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, {
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ fromNumericBase: 'hex',
+ toNumericBase: 'hex',
+ numberOfDecimals: 1,
+}))
+
+const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+})
+
+const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb'
+
+const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds'
+const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens'
+const NEGATIVE_ETH_ERROR = 'negativeETH'
+const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient'
+const INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR = 'invalidAddressRecipientNotEthNetwork'
+const REQUIRED_ERROR = 'required'
+const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient'
+
+const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ fromNumericBase: 'hex',
+ toNumericBase: 'hex',
+}))
+
+const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
+const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
+
+module.exports = {
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_RECIPIENT_ADDRESS_ERROR,
+ INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
+ MIN_GAS_LIMIT_DEC,
+ MIN_GAS_LIMIT_HEX,
+ MIN_GAS_PRICE_DEC,
+ MIN_GAS_PRICE_GWEI,
+ MIN_GAS_PRICE_HEX,
+ MIN_GAS_TOTAL,
+ NEGATIVE_ETH_ERROR,
+ ONE_GWEI_IN_WEI_HEX,
+ REQUIRED_ERROR,
+ SIMPLE_GAS_COST,
+ TOKEN_TRANSFER_FUNCTION_SIGNATURE,
+ BASE_TOKEN_GAS_COST,
+}
diff --git a/ui/app/components/app/send/send.container.js b/ui/app/components/app/send/send.container.js
new file mode 100644
index 000000000..e65463b93
--- /dev/null
+++ b/ui/app/components/app/send/send.container.js
@@ -0,0 +1,112 @@
+import { connect } from 'react-redux'
+import SendEther from './send.component'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import {
+ getAmountConversionRate,
+ getBlockGasLimit,
+ getConversionRate,
+ getCurrentNetwork,
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getPrimaryCurrency,
+ getRecentBlocks,
+ getSelectedAddress,
+ getSelectedToken,
+ getSelectedTokenContract,
+ getSelectedTokenToFiatRate,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendHexDataFeatureFlagState,
+ getSendFromObject,
+ getSendTo,
+ getTokenBalance,
+ getQrCodeData,
+} from './send.selectors'
+import {
+ updateSendTo,
+ updateSendTokenBalance,
+ updateGasData,
+ setGasTotal,
+ showQrScanner,
+ qrCodeDetected,
+} from '../../../store/actions'
+import {
+ resetSendState,
+ updateSendErrors,
+} from '../../../ducks/send/send.duck'
+import {
+ fetchBasicGasEstimates,
+} from '../../../ducks/gas/gas.duck'
+import {
+ calcGasTotal,
+} from './send.utils.js'
+
+import {
+ SEND_ROUTE,
+} from '../../../helpers/constants/routes'
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SendEther)
+
+function mapStateToProps (state) {
+ return {
+ amount: getSendAmount(state),
+ amountConversionRate: getAmountConversionRate(state),
+ blockGasLimit: getBlockGasLimit(state),
+ conversionRate: getConversionRate(state),
+ editingTransactionId: getSendEditingTransactionId(state),
+ from: getSendFromObject(state),
+ gasLimit: getGasLimit(state),
+ gasPrice: getGasPrice(state),
+ gasTotal: getGasTotal(state),
+ network: getCurrentNetwork(state),
+ primaryCurrency: getPrimaryCurrency(state),
+ recentBlocks: getRecentBlocks(state),
+ selectedAddress: getSelectedAddress(state),
+ selectedToken: getSelectedToken(state),
+ showHexData: getSendHexDataFeatureFlagState(state),
+ to: getSendTo(state),
+ tokenBalance: getTokenBalance(state),
+ tokenContract: getSelectedTokenContract(state),
+ tokenToFiatRate: getSelectedTokenToFiatRate(state),
+ qrCodeData: getQrCodeData(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateAndSetGasLimit: ({
+ blockGasLimit,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ recentBlocks,
+ selectedAddress,
+ selectedToken,
+ to,
+ value,
+ data,
+ }) => {
+ !editingTransactionId
+ ? dispatch(updateGasData({ gasPrice, recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data }))
+ : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
+ },
+ updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {
+ dispatch(updateSendTokenBalance({
+ selectedToken,
+ tokenContract,
+ address,
+ }))
+ },
+ updateSendErrors: newError => dispatch(updateSendErrors(newError)),
+ resetSendState: () => dispatch(resetSendState()),
+ scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
+ qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
+ updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
+ fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
+ }
+}
diff --git a/ui/app/components/send/send.scss b/ui/app/components/app/send/send.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/send.scss
+++ b/ui/app/components/app/send/send.scss
diff --git a/ui/app/components/app/send/send.selectors.js b/ui/app/components/app/send/send.selectors.js
new file mode 100644
index 000000000..2ec677ad1
--- /dev/null
+++ b/ui/app/components/app/send/send.selectors.js
@@ -0,0 +1,291 @@
+const { valuesFor } = require('../../../helpers/utils/util')
+const abi = require('human-standard-token-abi')
+const {
+ multiplyCurrencies,
+} = require('../../../helpers/utils/conversion-util')
+const {
+ getMetaMaskAccounts,
+} = require('../../../selectors/selectors')
+const {
+ estimateGasPriceFromRecentBlocks,
+ calcGasTotal,
+} = require('./send.utils')
+import {
+ getFastPriceEstimateInHexWEI,
+} from '../../../selectors/custom-gas'
+
+const selectors = {
+ accountsWithSendEtherInfoSelector,
+ getAddressBook,
+ getAmountConversionRate,
+ getBlockGasLimit,
+ getConversionRate,
+ getCurrentAccountWithSendEtherInfo,
+ getCurrentCurrency,
+ getCurrentNetwork,
+ getCurrentViewContext,
+ getForceGasMin,
+ getNativeCurrency,
+ getGasLimit,
+ getGasPrice,
+ getGasPriceFromRecentBlocks,
+ getGasTotal,
+ getPrimaryCurrency,
+ getRecentBlocks,
+ getSelectedAccount,
+ getSelectedAddress,
+ getSelectedIdentity,
+ getSelectedToken,
+ getSelectedTokenContract,
+ getSelectedTokenExchangeRate,
+ getSelectedTokenToFiatRate,
+ getSendAmount,
+ getSendHexData,
+ getSendHexDataFeatureFlagState,
+ getSendEditingTransactionId,
+ getSendErrors,
+ getSendFrom,
+ getSendFromBalance,
+ getSendFromObject,
+ getSendMaxModeState,
+ getSendTo,
+ getSendToAccounts,
+ getSendWarnings,
+ getTokenBalance,
+ getTokenExchangeRate,
+ getUnapprovedTxs,
+ transactionsSelector,
+ getQrCodeData,
+}
+
+module.exports = selectors
+
+function accountsWithSendEtherInfoSelector (state) {
+ const accounts = getMetaMaskAccounts(state)
+ const { identities } = state.metamask
+
+ const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
+ return Object.assign({}, account, identities[key])
+ })
+
+ return accountsWithSendEtherInfo
+}
+
+function getAddressBook (state) {
+ return state.metamask.addressBook
+}
+
+function getAmountConversionRate (state) {
+ return getSelectedToken(state)
+ ? getSelectedTokenToFiatRate(state)
+ : getConversionRate(state)
+}
+
+function getBlockGasLimit (state) {
+ return state.metamask.currentBlockGasLimit
+}
+
+function getConversionRate (state) {
+ return state.metamask.conversionRate
+}
+
+function getCurrentAccountWithSendEtherInfo (state) {
+ const currentAddress = getSelectedAddress(state)
+ const accounts = accountsWithSendEtherInfoSelector(state)
+
+ return accounts.find(({ address }) => address === currentAddress)
+}
+
+function getCurrentCurrency (state) {
+ return state.metamask.currentCurrency
+}
+
+function getNativeCurrency (state) {
+ return state.metamask.nativeCurrency
+}
+
+function getCurrentNetwork (state) {
+ return state.metamask.network
+}
+
+function getCurrentViewContext (state) {
+ const { currentView = {} } = state.appState
+ return currentView.context
+}
+
+function getForceGasMin (state) {
+ return state.metamask.send.forceGasMin
+}
+
+function getGasLimit (state) {
+ return state.metamask.send.gasLimit || '0'
+}
+
+function getGasPrice (state) {
+ return state.metamask.send.gasPrice || getFastPriceEstimateInHexWEI(state)
+}
+
+function getGasPriceFromRecentBlocks (state) {
+ return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks)
+}
+
+function getGasTotal (state) {
+ return calcGasTotal(getGasLimit(state), getGasPrice(state))
+}
+
+function getPrimaryCurrency (state) {
+ const selectedToken = getSelectedToken(state)
+ return selectedToken && selectedToken.symbol
+}
+
+function getRecentBlocks (state) {
+ return state.metamask.recentBlocks
+}
+
+function getSelectedAccount (state) {
+ const accounts = getMetaMaskAccounts(state)
+ const selectedAddress = getSelectedAddress(state)
+
+ return accounts[selectedAddress]
+}
+
+function getSelectedAddress (state) {
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
+
+ return selectedAddress
+}
+
+function getSelectedIdentity (state) {
+ const selectedAddress = getSelectedAddress(state)
+ const identities = state.metamask.identities
+
+ return identities[selectedAddress]
+}
+
+function getSelectedToken (state) {
+ const tokens = state.metamask.tokens || []
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
+ const sendToken = state.metamask.send.token
+
+ return selectedToken || sendToken || null
+}
+
+function getSelectedTokenContract (state) {
+ const selectedToken = getSelectedToken(state)
+
+ return selectedToken
+ ? global.eth.contract(abi).at(selectedToken.address)
+ : null
+}
+
+function getSelectedTokenExchangeRate (state) {
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const selectedToken = getSelectedToken(state) || {}
+ const { symbol = '' } = selectedToken
+ const pair = `${symbol.toLowerCase()}_eth`
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
+function getSelectedTokenToFiatRate (state) {
+ const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
+ const conversionRate = getConversionRate(state)
+
+ const tokenToFiatRate = multiplyCurrencies(
+ conversionRate,
+ selectedTokenExchangeRate,
+ { toNumericBase: 'dec' }
+ )
+
+ return tokenToFiatRate
+}
+
+function getSendAmount (state) {
+ return state.metamask.send.amount
+}
+
+function getSendHexData (state) {
+ return state.metamask.send.data
+}
+
+function getSendHexDataFeatureFlagState (state) {
+ return state.metamask.featureFlags.sendHexData
+}
+
+function getSendEditingTransactionId (state) {
+ return state.metamask.send.editingTransactionId
+}
+
+function getSendErrors (state) {
+ return state.send.errors
+}
+
+function getSendFrom (state) {
+ return state.metamask.send.from
+}
+
+function getSendFromBalance (state) {
+ const from = getSendFrom(state) || getSelectedAccount(state)
+ return from.balance
+}
+
+function getSendFromObject (state) {
+ return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
+}
+
+function getSendMaxModeState (state) {
+ return state.metamask.send.maxModeOn
+}
+
+function getSendTo (state) {
+ return state.metamask.send.to
+}
+
+function getSendToAccounts (state) {
+ const fromAccounts = accountsWithSendEtherInfoSelector(state)
+ const addressBookAccounts = getAddressBook(state)
+ const allAccounts = [...fromAccounts, ...addressBookAccounts]
+ // TODO: figure out exactly what the below returns and put a descriptive variable name on it
+ return Object.entries(allAccounts).map(([key, account]) => account)
+}
+
+function getSendWarnings (state) {
+ return state.send.warnings
+}
+
+function getTokenBalance (state) {
+ return state.metamask.send.tokenBalance
+}
+
+function getTokenExchangeRate (state, tokenSymbol) {
+ const pair = `${tokenSymbol.toLowerCase()}_eth`
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
+function getUnapprovedTxs (state) {
+ return state.metamask.unapprovedTxs
+}
+
+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)
+
+ 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 getQrCodeData (state) {
+ return state.appState.qrCodeData
+}
diff --git a/ui/app/components/app/send/send.utils.js b/ui/app/components/app/send/send.utils.js
new file mode 100644
index 000000000..7609d46ea
--- /dev/null
+++ b/ui/app/components/app/send/send.utils.js
@@ -0,0 +1,332 @@
+const {
+ addCurrencies,
+ conversionUtil,
+ conversionGTE,
+ multiplyCurrencies,
+ conversionGreaterThan,
+ conversionLessThan,
+} = require('../../../helpers/utils/conversion-util')
+const {
+ calcTokenAmount,
+} = require('../../../helpers/utils/token-util')
+const {
+ BASE_TOKEN_GAS_COST,
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+ NEGATIVE_ETH_ERROR,
+ ONE_GWEI_IN_WEI_HEX,
+ SIMPLE_GAS_COST,
+ TOKEN_TRANSFER_FUNCTION_SIGNATURE,
+} = require('./send.constants')
+const abi = require('ethereumjs-abi')
+const ethUtil = require('ethereumjs-util')
+
+module.exports = {
+ addGasBuffer,
+ calcGasTotal,
+ calcTokenBalance,
+ doesAmountErrorRequireUpdate,
+ estimateGas,
+ estimateGasPriceFromRecentBlocks,
+ generateTokenTransferData,
+ getAmountErrorObject,
+ getGasFeeErrorObject,
+ getToAddressForGasUpdate,
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+ removeLeadingZeroes,
+}
+
+function calcGasTotal (gasLimit = '0', gasPrice = '0') {
+ return multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+}
+
+function isBalanceSufficient ({
+ amount = '0x0',
+ amountConversionRate = 1,
+ balance = '0x0',
+ conversionRate = 1,
+ gasTotal = '0x0',
+ primaryCurrency,
+}) {
+ const totalAmount = addCurrencies(amount, gasTotal, {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+
+ const balanceIsSufficient = conversionGTE(
+ {
+ value: balance,
+ fromNumericBase: 'hex',
+ fromCurrency: primaryCurrency,
+ conversionRate,
+ },
+ {
+ value: totalAmount,
+ fromNumericBase: 'hex',
+ conversionRate: Number(amountConversionRate) || conversionRate,
+ fromCurrency: primaryCurrency,
+ },
+ )
+
+ return balanceIsSufficient
+}
+
+function isTokenBalanceSufficient ({
+ amount = '0x0',
+ tokenBalance,
+ decimals,
+}) {
+ const amountInDec = conversionUtil(amount, {
+ fromNumericBase: 'hex',
+ })
+
+ const tokenBalanceIsSufficient = conversionGTE(
+ {
+ value: tokenBalance,
+ fromNumericBase: 'hex',
+ },
+ {
+ value: calcTokenAmount(amountInDec, decimals),
+ },
+ )
+
+ return tokenBalanceIsSufficient
+}
+
+function getAmountErrorObject ({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+}) {
+ let insufficientFunds = false
+ if (gasTotal && conversionRate && !selectedToken) {
+ insufficientFunds = !isBalanceSufficient({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ })
+ }
+
+ let inSufficientTokens = false
+ if (selectedToken && tokenBalance !== null) {
+ const { decimals } = selectedToken
+ inSufficientTokens = !isTokenBalanceSufficient({
+ tokenBalance,
+ amount,
+ decimals,
+ })
+ }
+
+ const amountLessThanZero = conversionGreaterThan(
+ { value: 0, fromNumericBase: 'dec' },
+ { value: amount, fromNumericBase: 'hex' },
+ )
+
+ let amountError = null
+
+ if (insufficientFunds) {
+ amountError = INSUFFICIENT_FUNDS_ERROR
+ } else if (inSufficientTokens) {
+ amountError = INSUFFICIENT_TOKENS_ERROR
+ } else if (amountLessThanZero) {
+ amountError = NEGATIVE_ETH_ERROR
+ }
+
+ return { amount: amountError }
+}
+
+function getGasFeeErrorObject ({
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+}) {
+ let gasFeeError = null
+
+ if (gasTotal && conversionRate) {
+ const insufficientFunds = !isBalanceSufficient({
+ amount: '0x0',
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ })
+
+ if (insufficientFunds) {
+ gasFeeError = INSUFFICIENT_FUNDS_ERROR
+ }
+ }
+
+ return { gasFee: gasFeeError }
+}
+
+function calcTokenBalance ({ selectedToken, usersToken }) {
+ const { decimals } = selectedToken || {}
+ return calcTokenAmount(usersToken.balance.toString(), decimals).toString(16)
+}
+
+function doesAmountErrorRequireUpdate ({
+ balance,
+ gasTotal,
+ prevBalance,
+ prevGasTotal,
+ prevTokenBalance,
+ selectedToken,
+ tokenBalance,
+}) {
+ const balanceHasChanged = balance !== prevBalance
+ const gasTotalHasChange = gasTotal !== prevGasTotal
+ const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance
+ const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged
+
+ return amountErrorRequiresUpdate
+}
+
+async function estimateGas ({
+ selectedAddress,
+ selectedToken,
+ blockGasLimit,
+ to,
+ value,
+ data,
+ gasPrice,
+ estimateGasMethod,
+}) {
+ const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }
+
+ // if recipient has no code, gas is 21k max:
+ if (!selectedToken && !data) {
+ const code = Boolean(to) && await global.eth.getCode(to)
+ // Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
+ const codeIsEmpty = !code || code === '0x' || code === '0x0'
+ if (codeIsEmpty) {
+ return SIMPLE_GAS_COST
+ }
+ } else if (selectedToken && !to) {
+ return BASE_TOKEN_GAS_COST
+ }
+
+ 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 (!value || value === '0') {
+ paramsForGasEstimate.value = '0xff'
+ }
+ }
+
+ // if not, fall back to block gasLimit
+ paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit || '0x5208', 0.95, {
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ roundDown: '0',
+ toNumericBase: 'hex',
+ }))
+ // run tx
+ return new Promise((resolve, reject) => {
+ return estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => {
+ if (err) {
+ const simulationFailed = (
+ err.message.includes('Transaction execution error.') ||
+ err.message.includes('gas required exceeds allowance or always failing transaction')
+ )
+ if (simulationFailed) {
+ const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5)
+ return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
+ } else {
+ return reject(err)
+ }
+ }
+ const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5)
+ return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
+ })
+ })
+}
+
+function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) {
+ const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ numberOfDecimals: '0',
+ })
+ const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ numberOfDecimals: '0',
+ })
+
+ // if initialGasLimit is above blockGasLimit, dont modify it
+ if (conversionGreaterThan(
+ { value: initialGasLimitHex, fromNumericBase: 'hex' },
+ { value: upperGasLimit, fromNumericBase: 'hex' },
+ )) return initialGasLimitHex
+ // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
+ if (conversionLessThan(
+ { value: bufferedGasLimit, fromNumericBase: 'hex' },
+ { value: upperGasLimit, fromNumericBase: 'hex' },
+ )) return bufferedGasLimit
+ // otherwise use blockGasLimit
+ return upperGasLimit
+}
+
+function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) {
+ if (!selectedToken) return
+ return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
+ abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+}
+
+function estimateGasPriceFromRecentBlocks (recentBlocks) {
+ // Return 1 gwei if no blocks have been observed:
+ if (!recentBlocks || recentBlocks.length === 0) {
+ return ONE_GWEI_IN_WEI_HEX
+ }
+
+ const lowestPrices = recentBlocks.map((block) => {
+ if (!block.gasPrices || block.gasPrices.length < 1) {
+ return ONE_GWEI_IN_WEI_HEX
+ }
+ return block.gasPrices.reduce((currentLowest, next) => {
+ return parseInt(next, 16) < parseInt(currentLowest, 16) ? next : currentLowest
+ })
+ })
+ .sort((a, b) => parseInt(a, 16) > parseInt(b, 16) ? 1 : -1)
+
+ return lowestPrices[Math.floor(lowestPrices.length / 2)]
+}
+
+function getToAddressForGasUpdate (...addresses) {
+ return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase()
+}
+
+function removeLeadingZeroes (str) {
+ return str.replace(/^0*(?=\d)/, '')
+}
diff --git a/ui/app/components/app/send/tests/send-component.test.js b/ui/app/components/app/send/tests/send-component.test.js
new file mode 100644
index 000000000..738c14839
--- /dev/null
+++ b/ui/app/components/app/send/tests/send-component.test.js
@@ -0,0 +1,354 @@
+import React from 'react'
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import timeout from '../../../../../lib/test-timeout'
+
+import SendHeader from '../send-header/send-header.container'
+import SendContent from '../send-content/send-content.component'
+import SendFooter from '../send-footer/send-footer.container'
+
+const mockBasicGasEstimates = {
+ blockTime: 'mockBlockTime',
+}
+
+const propsMethodSpies = {
+ updateAndSetGasLimit: sinon.spy(),
+ updateSendErrors: sinon.spy(),
+ updateSendTokenBalance: sinon.spy(),
+ resetSendState: sinon.spy(),
+ fetchBasicGasEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
+ fetchGasEstimates: sinon.spy(),
+}
+const utilsMethodStubs = {
+ getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
+ getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }),
+ doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance),
+}
+
+const SendTransactionScreen = proxyquire('../send.component.js', {
+ './send.utils': utilsMethodStubs,
+}).default
+
+sinon.spy(SendTransactionScreen.prototype, 'componentDidMount')
+sinon.spy(SendTransactionScreen.prototype, 'updateGas')
+
+describe('Send Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendTransactionScreen
+ amount={'mockAmount'}
+ amountConversionRate={'mockAmountConversionRate'}
+ blockGasLimit={'mockBlockGasLimit'}
+ conversionRate={10}
+ editingTransactionId={'mockEditingTransactionId'}
+ fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
+ fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={{ mockProp: 'history-abc'}}
+ network={'3'}
+ primaryCurrency={'mockPrimaryCurrency'}
+ recentBlocks={['mockBlock']}
+ selectedAddress={'mockSelectedAddress'}
+ selectedToken={'mockSelectedToken'}
+ showHexData={true}
+ tokenBalance={'mockTokenBalance'}
+ tokenContract={'mockTokenContract'}
+ updateAndSetGasLimit={propsMethodSpies.updateAndSetGasLimit}
+ updateSendErrors={propsMethodSpies.updateSendErrors}
+ updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
+ resetSendState={propsMethodSpies.resetSendState}
+ />)
+ })
+
+ afterEach(() => {
+ SendTransactionScreen.prototype.componentDidMount.resetHistory()
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ propsMethodSpies.fetchBasicGasEstimates.resetHistory()
+ propsMethodSpies.updateAndSetGasLimit.resetHistory()
+ propsMethodSpies.updateSendErrors.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ })
+
+ it('should call componentDidMount', () => {
+ assert(SendTransactionScreen.prototype.componentDidMount.calledOnce)
+ })
+
+ describe('componentDidMount', () => {
+ it('should call props.fetchBasicGasAndTimeEstimates', () => {
+ propsMethodSpies.fetchBasicGasEstimates.resetHistory()
+ assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0)
+ wrapper.instance().componentDidMount()
+ assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 1)
+ })
+
+ it('should call this.updateGas', async () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendErrors.resetHistory()
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ wrapper.instance().componentDidMount()
+ await timeout(250)
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
+ })
+ })
+
+ describe('componentWillUnmount', () => {
+ it('should call this.props.resetSendState', () => {
+ propsMethodSpies.resetSendState.resetHistory()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 0)
+ wrapper.instance().componentWillUnmount()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 1)
+ })
+ })
+
+ describe('componentDidUpdate', () => {
+ it('should call doesAmountErrorRequireUpdate with the expected params', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: '',
+ },
+ })
+ assert(utilsMethodStubs.doesAmountErrorRequireUpdate.calledOnce)
+ assert.deepEqual(
+ utilsMethodStubs.doesAmountErrorRequireUpdate.getCall(0).args[0],
+ {
+ balance: 'mockBalance',
+ gasTotal: 'mockGasTotal',
+ prevBalance: '',
+ prevGasTotal: undefined,
+ prevTokenBalance: undefined,
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'mockBalance',
+ },
+ })
+ assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 0)
+ })
+
+ it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 1)
+ assert.deepEqual(
+ utilsMethodStubs.getAmountErrorObject.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 10,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1)
+ assert.deepEqual(
+ utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0],
+ {
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 10,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: 'mockSelectedToken',
+ }
+ )
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: { address: 'mockAddress', balance: 'mockBalance' },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is falsy', () => {
+ propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendErrors.getCall(0).args[0],
+ { amount: 'mockAmountError', gasFee: null }
+ )
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is truthy', () => {
+ propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: 'someToken' })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendErrors.getCall(0).args[0],
+ { amount: 'mockAmountError', gasFee: 'mockGasFeeError' }
+ )
+ })
+
+ it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '3',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ })
+
+ it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => {
+ wrapper.setProps({ network: 'loading' })
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '3',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ })
+
+ it('should call updateSendTokenBalance and this.updateGas with the correct params', () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '2',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendTokenBalance.getCall(0).args[0],
+ {
+ selectedToken: 'mockSelectedToken',
+ tokenContract: 'mockTokenContract',
+ address: 'mockAddress',
+ }
+ )
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
+ assert.deepEqual(
+ SendTransactionScreen.prototype.updateGas.getCall(0).args,
+ []
+ )
+ })
+ })
+
+ describe('updateGas', () => {
+ it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => {
+ propsMethodSpies.updateAndSetGasLimit.resetHistory()
+ wrapper.instance().updateGas()
+ assert.equal(propsMethodSpies.updateAndSetGasLimit.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0],
+ {
+ blockGasLimit: 'mockBlockGasLimit',
+ editingTransactionId: 'mockEditingTransactionId',
+ gasLimit: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ recentBlocks: ['mockBlock'],
+ selectedAddress: 'mockSelectedAddress',
+ selectedToken: 'mockSelectedToken',
+ to: '',
+ value: 'mockAmount',
+ data: undefined,
+ }
+ )
+ })
+
+ it('should call updateAndSetGasLimit with the correct params if a to prop is passed', () => {
+ propsMethodSpies.updateAndSetGasLimit.resetHistory()
+ wrapper.setProps({ to: 'someAddress' })
+ wrapper.instance().updateGas()
+ assert.equal(
+ propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to,
+ 'someaddress',
+ )
+ })
+
+ it('should call updateAndSetGasLimit with to set to lowercase if passed', () => {
+ propsMethodSpies.updateAndSetGasLimit.resetHistory()
+ wrapper.instance().updateGas({ to: '0xABC' })
+ assert.equal(propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, '0xabc')
+ })
+ })
+
+ describe('render', () => {
+ it('should render a page-container class', () => {
+ assert.equal(wrapper.find('.page-container').length, 1)
+ })
+
+ it('should render SendHeader, SendContent and SendFooter', () => {
+ assert.equal(wrapper.find(SendHeader).length, 1)
+ assert.equal(wrapper.find(SendContent).length, 1)
+ assert.equal(wrapper.find(SendFooter).length, 1)
+ })
+
+ it('should pass the history prop to SendHeader and SendFooter', () => {
+ assert.deepEqual(
+ wrapper.find(SendFooter).props(),
+ {
+ history: { mockProp: 'history-abc' },
+ }
+ )
+ })
+
+ it('should pass showHexData to SendContent', () => {
+ assert.equal(wrapper.find(SendContent).props().showHexData, true)
+ })
+ })
+})
diff --git a/ui/app/components/app/send/tests/send-container.test.js b/ui/app/components/app/send/tests/send-container.test.js
new file mode 100644
index 000000000..9538b67b3
--- /dev/null
+++ b/ui/app/components/app/send/tests/send-container.test.js
@@ -0,0 +1,174 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ updateSendTokenBalance: sinon.spy(),
+ updateGasData: sinon.spy(),
+ setGasTotal: sinon.spy(),
+}
+const duckActionSpies = {
+ updateSendErrors: sinon.spy(),
+ resetSendState: sinon.spy(),
+}
+
+proxyquire('../send.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ 'react-router-dom': { withRouter: () => {} },
+ 'recompose': { compose: (arg1, arg2) => () => arg2() },
+ './send.selectors': {
+ getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
+ getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`,
+ getConversionRate: (s) => `mockConversionRate:${s}`,
+ getCurrentNetwork: (s) => `mockNetwork:${s}`,
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
+ getRecentBlocks: (s) => `mockRecentBlocks:${s}`,
+ getSelectedAddress: (s) => `mockSelectedAddress:${s}`,
+ 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}`,
+ getSendFromObject: (s) => `mockFrom:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getQrCodeData: (s) => `mockQrCodeData:${s}`,
+ },
+ '../../../store/actions': actionSpies,
+ '../../../ducks/send/send.duck': duckActionSpies,
+ './send.utils.js': {
+ calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
+ },
+})
+
+describe('send container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ amount: 'mockAmount:mockState',
+ amountConversionRate: 'mockAmountConversionRate:mockState',
+ blockGasLimit: 'mockBlockGasLimit:mockState',
+ conversionRate: 'mockConversionRate:mockState',
+ editingTransactionId: 'mockEditingTransactionId:mockState',
+ from: 'mockFrom:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ network: 'mockNetwork:mockState',
+ primaryCurrency: 'mockPrimaryCurrency:mockState',
+ recentBlocks: 'mockRecentBlocks:mockState',
+ selectedAddress: 'mockSelectedAddress:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ showHexData: 'mockSendHexDataFeatureFlagState:mockState',
+ to: 'mockTo:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ tokenContract: 'mockTokenContract:mockState',
+ tokenToFiatRate: 'mockTokenToFiatRate:mockState',
+ qrCodeData: 'mockQrCodeData:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('updateAndSetGasLimit()', () => {
+ const mockProps = {
+ blockGasLimit: 'mockBlockGasLimit',
+ editingTransactionId: '0x2',
+ gasLimit: '0x3',
+ gasPrice: '0x4',
+ recentBlocks: ['mockBlock'],
+ selectedAddress: '0x4',
+ selectedToken: { address: '0x1' },
+ to: 'mockTo',
+ value: 'mockValue',
+ data: undefined,
+ }
+
+ it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
+ mapDispatchToPropsObject.updateAndSetGasLimit(mockProps)
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ actionSpies.setGasTotal.getCall(0).args[0],
+ '0x30x4'
+ )
+ })
+
+ it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
+ const { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
+ mapDispatchToPropsObject.updateAndSetGasLimit(
+ Object.assign({}, mockProps, {editingTransactionId: false})
+ )
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.updateGasData.getCall(0).args[0],
+ { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data }
+ )
+ })
+ })
+
+ describe('updateSendTokenBalance()', () => {
+ const mockProps = {
+ address: '0x10',
+ tokenContract: '0x00a',
+ selectedToken: {address: '0x1'},
+ }
+
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendTokenBalance(Object.assign({}, mockProps))
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.updateSendTokenBalance.getCall(0).args[0],
+ mockProps
+ )
+ })
+ })
+
+ describe('updateSendErrors()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendErrors('mockError')
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ 'mockError'
+ )
+ })
+ })
+
+ describe('resetSendState()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.resetSendState()
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ duckActionSpies.resetSendState.getCall(0).args.length,
+ 0
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/app/send/tests/send-selectors-test-data.js
index d43d7c650..d43d7c650 100644
--- a/ui/app/components/send/tests/send-selectors-test-data.js
+++ b/ui/app/components/app/send/tests/send-selectors-test-data.js
diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/app/send/tests/send-selectors.test.js
index cdc86fe59..cdc86fe59 100644
--- a/ui/app/components/send/tests/send-selectors.test.js
+++ b/ui/app/components/app/send/tests/send-selectors.test.js
diff --git a/ui/app/components/app/send/tests/send-utils.test.js b/ui/app/components/app/send/tests/send-utils.test.js
new file mode 100644
index 000000000..fc4c6deed
--- /dev/null
+++ b/ui/app/components/app/send/tests/send-utils.test.js
@@ -0,0 +1,527 @@
+import assert from 'assert'
+import sinon from 'sinon'
+import proxyquire from 'proxyquire'
+import {
+ BASE_TOKEN_GAS_COST,
+ ONE_GWEI_IN_WEI_HEX,
+ SIMPLE_GAS_COST,
+} from '../send.constants'
+const {
+ addCurrencies,
+ subtractCurrencies,
+} = require('../../../../helpers/utils/conversion-util')
+
+const {
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+} = require('../send.constants')
+
+const stubs = {
+ addCurrencies: sinon.stub().callsFake((a, b, obj) => {
+ if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2))
+ if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2))
+ return a + b
+ }),
+ conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
+ conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
+ multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
+ calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
+ rawEncode: sinon.stub().returns([16, 1100]),
+ conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
+ conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value),
+}
+
+const sendUtils = proxyquire('../send.utils.js', {
+ '../../../helpers/utils/conversion-util': {
+ addCurrencies: stubs.addCurrencies,
+ conversionUtil: stubs.conversionUtil,
+ conversionGTE: stubs.conversionGTE,
+ multiplyCurrencies: stubs.multiplyCurrencies,
+ conversionGreaterThan: stubs.conversionGreaterThan,
+ conversionLessThan: stubs.conversionLessThan,
+ },
+ '../../../helpers/utils/token-util': { calcTokenAmount: stubs.calcTokenAmount },
+ 'ethereumjs-abi': {
+ rawEncode: stubs.rawEncode,
+ },
+})
+
+const {
+ calcGasTotal,
+ estimateGas,
+ doesAmountErrorRequireUpdate,
+ estimateGasPriceFromRecentBlocks,
+ generateTokenTransferData,
+ getAmountErrorObject,
+ getGasFeeErrorObject,
+ getToAddressForGasUpdate,
+ calcTokenBalance,
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+ removeLeadingZeroes,
+} = sendUtils
+
+describe('send utils', () => {
+
+ describe('calcGasTotal()', () => {
+ it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => {
+ const result = calcGasTotal(12, 15)
+ assert.equal(result, '12x15')
+ const call_ = stubs.multiplyCurrencies.getCall(0).args
+ assert.deepEqual(
+ call_,
+ [12, 15, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ } ]
+ )
+ })
+ })
+
+ describe('doesAmountErrorRequireUpdate()', () => {
+ const config = {
+ 'should return true if balances are different': {
+ balance: 0,
+ prevBalance: 1,
+ expectedResult: true,
+ },
+ 'should return true if gasTotals are different': {
+ gasTotal: 0,
+ prevGasTotal: 1,
+ expectedResult: true,
+ },
+ 'should return true if token balances are different': {
+ tokenBalance: 0,
+ prevTokenBalance: 1,
+ selectedToken: 'someToken',
+ expectedResult: true,
+ },
+ 'should return false if they are all the same': {
+ balance: 1,
+ prevBalance: 1,
+ gasTotal: 1,
+ prevGasTotal: 1,
+ tokenBalance: 1,
+ prevTokenBalance: 1,
+ selectedToken: 'someToken',
+ expectedResult: false,
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult)
+ })
+ })
+
+ })
+
+ describe('generateTokenTransferData()', () => {
+ it('should return undefined if not passed a selected token', () => {
+ assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined)
+ })
+
+ it('should call abi.rawEncode with the correct params', () => {
+ stubs.rawEncode.resetHistory()
+ generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true})
+ assert.deepEqual(
+ stubs.rawEncode.getCall(0).args,
+ [['address', 'uint256'], ['mockAddress', '0xab']]
+ )
+ })
+
+ it('should return encoded token transfer data', () => {
+ assert.equal(
+ generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}),
+ '0xa9059cbb104c'
+ )
+ })
+ })
+
+ describe('getAmountErrorObject()', () => {
+ const config = {
+ 'should return insufficientFunds error if isBalanceSufficient returns false': {
+ amount: 15,
+ amountConversionRate: 2,
+ balance: 1,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
+ },
+ 'should not return insufficientFunds error if selectedToken is truthy': {
+ amount: '0x0',
+ amountConversionRate: 2,
+ balance: 1,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ selectedToken: { symbole: 'DEF', decimals: 0 },
+ decimals: 0,
+ tokenBalance: 'sometokenbalance',
+ expectedResult: { amount: null },
+ },
+ 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
+ amount: '0x10',
+ amountConversionRate: 2,
+ balance: 100,
+ conversionRate: 3,
+ decimals: 10,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ selectedToken: 'someToken',
+ tokenBalance: 123,
+ expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR },
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult)
+ })
+ })
+ })
+
+ describe('getGasFeeErrorObject()', () => {
+ const config = {
+ 'should return insufficientFunds error if isBalanceSufficient returns false': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR },
+ },
+ 'should return null error if isBalanceSufficient returns true': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 15,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: null },
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult)
+ })
+ })
+ })
+
+ describe('calcTokenBalance()', () => {
+ it('should return the calculated token blance', () => {
+ assert.equal(calcTokenBalance({
+ selectedToken: {
+ decimals: 11,
+ },
+ usersToken: {
+ balance: 20,
+ },
+ }), 'calc:2011')
+ })
+ })
+
+ describe('isBalanceSufficient()', () => {
+ it('should correctly call addCurrencies and return the result of calling conversionGTE', () => {
+ stubs.conversionGTE.resetHistory()
+ const result = isBalanceSufficient({
+ amount: 15,
+ amountConversionRate: 2,
+ balance: 100,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ })
+ assert.deepEqual(
+ stubs.addCurrencies.getCall(0).args,
+ [
+ 15, 17, {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ },
+ ]
+ )
+ assert.deepEqual(
+ stubs.conversionGTE.getCall(0).args,
+ [
+ {
+ value: 100,
+ fromNumericBase: 'hex',
+ fromCurrency: 'ABC',
+ conversionRate: 3,
+ },
+ {
+ value: 32,
+ fromNumericBase: 'hex',
+ conversionRate: 2,
+ fromCurrency: 'ABC',
+ },
+ ]
+ )
+
+ assert.equal(result, true)
+ })
+ })
+
+ describe('isTokenBalanceSufficient()', () => {
+ it('should correctly call conversionUtil and return the result of calling conversionGTE', () => {
+ stubs.conversionGTE.resetHistory()
+ stubs.conversionUtil.resetHistory()
+ const result = isTokenBalanceSufficient({
+ amount: '0x10',
+ tokenBalance: 123,
+ decimals: 10,
+ })
+ assert.deepEqual(
+ stubs.conversionUtil.getCall(0).args,
+ [
+ '0x10', {
+ fromNumericBase: 'hex',
+ },
+ ]
+ )
+ assert.deepEqual(
+ stubs.conversionGTE.getCall(0).args,
+ [
+ {
+ value: 123,
+ fromNumericBase: 'hex',
+ },
+ {
+ value: 'calc:1610',
+ },
+ ]
+ )
+
+ assert.equal(result, false)
+ })
+ })
+
+ describe('estimateGas', () => {
+ const baseMockParams = {
+ blockGasLimit: '0x64',
+ selectedAddress: 'mockAddress',
+ to: '0xisContract',
+ estimateGasMethod: sinon.stub().callsFake(
+ ({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 = {
+ from: 'mockAddress',
+ gas: '0x64x0.95',
+ to: '0xisContract',
+ value: '0xff',
+ }
+
+ beforeEach(() => {
+ global.eth = {
+ getCode: sinon.stub().callsFake(
+ (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
+ ),
+ }
+ })
+
+ afterEach(() => {
+ baseMockParams.estimateGasMethod.resetHistory()
+ global.eth.getCode.resetHistory()
+ })
+
+ it('should call ethQuery.estimateGas with the expected params', async () => {
+ const result = await sendUtils.estimateGas(baseMockParams)
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
+ )
+ assert.equal(result, '0xabc16')
+ })
+
+ it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' }))
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' })
+ )
+ assert.equal(result, '0xabc16x1.5')
+ })
+
+ it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
+ const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams))
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ Object.assign({}, baseExpectedCall, {
+ gasPrice: undefined,
+ value: '0x0',
+ data: '0xa9059cbb104c',
+ to: 'mockAddress',
+ })
+ )
+ 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: '0xff', 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' }))
+ assert.equal(result, SIMPLE_GAS_COST)
+ })
+
+ it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => {
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: null }))
+ assert.equal(result, SIMPLE_GAS_COST)
+ })
+
+ it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => {
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
+ assert.notEqual(result, SIMPLE_GAS_COST)
+ })
+
+ it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } }))
+ assert.equal(result, BASE_TOKEN_GAS_COST)
+ })
+
+ it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:Transaction execution error.',
+ }))
+ assert.equal(result, '0x64x0.95')
+ })
+
+ it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.',
+ }))
+ assert.equal(result, '0x64x0.95')
+ })
+
+ it(`should reject other errors`, async () => {
+ try {
+ await estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:some other error',
+ }))
+ } catch (err) {
+ assert.equal(err.message, 'some other error')
+ }
+ })
+ })
+
+ describe('estimateGasPriceFromRecentBlocks', () => {
+ const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+ const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+ const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+
+ it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => {
+ assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => {
+ assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: null },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should return the middle value of all blocks lowest prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE)
+ })
+
+ it(`should work if a block has multiple gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] },
+ { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] },
+ { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
+ })
+ })
+
+ describe('getToAddressForGasUpdate()', () => {
+ it('should return empty string if all params are undefined or null', () => {
+ assert.equal(getToAddressForGasUpdate(undefined, null), '')
+ })
+
+ it('should return the first string that is not defined or null in lower case', () => {
+ assert.equal(getToAddressForGasUpdate('A', null), 'a')
+ assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
+ })
+ })
+
+ describe('removeLeadingZeroes()', () => {
+ it('should remove leading zeroes from int when user types', () => {
+ assert.equal(removeLeadingZeroes('0'), '0')
+ assert.equal(removeLeadingZeroes('1'), '1')
+ assert.equal(removeLeadingZeroes('00'), '0')
+ assert.equal(removeLeadingZeroes('01'), '1')
+ })
+
+ it('should remove leading zeroes from int when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('001'), '1')
+ })
+
+ it('should remove leading zeroes from float when user types', () => {
+ assert.equal(removeLeadingZeroes('0.'), '0.')
+ assert.equal(removeLeadingZeroes('0.0'), '0.0')
+ assert.equal(removeLeadingZeroes('0.00'), '0.00')
+ assert.equal(removeLeadingZeroes('0.001'), '0.001')
+ assert.equal(removeLeadingZeroes('0.10'), '0.10')
+ })
+
+ it('should remove leading zeroes from float when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('00.1'), '0.1')
+ })
+ })
+})
diff --git a/ui/app/components/app/send/to-autocomplete.component.js b/ui/app/components/app/send/to-autocomplete.component.js
new file mode 100644
index 000000000..183967c58
--- /dev/null
+++ b/ui/app/components/app/send/to-autocomplete.component.js
@@ -0,0 +1,141 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import AccountListItem from './account-list-item/account-list-item.component'
+
+
+export default class ToAutoComplete extends Component {
+
+ static propTypes = {
+ dropdownOpen: PropTypes.bool,
+ openDropdown: PropTypes.func,
+ closeDropdown: PropTypes.func,
+ onChange: PropTypes.func,
+ to: PropTypes.string,
+ accounts: PropTypes.array,
+ inError: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ accountsToRender: [],
+ }
+
+ getListItemIcon (listItemAddress, toAddress) {
+ return toAddress && listItemAddress === toAddress
+ ? <i className={'fa fa-check fa-lg'}
+ style={{
+ color: '#02c9b1',
+ }}
+ />
+ : null
+ }
+
+ renderDropdown () {
+ const {
+ closeDropdown,
+ onChange,
+ to,
+ } = this.props
+ const {accountsToRender} = this.state
+
+ if (!accountsToRender.length) {
+ return null
+ }
+
+ return (
+ <div>
+ <div className={'send-v2__from-dropdown__close-area'} onClick={closeDropdown} />
+ <div className={'send-v2__from-dropdown__list'}>
+ {accountsToRender.map((account, i) => (
+ <AccountListItem
+ key={i}
+ account={account}
+ className={'account-list-item__dropdown'}
+ handleClick={() => {
+ onChange(account.address)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account.address, to)}
+ displayBalance={false}
+ displayAddress={true}
+ />
+ ))}
+ </div>
+ </div>
+ )
+ }
+
+ handleInputEvent (event = {}, cb) {
+ const {
+ to,
+ accounts,
+ closeDropdown,
+ openDropdown,
+ } = this.props
+
+ const matchingAccounts = accounts.filter(({address}) => address.match(to || ''))
+ const matches = matchingAccounts.length
+
+ if (!matches || matchingAccounts[0].address === to) {
+ this.setState({accountsToRender: []})
+ event.target && event.target.select()
+ closeDropdown()
+ } else {
+ this.setState({accountsToRender: matchingAccounts})
+ openDropdown()
+ }
+ cb && cb(event.target.value)
+ }
+
+ componentDidUpdate (nextProps) {
+ if (this.props.to !== nextProps.to) {
+ this.handleInputEvent()
+ }
+ }
+
+ render () {
+ const {
+ to,
+ dropdownOpen,
+ onChange,
+ inError,
+ } = this.props
+
+ return (
+ <div className={'send-v2__to-autocomplete'}>
+ <input
+ className={classnames('send-v2__to-autocomplete__input', {
+ 'send-v2__error-border': inError,
+ })}
+ placeholder={this.context.t('recipientAddress')}
+ value={to}
+ onChange={event => onChange(event.target.value)}
+ onFocus={event => this.handleInputEvent(event)}
+ style={{
+ borderColor: inError ? 'red' : null,
+ }}
+ />
+ {
+ to
+ ? null
+ : <i className={'fa fa-caret-down fa-lg send-v2__to-autocomplete__down-caret'}
+ onClick={() => this.handleInputEvent()}
+ style={{
+ style: {color: '#dedede'},
+ }}
+ />
+ }
+ {
+ dropdownOpen
+ ? this.renderDropdown()
+ : null
+ }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/app/send/to-autocomplete/index.js
index 244d301d1..244d301d1 100644
--- a/ui/app/components/send/to-autocomplete/index.js
+++ b/ui/app/components/app/send/to-autocomplete/index.js
diff --git a/ui/app/components/app/send/to-autocomplete/to-autocomplete.js b/ui/app/components/app/send/to-autocomplete/to-autocomplete.js
new file mode 100644
index 000000000..d3db8cb59
--- /dev/null
+++ b/ui/app/components/app/send/to-autocomplete/to-autocomplete.js
@@ -0,0 +1,129 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+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('../../../ui/tooltip')
+const checksumAddress = require('../../../../helpers/utils/util').checksumAddress
+
+ToAutoComplete.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect()(ToAutoComplete)
+
+
+inherits(ToAutoComplete, Component)
+function ToAutoComplete () {
+ Component.call(this)
+
+ this.state = { accountsToRender: [] }
+}
+
+ToAutoComplete.prototype.getListItemIcon = function (listItemAddress, toAddress) {
+ const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
+
+ return toAddress && listItemAddress === toAddress
+ ? listItemIcon
+ : null
+}
+
+ToAutoComplete.prototype.renderDropdown = function () {
+ const {
+ closeDropdown,
+ onChange,
+ to,
+ } = this.props
+ const { accountsToRender } = this.state
+
+ return accountsToRender.length && h('div', {}, [
+
+ h('div.send-v2__from-dropdown__close-area', {
+ onClick: closeDropdown,
+ }),
+
+ h('div.send-v2__from-dropdown__list', {}, [
+
+ ...accountsToRender.map(account => h(AccountListItem, {
+ account,
+ className: 'account-list-item__dropdown',
+ handleClick: () => {
+ onChange(checksumAddress(account.address))
+ closeDropdown()
+ },
+ icon: this.getListItemIcon(account.address, to),
+ displayBalance: false,
+ displayAddress: true,
+ })),
+
+ ]),
+
+ ])
+}
+
+ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) {
+ const {
+ to,
+ accounts,
+ closeDropdown,
+ openDropdown,
+ } = this.props
+
+ const matchingAccounts = accounts.filter(({ address }) => address.match(to || ''))
+ const matches = matchingAccounts.length
+
+ if (!matches || matchingAccounts[0].address === to) {
+ this.setState({ accountsToRender: [] })
+ event.target && event.target.select()
+ closeDropdown()
+ } else {
+ this.setState({ accountsToRender: matchingAccounts })
+ openDropdown()
+ }
+ cb && cb(event.target.value)
+}
+
+ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) {
+ if (this.props.to !== nextProps.to) {
+ this.handleInputEvent()
+ }
+}
+
+ToAutoComplete.prototype.render = function () {
+ const {
+ to,
+ dropdownOpen,
+ onChange,
+ inError,
+ qrScanner,
+ } = this.props
+
+ return h('div.send-v2__to-autocomplete', {}, [
+
+ h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
+ placeholder: this.context.t('recipientAddress'),
+ className: inError ? `send-v2__error-border` : '',
+ value: to,
+ onChange: event => onChange(event.target.value),
+ onFocus: event => this.handleInputEvent(event),
+ style: {
+ borderColor: inError ? 'red' : null,
+ },
+ }),
+ qrScanner && h(Tooltip, {
+ title: this.context.t('scanQrCode'),
+ position: 'bottom',
+ }, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, {
+ style: { color: '#33333' },
+ onClick: () => this.props.scanQrCode(),
+ })),
+ !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
+ style: { color: '#dedede' },
+ onClick: () => this.handleInputEvent(),
+ }),
+
+ dropdownOpen && this.renderDropdown(),
+
+ ])
+}
diff --git a/ui/app/components/app/shapeshift-form.js b/ui/app/components/app/shapeshift-form.js
new file mode 100644
index 000000000..11459fd5e
--- /dev/null
+++ b/ui/app/components/app/shapeshift-form.js
@@ -0,0 +1,256 @@
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const PropTypes = require('prop-types')
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const classnames = require('classnames')
+const qrcode = require('qrcode-generator')
+const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../../store/actions')
+const { isValidAddress } = require('../../helpers/utils/util')
+const SimpleDropdown = require('./dropdowns/simple-dropdown')
+
+import Button from '../ui/button'
+
+function mapStateToProps (state) {
+ const {
+ coinOptions,
+ tokenExchangeRates,
+ selectedAddress,
+ } = state.metamask
+ const { warning } = state.appState
+
+ return {
+ coinOptions,
+ tokenExchangeRates,
+ selectedAddress,
+ warning,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ shapeShiftSubview: () => dispatch(shapeShiftSubview()),
+ pairUpdate: coin => dispatch(pairUpdate(coin)),
+ buyWithShapeShift: data => dispatch(buyWithShapeShift(data)),
+ }
+}
+
+ShapeshiftForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftForm)
+
+
+inherits(ShapeshiftForm, Component)
+function ShapeshiftForm () {
+ Component.call(this)
+
+ this.state = {
+ depositCoin: 'btc',
+ refundAddress: '',
+ showQrCode: false,
+ depositAddress: '',
+ errorMessage: '',
+ isLoading: false,
+ bought: false,
+ }
+}
+
+ShapeshiftForm.prototype.getCoinPair = function () {
+ return `${this.state.depositCoin.toUpperCase()}_ETH`
+}
+
+ShapeshiftForm.prototype.componentWillMount = function () {
+ this.props.shapeShiftSubview()
+}
+
+ShapeshiftForm.prototype.onCoinChange = function (coin) {
+ this.setState({
+ depositCoin: coin,
+ errorMessage: '',
+ })
+ this.props.pairUpdate(coin)
+}
+
+ShapeshiftForm.prototype.onBuyWithShapeShift = function () {
+ this.setState({
+ isLoading: true,
+ showQrCode: true,
+ })
+
+ const {
+ buyWithShapeShift,
+ selectedAddress: withdrawal,
+ } = this.props
+ const {
+ refundAddress: returnAddress,
+ depositCoin,
+ } = this.state
+ const pair = `${depositCoin}_eth`
+ const data = {
+ withdrawal,
+ pair,
+ returnAddress,
+ // Public api key
+ 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6',
+ }
+
+ if (isValidAddress(withdrawal)) {
+ buyWithShapeShift(data)
+ .then(d => this.setState({
+ showQrCode: true,
+ depositAddress: d.deposit,
+ isLoading: false,
+ }))
+ .catch(() => this.setState({
+ showQrCode: false,
+ errorMessage: this.context.t('invalidRequest'),
+ isLoading: false,
+ }))
+ }
+}
+
+ShapeshiftForm.prototype.renderMetadata = function (label, value) {
+ return h('div', {className: 'shapeshift-form__metadata-wrapper'}, [
+
+ h('div.shapeshift-form__metadata-label', {}, [
+ h('span', `${label}:`),
+ ]),
+
+ h('div.shapeshift-form__metadata-value', {}, [
+ h('span', value),
+ ]),
+
+ ])
+}
+
+ShapeshiftForm.prototype.renderMarketInfo = function () {
+ const { tokenExchangeRates } = this.props
+ const {
+ limit,
+ rate,
+ minimum,
+ } = tokenExchangeRates[this.getCoinPair()] || {}
+
+ return h('div.shapeshift-form__metadata', {}, [
+
+ this.renderMetadata(this.context.t('status'), limit ? this.context.t('available') : this.context.t('unavailable')),
+ this.renderMetadata(this.context.t('limit'), limit),
+ this.renderMetadata(this.context.t('exchangeRate'), rate),
+ this.renderMetadata(this.context.t('min'), minimum),
+
+ ])
+}
+
+ShapeshiftForm.prototype.renderQrCode = function () {
+ const { depositAddress, isLoading, depositCoin } = this.state
+ const qrImage = qrcode(4, 'M')
+ qrImage.addData(depositAddress)
+ qrImage.make()
+
+ return h('div.shapeshift-form', {}, [
+
+ h('div.shapeshift-form__deposit-instruction', [
+ this.context.t('depositCoin', [depositCoin.toUpperCase()]),
+ ]),
+
+ h('div', depositAddress),
+
+ h('div.shapeshift-form__qr-code', [
+ isLoading
+ ? h('img', {
+ src: 'images/loading.svg',
+ style: { width: '60px'},
+ })
+ : h('div', {
+ dangerouslySetInnerHTML: { __html: qrImage.createTableTag(4) },
+ }),
+ ]),
+
+ this.renderMarketInfo(),
+
+ ])
+}
+
+
+ShapeshiftForm.prototype.render = function () {
+ const { coinOptions, btnClass, warning } = this.props
+ const { errorMessage, showQrCode, depositAddress } = this.state
+ const { tokenExchangeRates } = this.props
+ const token = tokenExchangeRates[this.getCoinPair()]
+
+ return h('div.shapeshift-form-wrapper', [
+ showQrCode
+ ? this.renderQrCode()
+ : h('div.modal-shapeshift-form', [
+ h('div.shapeshift-form__selectors', [
+
+ h('div.shapeshift-form__selector', [
+
+ h('div.shapeshift-form__selector-label', this.context.t('deposit')),
+
+ h(SimpleDropdown, {
+ selectedOption: this.state.depositCoin,
+ onSelect: (coin) => this.onCoinChange(coin),
+ options: Object.entries(coinOptions).map(([coin]) => ({
+ value: coin.toLowerCase(),
+ displayValue: coin,
+ })),
+ }),
+
+ ]),
+
+ h('div.icon.shapeshift-form__caret', {
+ style: { backgroundImage: 'url(images/caret-right.svg)'},
+ }),
+
+ h('div.shapeshift-form__selector', [
+
+ h('div.shapeshift-form__selector-label', [
+ this.context.t('receive'),
+ ]),
+
+ h('div.shapeshift-form__selector-input', ['ETH']),
+
+ ]),
+
+ ]),
+
+ warning && h('div.shapeshift-form__address-input-label', warning),
+
+ !warning && h('div', {
+ className: classnames('shapeshift-form__address-input-wrapper', {
+ 'shapeshift-form__address-input-wrapper--error': errorMessage,
+ }),
+ }, [
+
+ h('div.shapeshift-form__address-input-label', [
+ this.context.t('refundAddress'),
+ ]),
+
+ h('input.shapeshift-form__address-input', {
+ type: 'text',
+ onChange: e => this.setState({
+ refundAddress: e.target.value,
+ errorMessage: '',
+ }),
+ }),
+
+ h('divshapeshift-form__address-input-error-message', [errorMessage]),
+ ]),
+
+ !warning && this.renderMarketInfo(),
+
+ ]),
+
+ !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/app/shift-list-item.js b/ui/app/components/app/shift-list-item.js
new file mode 100644
index 000000000..f5fa00047
--- /dev/null
+++ b/ui/app/components/app/shift-list-item.js
@@ -0,0 +1,204 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const explorerLink = require('etherscan-link').createExplorerLink
+const actions = require('../../store/actions')
+const { formatDate, addressSummary } = require('../../helpers/utils/util')
+
+const CopyButton = require('../ui/copyButton')
+const EthBalance = require('../ui/eth-balance')
+const Tooltip = require('../ui/tooltip')
+
+
+ShiftListItem.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps)(ShiftListItem)
+
+
+function mapStateToProps (state) {
+ return {
+ selectedAddress: state.metamask.selectedAddress,
+ conversionRate: state.metamask.conversionRate,
+ currentCurrency: state.metamask.currentCurrency,
+ }
+}
+
+inherits(ShiftListItem, Component)
+
+function ShiftListItem () {
+ Component.call(this)
+}
+
+ShiftListItem.prototype.render = function () {
+ return h('div.transaction-list-item.tx-list-clickable', {
+ style: {
+ paddingTop: '20px',
+ paddingBottom: '20px',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ }, [
+ h('div', {
+ style: {
+ width: '0px',
+ position: 'relative',
+ bottom: '19px',
+ },
+ }, [
+ h('img', {
+ src: 'https://shapeshift.io/logo.png',
+ style: {
+ height: '35px',
+ width: '132px',
+ position: 'absolute',
+ clip: 'rect(0px,30px,34px,0px)',
+ },
+ }),
+ ]),
+
+ this.renderInfo(),
+ this.renderUtilComponents(),
+ ])
+}
+
+ShiftListItem.prototype.renderUtilComponents = function () {
+ var props = this.props
+ const { conversionRate, currentCurrency } = props
+
+ switch (props.response.status) {
+ case 'no_deposits':
+ return h('.flex-row', [
+ h(CopyButton, {
+ value: this.props.depositAddress,
+ }),
+ h(Tooltip, {
+ title: this.context.t('qrCode'),
+ }, [
+ h('i.fa.fa-qrcode.pointer.pop-hover', {
+ onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)),
+ style: {
+ margin: '5px',
+ marginLeft: '23px',
+ marginRight: '12px',
+ fontSize: '20px',
+ color: '#F7861C',
+ },
+ }),
+ ]),
+ ])
+ case 'received':
+ return h('.flex-row')
+
+ case 'complete':
+ return h('.flex-row', [
+ h(CopyButton, {
+ value: this.props.response.transaction,
+ }),
+ h(EthBalance, {
+ value: `${props.response.outgoingCoin}`,
+ conversionRate,
+ currentCurrency,
+ width: '55px',
+ shorten: true,
+ needsParse: false,
+ incoming: true,
+ style: {
+ fontSize: '15px',
+ color: '#01888C',
+ },
+ }),
+ ])
+
+ case 'failed':
+ return ''
+ default:
+ return ''
+ }
+}
+
+ShiftListItem.prototype.renderInfo = function () {
+ var props = this.props
+ switch (props.response.status) {
+ case 'no_deposits':
+ return h('.flex-column', {
+ style: {
+ overflow: 'hidden',
+ },
+ }, [
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, this.context.t('toETHviaShapeShift', [props.depositType])),
+ h('div', this.context.t('noDeposits')),
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, formatDate(props.time)),
+ ])
+ case 'received':
+ return h('.flex-column', {
+ style: {
+ width: '200px',
+ overflow: 'hidden',
+ },
+ }, [
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, this.context.t('toETHviaShapeShift', [props.depositType])),
+ h('div', this.context.t('conversionProgress')),
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, formatDate(props.time)),
+ ])
+ case 'complete':
+ var url = explorerLink(props.response.transaction, parseInt('1'))
+
+ return h('.flex-column.pointer', {
+ style: {
+ width: '200px',
+ overflow: 'hidden',
+ },
+ onClick: () => global.platform.openWindow({ url }),
+ }, [
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, this.context.t('fromShapeShift')),
+ h('div', formatDate(props.time)),
+ h('div', {
+ style: {
+ fontSize: 'x-small',
+ color: '#ABA9AA',
+ width: '100%',
+ },
+ }, addressSummary(props.response.transaction)),
+ ])
+
+ case 'failed':
+ return h('span.error', '(' + this.context.t('failed') + ')')
+ default:
+ return ''
+ }
+}
diff --git a/ui/app/components/sidebars/index.js b/ui/app/components/app/sidebars/index.js
index 732925f69..732925f69 100644
--- a/ui/app/components/sidebars/index.js
+++ b/ui/app/components/app/sidebars/index.js
diff --git a/ui/app/components/app/sidebars/index.scss b/ui/app/components/app/sidebars/index.scss
new file mode 100644
index 000000000..08181426f
--- /dev/null
+++ b/ui/app/components/app/sidebars/index.scss
@@ -0,0 +1,81 @@
+@import 'sidebar-content';
+
+.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);
+ }
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ left: 0%;
+ }
+}
+
+.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);
+}
diff --git a/ui/app/components/sidebars/sidebar-content.scss b/ui/app/components/app/sidebars/sidebar-content.scss
index ca6b0a458..ca6b0a458 100644
--- a/ui/app/components/sidebars/sidebar-content.scss
+++ b/ui/app/components/app/sidebars/sidebar-content.scss
diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/app/sidebars/sidebar.component.js
index b9e0f9e81..b9e0f9e81 100644
--- a/ui/app/components/sidebars/sidebar.component.js
+++ b/ui/app/components/app/sidebars/sidebar.component.js
diff --git a/ui/app/components/sidebars/sidebar.constants.js b/ui/app/components/app/sidebars/sidebar.constants.js
index 1613a8245..1613a8245 100644
--- a/ui/app/components/sidebars/sidebar.constants.js
+++ b/ui/app/components/app/sidebars/sidebar.constants.js
diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/tests/sidebars-component.test.js
index cee22aca8..cee22aca8 100644
--- a/ui/app/components/sidebars/tests/sidebars-component.test.js
+++ b/ui/app/components/app/sidebars/tests/sidebars-component.test.js
diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js
new file mode 100644
index 000000000..e47791b67
--- /dev/null
+++ b/ui/app/components/app/signature-request.js
@@ -0,0 +1,349 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+import Identicon from '../ui/identicon'
+const connect = require('react-redux').connect
+const ethUtil = require('ethereumjs-util')
+const classnames = require('classnames')
+const { compose } = require('recompose')
+const { withRouter } = require('react-router-dom')
+const { ObjectInspector } = require('react-inspector')
+
+import AccountDropdownMini from '../ui/account-dropdown-mini'
+
+const actions = require('../../store/actions')
+const { conversionUtil } = require('../../helpers/utils/conversion-util')
+
+const {
+ getSelectedAccount,
+ getCurrentAccountWithSendEtherInfo,
+ getSelectedAddress,
+ accountsWithSendEtherInfoSelector,
+ conversionRateSelector,
+} = require('../../selectors/selectors.js')
+
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import Button from '../ui/button'
+
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+
+function mapStateToProps (state) {
+ return {
+ balance: getSelectedAccount(state).balance,
+ selectedAccount: getCurrentAccountWithSendEtherInfo(state),
+ selectedAddress: getSelectedAddress(state),
+ requester: null,
+ requesterAddress: null,
+ accounts: accountsWithSendEtherInfoSelector(state),
+ conversionRate: conversionRateSelector(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ }
+}
+
+function mergeProps (stateProps, dispatchProps, ownProps) {
+ const {
+ signPersonalMessage,
+ signTypedMessage,
+ cancelPersonalMessage,
+ cancelTypedMessage,
+ signMessage,
+ cancelMessage,
+ txData,
+ } = ownProps
+
+ const { type } = txData
+
+ let cancel
+ let sign
+ if (type === 'personal_sign') {
+ cancel = cancelPersonalMessage
+ sign = signPersonalMessage
+ } else if (type === 'eth_signTypedData') {
+ cancel = cancelTypedMessage
+ sign = signTypedMessage
+ } else if (type === 'eth_sign') {
+ cancel = cancelMessage
+ sign = signMessage
+ }
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ txData,
+ cancel,
+ sign,
+ }
+}
+
+SignatureRequest.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(SignatureRequest)
+
+
+inherits(SignatureRequest, Component)
+function SignatureRequest (props) {
+ Component.call(this)
+
+ this.state = {
+ selectedAccount: props.selectedAccount,
+ }
+}
+
+SignatureRequest.prototype.componentDidMount = function () {
+ const { clearConfirmTransaction, cancel } = this.props
+ const { metricsEvent } = this.context
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
+ window.onbeforeunload = event => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Sign Request',
+ name: 'Cancel Sig Request Via Notification Close',
+ },
+ })
+ clearConfirmTransaction()
+ cancel(event)
+ }
+ }
+}
+
+SignatureRequest.prototype.renderHeader = function () {
+ return h('div.request-signature__header', [
+
+ h('div.request-signature__header-background'),
+
+ h('div.request-signature__header__text', this.context.t('sigRequest')),
+
+ h('div.request-signature__header__tip-container', [
+ h('div.request-signature__header__tip'),
+ ]),
+
+ ])
+}
+
+SignatureRequest.prototype.renderAccountDropdown = function () {
+ const { selectedAccount } = this.state
+
+ const {
+ accounts,
+ } = this.props
+
+ return h('div.request-signature__account', [
+
+ h('div.request-signature__account-text', [this.context.t('account') + ':']),
+
+ h(AccountDropdownMini, {
+ selectedAccount,
+ accounts,
+ disabled: true,
+ }),
+
+ ])
+}
+
+SignatureRequest.prototype.renderBalance = function () {
+ const { balance, conversionRate } = this.props
+
+ const balanceInEther = conversionUtil(balance, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ return h('div.request-signature__balance', [
+
+ h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
+
+ h('div.request-signature__balance-value', `${balanceInEther} ETH`),
+
+ ])
+}
+
+SignatureRequest.prototype.renderAccountInfo = function () {
+ return h('div.request-signature__account-info', [
+
+ this.renderAccountDropdown(),
+
+ this.renderRequestIcon(),
+
+ this.renderBalance(),
+
+ ])
+}
+
+SignatureRequest.prototype.renderRequestIcon = function () {
+ const { requesterAddress } = this.props
+
+ return h('div.request-signature__request-icon', [
+ h(Identicon, {
+ diameter: 40,
+ address: requesterAddress,
+ }),
+ ])
+}
+
+SignatureRequest.prototype.renderRequestInfo = function () {
+ return h('div.request-signature__request-info', [
+
+ h('div.request-signature__headline', [
+ this.context.t('yourSigRequested'),
+ ]),
+
+ ])
+}
+
+SignatureRequest.prototype.msgHexToText = function (hex) {
+ try {
+ const stripped = ethUtil.stripHexPrefix(hex)
+ const buff = Buffer.from(stripped, 'hex')
+ return buff.length === 32 ? hex : buff.toString('utf8')
+ } catch (e) {
+ return 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, version } } = txData
+
+ if (type === 'personal_sign') {
+ rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }]
+ } else if (type === 'eth_signTypedData') {
+ rows = data
+ } else if (type === 'eth_sign') {
+ rows = [{ name: this.context.t('message'), value: data }]
+ notice = [this.context.t('signNotice'),
+ h('span.request-signature__help-link', {
+ onClick: () => {
+ global.platform.openWindow({
+ url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751',
+ })
+ },
+ }, this.context.t('learnMore'))]
+ }
+
+ return h('div.request-signature__body', {}, [
+
+ this.renderAccountInfo(),
+
+ this.renderRequestInfo(),
+
+ h('div.request-signature__notice', {
+ className: classnames({
+ 'request-signature__notice': type === 'personal_sign' || type === 'eth_signTypedData',
+ 'request-signature__warning': type === 'eth_sign',
+ }),
+ }, [notice]),
+
+ h('div.request-signature__rows', type === 'eth_signTypedData' && version === 'V3' ?
+ this.renderTypedDataV3(data) :
+ rows.map(({ name, value }) => {
+ if (typeof value === 'boolean') {
+ value = value.toString()
+ }
+ return h('div.request-signature__row', [
+ h('div.request-signature__row-title', [`${name}:`]),
+ h('div.request-signature__row-value', value),
+ ])
+ }),
+ ),
+ ])
+}
+
+SignatureRequest.prototype.renderFooter = function () {
+ const { cancel, sign } = this.props
+
+ return h('div.request-signature__footer', [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'request-signature__footer__cancel-button',
+ onClick: event => {
+ cancel(event).then(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Sign Request',
+ name: 'Cancel',
+ },
+ })
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
+ },
+ }, this.context.t('cancel')),
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'request-signature__footer__sign-button',
+ onClick: event => {
+ sign(event).then(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Sign Request',
+ name: 'Confirm',
+ },
+ })
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
+ },
+ }, this.context.t('sign')),
+ ])
+}
+
+SignatureRequest.prototype.render = function () {
+ return (
+
+ h('div.request-signature__container', [
+
+ this.renderHeader(),
+
+ this.renderBody(),
+
+ this.renderFooter(),
+
+ ])
+
+ )
+
+}
diff --git a/ui/app/components/app/tab-bar.js b/ui/app/components/app/tab-bar.js
new file mode 100644
index 000000000..43923989a
--- /dev/null
+++ b/ui/app/components/app/tab-bar.js
@@ -0,0 +1,37 @@
+import React, { Component } from 'react'
+const PropTypes = require('prop-types')
+const classnames = require('classnames')
+
+class TabBar extends Component {
+ render () {
+ const { tabs = [], onSelect, isActive } = this.props
+
+ return (
+ <div className="tab-bar">
+ {tabs.map(({ key, content, description }) => (
+ <div
+ key={key}
+ className={classnames('tab-bar__tab pointer', {
+ 'tab-bar__tab--active': isActive(key, content),
+ })}
+ onClick={() => onSelect(key)}
+ >
+ <div className="tab-bar__tab__content">
+ <div className="tab-bar__tab__content__title">{content}</div>
+ <div className="tab-bar__tab__content__description">{description}</div>
+ </div>
+ <div className="tab-bar__tab__caret" />
+ </div>
+ ))}
+ </div>
+ )
+ }
+}
+
+TabBar.propTypes = {
+ isActive: PropTypes.func.isRequired,
+ tabs: PropTypes.array,
+ onSelect: PropTypes.func,
+}
+
+module.exports = TabBar
diff --git a/ui/app/components/app/token-cell.js b/ui/app/components/app/token-cell.js
new file mode 100644
index 000000000..cef809e8a
--- /dev/null
+++ b/ui/app/components/app/token-cell.js
@@ -0,0 +1,177 @@
+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
+import Identicon from '../ui/identicon'
+const prefixForNetwork = require('../../../lib/etherscan-prefix-for-network')
+const selectors = require('../../selectors/selectors')
+const actions = require('../../store/actions')
+const { conversionUtil, multiplyCurrencies } = require('../../helpers/utils/conversion-util')
+
+const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js')
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ currentCurrency: state.metamask.currentCurrency,
+ selectedTokenAddress: state.metamask.selectedTokenAddress,
+ userAddress: selectors.getSelectedAddress(state),
+ contractExchangeRates: state.metamask.contractExchangeRates,
+ conversionRate: state.metamask.conversionRate,
+ sidebarOpen: state.appState.sidebar.isOpen,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setSelectedToken: address => dispatch(actions.setSelectedToken(address)),
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell)
+
+inherits(TokenCell, Component)
+function TokenCell () {
+ Component.call(this)
+
+ this.state = {
+ tokenMenuOpen: false,
+ }
+}
+
+TokenCell.contextTypes = {
+ metricsEvent: PropTypes.func,
+}
+
+TokenCell.prototype.render = function () {
+ const { tokenMenuOpen } = this.state
+ const props = this.props
+ const {
+ address,
+ symbol,
+ string,
+ network,
+ setSelectedToken,
+ selectedTokenAddress,
+ contractExchangeRates,
+ conversionRate,
+ hideSidebar,
+ sidebarOpen,
+ currentCurrency,
+ // userAddress,
+ image,
+ } = props
+ let currentTokenToFiatRate
+ let currentTokenInFiat
+ let formattedFiat = ''
+
+ if (contractExchangeRates[address]) {
+ currentTokenToFiatRate = multiplyCurrencies(
+ contractExchangeRates[address],
+ conversionRate
+ )
+ currentTokenInFiat = conversionUtil(string, {
+ fromNumericBase: 'dec',
+ fromCurrency: symbol,
+ toCurrency: currentCurrency.toUpperCase(),
+ numberOfDecimals: 2,
+ conversionRate: currentTokenToFiatRate,
+ })
+ formattedFiat = currentTokenInFiat.toString() === '0'
+ ? ''
+ : `${currentTokenInFiat} ${currentCurrency.toUpperCase()}`
+ }
+
+ const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
+
+ return (
+ h('div.token-list-item', {
+ className: `token-list-item ${selectedTokenAddress === address ? 'token-list-item--active' : ''}`,
+ // style: { cursor: network === '1' ? 'pointer' : 'default' },
+ // onClick: this.view.bind(this, address, userAddress, network),
+ onClick: () => {
+ setSelectedToken(address)
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Token Menu',
+ name: 'Clicked Token',
+ },
+ })
+ selectedTokenAddress !== address && sidebarOpen && hideSidebar()
+ },
+ }, [
+
+ h(Identicon, {
+ className: 'token-list-item__identicon',
+ diameter: 50,
+ address,
+ network,
+ image,
+ }),
+
+ h('div.token-list-item__balance-ellipsis', null, [
+ h('div.token-list-item__balance-wrapper', null, [
+ h('div.token-list-item__token-balance', `${string || 0}`),
+ h('div.token-list-item__token-symbol', symbol),
+ showFiat && h('div.token-list-item__fiat-amount', {
+ style: {},
+ }, formattedFiat),
+ ]),
+
+ h('i.fa.fa-ellipsis-h.fa-lg.token-list-item__ellipsis.cursor-pointer', {
+ onClick: (e) => {
+ e.stopPropagation()
+ this.setState({ tokenMenuOpen: true })
+ },
+ }),
+
+ ]),
+
+
+ tokenMenuOpen && h(TokenMenuDropdown, {
+ onClose: () => this.setState({ tokenMenuOpen: false }),
+ token: { symbol, address },
+ }),
+
+ /*
+ h('button', {
+ onClick: this.send.bind(this, address),
+ }, 'SEND'),
+ */
+
+ ])
+ )
+}
+
+TokenCell.prototype.send = function (address, event) {
+ event.preventDefault()
+ event.stopPropagation()
+ const url = tokenFactoryFor(address)
+ if (url) {
+ navigateTo(url)
+ }
+}
+
+TokenCell.prototype.view = function (address, userAddress, network, event) {
+ const url = etherscanLinkFor(address, userAddress, network)
+ if (url) {
+ navigateTo(url)
+ }
+}
+
+function navigateTo (url) {
+ global.platform.openWindow({ url })
+}
+
+function etherscanLinkFor (tokenAddress, address, network) {
+ const prefix = prefixForNetwork(network)
+ return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
+}
+
+function tokenFactoryFor (tokenAddress) {
+ return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
+}
+
diff --git a/ui/app/components/app/token-list.js b/ui/app/components/app/token-list.js
new file mode 100644
index 000000000..2188e7020
--- /dev/null
+++ b/ui/app/components/app/token-list.js
@@ -0,0 +1,188 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const TokenTracker = require('eth-token-tracker')
+const TokenCell = require('./token-cell.js')
+const connect = require('react-redux').connect
+const selectors = require('../../selectors/selectors')
+const log = require('loglevel')
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ tokens: state.metamask.tokens,
+ userAddress: selectors.getSelectedAddress(state),
+ assetImages: state.metamask.assetImages,
+ }
+}
+
+const defaultTokens = []
+const contracts = require('eth-contract-metadata')
+for (const address in contracts) {
+ const contract = contracts[address]
+ if (contract.erc20) {
+ contract.address = address
+ defaultTokens.push(contract)
+ }
+}
+
+TokenList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps)(TokenList)
+
+
+inherits(TokenList, Component)
+function TokenList () {
+ this.state = {
+ tokens: [],
+ isLoading: true,
+ network: null,
+ }
+ Component.call(this)
+}
+
+TokenList.prototype.render = function () {
+ const { userAddress, assetImages } = this.props
+ const state = this.state
+ const { tokens, isLoading, error } = state
+ if (isLoading) {
+ return this.message(this.context.t('loadingTokens'))
+ }
+
+ if (error) {
+ log.error(error)
+ return h('.hotFix', {
+ style: {
+ padding: '80px',
+ },
+ }, [
+ this.context.t('troubleTokenBalances'),
+ h('span.hotFix', {
+ style: {
+ color: 'rgba(247, 134, 28, 1)',
+ cursor: 'pointer',
+ },
+ onClick: () => {
+ global.platform.openWindow({
+ url: `https://ethplorer.io/address/${userAddress}`,
+ })
+ },
+ }, this.context.t('here')),
+ ])
+ }
+
+ return h('div', tokens.map((tokenData) => {
+ tokenData.image = assetImages[tokenData.address]
+ return h(TokenCell, tokenData)
+ }))
+
+}
+
+TokenList.prototype.message = function (body) {
+ return h('div', {
+ style: {
+ display: 'flex',
+ height: '250px',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '30px',
+ },
+ }, body)
+}
+
+TokenList.prototype.componentDidMount = function () {
+ this.createFreshTokenTracker()
+}
+
+TokenList.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 } = this.props
+
+ this.tracker = new TokenTracker({
+ userAddress,
+ provider: global.ethereumProvider,
+ tokens: this.props.tokens,
+ pollingInterval: 8000,
+ })
+
+
+ // Set up listener instances for cleaning up
+ this.balanceUpdater = this.updateBalances.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.updateBalances(this.tracker.serialize())
+ })
+ .catch((reason) => {
+ log.error(`Problem updating balances`, reason)
+ this.setState({ isLoading: false })
+ })
+}
+
+TokenList.prototype.componentDidUpdate = function (prevProps) {
+ const {
+ network: oldNet,
+ userAddress: oldAddress,
+ tokens,
+ } = prevProps
+ const {
+ network: newNet,
+ userAddress: newAddress,
+ tokens: newTokens,
+ } = this.props
+
+ const isLoading = newNet === 'loading'
+ const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress
+ const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet
+ const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork
+
+ const oldTokensLength = tokens ? tokens.length : 0
+ const tokensLengthUnchanged = oldTokensLength === newTokens.length
+
+ if (tokensLengthUnchanged && shouldUpdateTokens) return
+
+ this.setState({ isLoading: true })
+ this.createFreshTokenTracker()
+}
+
+TokenList.prototype.updateBalances = function (tokens) {
+ if (!this.tracker.running) {
+ return
+ }
+ this.setState({ tokens, isLoading: false })
+}
+
+TokenList.prototype.componentWillUnmount = function () {
+ if (!this.tracker) return
+ this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
+}
+
+// function uniqueMergeTokens (tokensA, tokensB = []) {
+// const uniqueAddresses = []
+// const result = []
+// tokensA.concat(tokensB).forEach((token) => {
+// const normal = normalizeAddress(token.address)
+// if (!uniqueAddresses.includes(normal)) {
+// uniqueAddresses.push(normal)
+// result.push(token)
+// }
+// })
+// return result
+// }
diff --git a/ui/app/components/transaction-action/index.js b/ui/app/components/app/transaction-action/index.js
index a6e9097f1..a6e9097f1 100644
--- a/ui/app/components/transaction-action/index.js
+++ b/ui/app/components/app/transaction-action/index.js
diff --git a/ui/app/components/transaction-action/tests/transaction-action.component.test.js b/ui/app/components/app/transaction-action/tests/transaction-action.component.test.js
index b22a9db39..b22a9db39 100644
--- a/ui/app/components/transaction-action/tests/transaction-action.component.test.js
+++ b/ui/app/components/app/transaction-action/tests/transaction-action.component.test.js
diff --git a/ui/app/components/app/transaction-action/transaction-action.component.js b/ui/app/components/app/transaction-action/transaction-action.component.js
new file mode 100644
index 000000000..4a5efdaae
--- /dev/null
+++ b/ui/app/components/app/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/utils/transactions.util'
+import { camelCaseToCapitalize } from '../../../helpers/utils/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/app/transaction-activity-log/index.js
index a33da15a3..a33da15a3 100644
--- a/ui/app/components/transaction-activity-log/index.js
+++ b/ui/app/components/app/transaction-activity-log/index.js
diff --git a/ui/app/components/transaction-activity-log/index.scss b/ui/app/components/app/transaction-activity-log/index.scss
index 00c17e6aa..00c17e6aa 100644
--- a/ui/app/components/transaction-activity-log/index.scss
+++ b/ui/app/components/app/transaction-activity-log/index.scss
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js
index a2946e53d..a2946e53d 100644
--- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
+++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js
index a7c35f51e..a7c35f51e 100644
--- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
+++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js
index d014b8886..d014b8886 100644
--- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
+++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js
index 86b12360a..86b12360a 100644
--- a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js
index 871716002..871716002 100644
--- a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js
diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js
new file mode 100644
index 000000000..de4d29750
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js
@@ -0,0 +1,131 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../../helpers/utils/conversions.util'
+import { formatDate } from '../../../helpers/utils/util'
+import TransactionActivityLogIcon from './transaction-activity-log-icon'
+import { CONFIRMED_STATUS } from './transaction-activity-log.constants'
+import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network'
+
+export default class TransactionActivityLog extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ activities: PropTypes.array,
+ className: PropTypes.string,
+ conversionRate: PropTypes.number,
+ inlineRetryIndex: PropTypes.number,
+ inlineCancelIndex: PropTypes.number,
+ nativeCurrency: PropTypes.string,
+ onCancel: PropTypes.func,
+ onRetry: PropTypes.func,
+ primaryTransaction: PropTypes.object,
+ }
+
+ handleActivityClick = hash => {
+ const { primaryTransaction } = this.props
+ const { metamaskNetworkId } = primaryTransaction
+
+ const prefix = prefixForNetwork(metamaskNetworkId)
+ const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
+
+ global.platform.openWindow({ url: etherscanUrl })
+ }
+
+ renderInlineRetry (index, activity) {
+ const { t } = this.context
+ const { inlineRetryIndex, primaryTransaction = {}, onRetry } = this.props
+ const { status } = primaryTransaction
+ const { id } = activity
+
+ return status !== CONFIRMED_STATUS && index === inlineRetryIndex
+ ? (
+ <div
+ className="transaction-activity-log__action-link"
+ onClick={() => onRetry(id)}
+ >
+ { t('speedUpTransaction') }
+ </div>
+ ) : null
+ }
+
+ renderInlineCancel (index, activity) {
+ const { t } = this.context
+ const { inlineCancelIndex, primaryTransaction = {}, onCancel } = this.props
+ const { status } = primaryTransaction
+ const { id } = activity
+
+ return status !== CONFIRMED_STATUS && index === inlineCancelIndex
+ ? (
+ <div
+ className="transaction-activity-log__action-link"
+ onClick={() => onCancel(id)}
+ >
+ { t('speedUpCancellation') }
+ </div>
+ ) : null
+ }
+
+ renderActivity (activity, index) {
+ const { conversionRate, nativeCurrency } = this.props
+ const { eventKey, value, timestamp, hash } = activity
+ const ethValue = index === 0
+ ? `${getValueFromWeiHex({
+ value,
+ fromCurrency: nativeCurrency,
+ toCurrency: nativeCurrency,
+ conversionRate,
+ numberOfDecimals: 6,
+ })} ${nativeCurrency}`
+ : getEthConversionFromWeiHex({
+ value,
+ fromCurrency: nativeCurrency,
+ conversionRate,
+ numberOfDecimals: 3,
+ })
+ const formattedTimestamp = formatDate(timestamp, 'T \'on\' M/d/y')
+ const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
+
+ return (
+ <div
+ key={index}
+ className="transaction-activity-log__activity"
+ >
+ <TransactionActivityLogIcon
+ className="transaction-activity-log__activity-icon"
+ eventKey={eventKey}
+ />
+ <div className="transaction-activity-log__entry-container">
+ <div
+ className="transaction-activity-log__activity-text"
+ title={activityText}
+ onClick={() => this.handleActivityClick(hash)}
+ >
+ { activityText }
+ </div>
+ { this.renderInlineRetry(index, activity) }
+ { this.renderInlineCancel(index, activity) }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { className, activities } = this.props
+
+ return (
+ <div className={classnames('transaction-activity-log', className)}>
+ <div className="transaction-activity-log__title">
+ { t('activityLog') }
+ </div>
+ <div className="transaction-activity-log__activities-container">
+ { activities.map((activity, index) => this.renderActivity(activity, index)) }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.constants.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js
index 72e63d85c..72e63d85c 100644
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.constants.js
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js
diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js
new file mode 100644
index 000000000..11b20f245
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js
@@ -0,0 +1,44 @@
+import { connect } from 'react-redux'
+import R from 'ramda'
+import TransactionActivityLog from './transaction-activity-log.component'
+import { conversionRateSelector, getNativeCurrency } from '../../../selectors/selectors'
+import { combineTransactionHistories } from './transaction-activity-log.util'
+import {
+ TRANSACTION_RESUBMITTED_EVENT,
+ TRANSACTION_CANCEL_ATTEMPTED_EVENT,
+} from './transaction-activity-log.constants'
+
+const matchesEventKey = matchEventKey => ({ eventKey }) => eventKey === matchEventKey
+
+const mapStateToProps = state => {
+ return {
+ conversionRate: conversionRateSelector(state),
+ nativeCurrency: getNativeCurrency(state),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const {
+ transactionGroup: {
+ transactions = [],
+ primaryTransaction,
+ } = {},
+ ...restOwnProps
+ } = ownProps
+
+ const activities = combineTransactionHistories(transactions)
+ const inlineRetryIndex = R.findLastIndex(matchesEventKey(TRANSACTION_RESUBMITTED_EVENT))(activities)
+ const inlineCancelIndex = R.findLastIndex(matchesEventKey(TRANSACTION_CANCEL_ATTEMPTED_EVENT))(activities)
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...restOwnProps,
+ activities,
+ inlineRetryIndex,
+ inlineCancelIndex,
+ primaryTransaction,
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(TransactionActivityLog)
diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js
new file mode 100644
index 000000000..b74513879
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js
@@ -0,0 +1,233 @@
+import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'
+
+// path constants
+const STATUS_PATH = '/status'
+const GAS_PRICE_PATH = '/txParams/gasPrice'
+const GAS_LIMIT_PATH = '/txParams/gas'
+
+// op constants
+const REPLACE_OP = 'replace'
+
+import {
+ // event constants
+ TRANSACTION_CREATED_EVENT,
+ TRANSACTION_SUBMITTED_EVENT,
+ TRANSACTION_RESUBMITTED_EVENT,
+ TRANSACTION_CONFIRMED_EVENT,
+ TRANSACTION_DROPPED_EVENT,
+ TRANSACTION_UPDATED_EVENT,
+ TRANSACTION_ERRORED_EVENT,
+ TRANSACTION_CANCEL_ATTEMPTED_EVENT,
+ TRANSACTION_CANCEL_SUCCESS_EVENT,
+ // status constants
+ SUBMITTED_STATUS,
+ CONFIRMED_STATUS,
+ DROPPED_STATUS,
+} from './transaction-activity-log.constants'
+
+import {
+ TRANSACTION_TYPE_CANCEL,
+ TRANSACTION_TYPE_RETRY,
+} from '../../../../../app/scripts/controllers/transactions/enums'
+
+const eventPathsHash = {
+ [STATUS_PATH]: true,
+ [GAS_PRICE_PATH]: true,
+ [GAS_LIMIT_PATH]: true,
+}
+
+const statusHash = {
+ [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
+ [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
+ [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
+}
+
+/**
+ * @name getActivities
+ * @param {Object} transaction - txMeta object
+ * @param {boolean} isFirstTransaction - True if the transaction is the first created transaction
+ * in the list of transactions with the same nonce. If so, we use this transaction to create the
+ * transactionCreated activity.
+ * @returns {Array}
+ */
+export function getActivities (transaction, isFirstTransaction = false) {
+ const {
+ id,
+ hash,
+ history = [],
+ txParams: { gas: paramsGasLimit, gasPrice: paramsGasPrice},
+ xReceipt: { status } = {},
+ type,
+ } = transaction
+
+ let cachedGasLimit = '0x0'
+ let cachedGasPrice = '0x0'
+
+ const historyActivities = history.reduce((acc, base, index) => {
+ // First history item should be transaction creation
+ if (index === 0 && !Array.isArray(base) && base.txParams) {
+ const { time: timestamp, txParams: { value, gas = '0x0', gasPrice = '0x0' } = {} } = base
+ // The cached gas limit and gas price are used to display the gas fee in the activity log. We
+ // need to cache these values because the status update history events don't provide us with
+ // the latest gas limit and gas price.
+ cachedGasLimit = gas
+ cachedGasPrice = gasPrice
+
+ if (isFirstTransaction) {
+ return acc.concat({
+ id,
+ hash,
+ eventKey: TRANSACTION_CREATED_EVENT,
+ timestamp,
+ value,
+ })
+ }
+ // An entry in the history may be an array of more sub-entries.
+ } else if (Array.isArray(base)) {
+ const events = []
+
+ base.forEach(entry => {
+ const { op, path, value, timestamp: entryTimestamp } = entry
+ // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a
+ // timestamp, the first sub-entry in a history entry should.
+ const timestamp = entryTimestamp || base[0] && base[0].timestamp
+
+ if (path in eventPathsHash && op === REPLACE_OP) {
+ switch (path) {
+ case STATUS_PATH: {
+ const gasFee = cachedGasLimit === '0x0' && cachedGasPrice === '0x0'
+ ? getHexGasTotal({ gasLimit: paramsGasLimit, gasPrice: paramsGasPrice })
+ : getHexGasTotal({ gasLimit: cachedGasLimit, gasPrice: cachedGasPrice })
+
+ if (value in statusHash) {
+ let eventKey = statusHash[value]
+
+ // If the status is 'submitted', we need to determine whether the event is a
+ // transaction retry or a cancellation attempt.
+ if (value === SUBMITTED_STATUS) {
+ if (type === TRANSACTION_TYPE_RETRY) {
+ eventKey = TRANSACTION_RESUBMITTED_EVENT
+ } else if (type === TRANSACTION_TYPE_CANCEL) {
+ eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT
+ }
+ } else if (value === CONFIRMED_STATUS) {
+ if (type === TRANSACTION_TYPE_CANCEL) {
+ eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT
+ }
+ }
+
+ events.push({
+ id,
+ hash,
+ eventKey,
+ timestamp,
+ value: gasFee,
+ })
+ }
+
+ break
+ }
+
+ // If the gas price or gas limit has been changed, we update the gasFee of the
+ // previously submitted event. These events happen when the gas limit and gas price is
+ // changed at the confirm screen.
+ case GAS_PRICE_PATH:
+ case GAS_LIMIT_PATH: {
+ const lastEvent = events[events.length - 1] || {}
+ const { lastEventKey } = lastEvent
+
+ if (path === GAS_LIMIT_PATH) {
+ cachedGasLimit = value
+ } else if (path === GAS_PRICE_PATH) {
+ cachedGasPrice = value
+ }
+
+ if (lastEventKey === TRANSACTION_SUBMITTED_EVENT ||
+ lastEventKey === TRANSACTION_RESUBMITTED_EVENT) {
+ lastEvent.value = getHexGasTotal({
+ gasLimit: cachedGasLimit,
+ gasPrice: cachedGasPrice,
+ })
+ }
+
+ break
+ }
+
+ default: {
+ events.push({
+ id,
+ hash,
+ eventKey: TRANSACTION_UPDATED_EVENT,
+ timestamp,
+ })
+ }
+ }
+ }
+ })
+
+ return acc.concat(events)
+ }
+
+ return acc
+ }, [])
+
+ // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction,
+ // so we add an error entry to the Activity Log.
+ return status === '0x0'
+ ? historyActivities.concat({ id, hash, eventKey: TRANSACTION_ERRORED_EVENT })
+ : historyActivities
+}
+
+/**
+ * @description Removes "Transaction dropped" activities from a list of sorted activities if one of
+ * the transactions has been confirmed. Typically, if multiple transactions have the same nonce,
+ * once one transaction is confirmed, the rest are dropped. In this case, we don't want to show
+ * multiple "Transaction dropped" activities, and instead want to show a single "Transaction
+ * confirmed".
+ * @param {Array} activities - List of sorted activities generated from the getActivities function.
+ * @returns {Array}
+ */
+function filterSortedActivities (activities) {
+ const filteredActivities = []
+ const hasConfirmedActivity = Boolean(activities.find(({ eventKey }) => (
+ eventKey === TRANSACTION_CONFIRMED_EVENT || eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT
+ )))
+ let addedDroppedActivity = false
+
+ activities.forEach(activity => {
+ if (activity.eventKey === TRANSACTION_DROPPED_EVENT) {
+ if (!hasConfirmedActivity && !addedDroppedActivity) {
+ filteredActivities.push(activity)
+ addedDroppedActivity = true
+ }
+ } else {
+ filteredActivities.push(activity)
+ }
+ })
+
+ return filteredActivities
+}
+
+/**
+ * Combines the histories of an array of transactions into a single array.
+ * @param {Array} transactions - Array of txMeta transaction objects.
+ * @returns {Array}
+ */
+export function combineTransactionHistories (transactions = []) {
+ if (!transactions.length) {
+ return []
+ }
+
+ const activities = []
+
+ transactions.forEach((transaction, index) => {
+ // The first transaction should be the transaction with the earliest submittedTime. We show the
+ // 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted'
+ // instead.
+ const transactionActivities = getActivities(transaction, index === 0)
+ activities.push(...transactionActivities)
+ })
+
+ const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp)
+ return filterSortedActivities(sortedActivities)
+}
diff --git a/ui/app/components/transaction-breakdown/index.js b/ui/app/components/app/transaction-breakdown/index.js
index 4a5b52663..4a5b52663 100644
--- a/ui/app/components/transaction-breakdown/index.js
+++ b/ui/app/components/app/transaction-breakdown/index.js
diff --git a/ui/app/components/app/transaction-breakdown/index.scss b/ui/app/components/app/transaction-breakdown/index.scss
new file mode 100644
index 000000000..c8144eac2
--- /dev/null
+++ b/ui/app/components/app/transaction-breakdown/index.scss
@@ -0,0 +1,24 @@
+@import 'transaction-breakdown-row/index';
+
+.transaction-breakdown {
+ &__title {
+ border-bottom: 1px solid #d8d8d8;
+ padding-bottom: 4px;
+ text-transform: capitalize;
+ }
+
+ &__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/app/transaction-breakdown/tests/transaction-breakdown.component.test.js
index 4512b84f0..4512b84f0 100644
--- a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
+++ b/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js
index 557bf75fb..557bf75fb 100644
--- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
+++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss
index 8c73be1a6..8c73be1a6 100644
--- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
+++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss
diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
new file mode 100644
index 000000000..82e40fce2
--- /dev/null
+++ b/ui/app/components/app/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 '../../../../ui/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/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
index c11ff8efa..c11ff8efa 100644
--- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
+++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js
new file mode 100644
index 000000000..5642e0fa5
--- /dev/null
+++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js
@@ -0,0 +1,106 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import TransactionBreakdownRow from './transaction-breakdown-row'
+import CurrencyDisplay from '../../ui/currency-display'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
+import HexToDecimal from '../../ui/hex-to-decimal'
+import { GWEI, PRIMARY, SECONDARY } from '../../../helpers/constants/common'
+
+export default class TransactionBreakdown extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ transaction: PropTypes.object,
+ className: PropTypes.string,
+ nativeCurrency: PropTypes.string.isRequired,
+ showFiat: PropTypes.bool,
+ gas: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ gasPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ gasUsed: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ totalInHex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }
+
+ static defaultProps = {
+ transaction: {},
+ showFiat: true,
+ }
+
+ render () {
+ const { t } = this.context
+ const { gas, gasPrice, value, className, nativeCurrency, showFiat, totalInHex, gasUsed } = this.props
+
+ return (
+ <div className={classnames('transaction-breakdown', className)}>
+ <div className="transaction-breakdown__title">
+ { t('transaction') }
+ </div>
+ <TransactionBreakdownRow title={t('amount')}>
+ <UserPreferencedCurrencyDisplay
+ className="transaction-breakdown__value"
+ type={PRIMARY}
+ value={value}
+ />
+ </TransactionBreakdownRow>
+ <TransactionBreakdownRow
+ title={`${t('gasLimit')} (${t('units')})`}
+ className="transaction-breakdown__row-title"
+ >
+ {typeof gas !== 'undefined'
+ ? <HexToDecimal
+ className="transaction-breakdown__value"
+ value={gas}
+ />
+ : '?'
+ }
+ </TransactionBreakdownRow>
+ {
+ typeof gasUsed === 'string' && (
+ <TransactionBreakdownRow
+ title={`${t('gasUsed')} (${t('units')})`}
+ className="transaction-breakdown__row-title"
+ >
+ <HexToDecimal
+ className="transaction-breakdown__value"
+ value={gasUsed}
+ />
+ </TransactionBreakdownRow>
+ )
+ }
+ <TransactionBreakdownRow title={t('gasPrice')}>
+ {typeof gasPrice !== 'undefined'
+ ? <CurrencyDisplay
+ className="transaction-breakdown__value"
+ currency={nativeCurrency}
+ denomination={GWEI}
+ value={gasPrice}
+ hideLabel
+ />
+ : '?'
+ }
+ </TransactionBreakdownRow>
+ <TransactionBreakdownRow title={t('total')}>
+ <div>
+ <UserPreferencedCurrencyDisplay
+ className="transaction-breakdown__value transaction-breakdown__value--eth-total"
+ type={PRIMARY}
+ value={totalInHex}
+ />
+ {
+ showFiat && (
+ <UserPreferencedCurrencyDisplay
+ className="transaction-breakdown__value"
+ type={SECONDARY}
+ value={totalInHex}
+ />
+ )
+ }
+ </div>
+ </TransactionBreakdownRow>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js
new file mode 100644
index 000000000..82f377358
--- /dev/null
+++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux'
+import TransactionBreakdown from './transaction-breakdown.component'
+import {getIsMainnet, getNativeCurrency, preferencesSelector} from '../../../selectors/selectors'
+import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'
+import { sumHexes } from '../../../helpers/utils/transactions.util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { transaction } = ownProps
+ const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+
+ const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas
+
+ const hexGasTotal = gasLimit && gasPrice && getHexGasTotal({ gasLimit, gasPrice }) || '0x0'
+ const totalInHex = sumHexes(hexGasTotal, value)
+
+ return {
+ nativeCurrency: getNativeCurrency(state),
+ showFiat: (isMainnet || !!showFiatInTestnets),
+ totalInHex,
+ gas,
+ gasPrice,
+ value,
+ gasUsed,
+ }
+}
+
+export default connect(mapStateToProps)(TransactionBreakdown)
diff --git a/ui/app/components/transaction-list-item-details/index.js b/ui/app/components/app/transaction-list-item-details/index.js
index 0e878d032..0e878d032 100644
--- a/ui/app/components/transaction-list-item-details/index.js
+++ b/ui/app/components/app/transaction-list-item-details/index.js
diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/app/transaction-list-item-details/index.scss
index 7cb253e4e..7cb253e4e 100644
--- a/ui/app/components/transaction-list-item-details/index.scss
+++ b/ui/app/components/app/transaction-list-item-details/index.scss
diff --git a/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
new file mode 100644
index 000000000..c4e118b01
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
@@ -0,0 +1,81 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionListItemDetails from '../transaction-list-item-details.component'
+import Button from '../../../ui/button'
+import SenderToRecipient from '../../../ui/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 transactionGroup = {
+ transactions: [transaction],
+ primaryTransaction: transaction,
+ initialTransaction: transaction,
+ }
+
+ const wrapper = shallow(
+ <TransactionListItemDetails
+ transactionGroup={transactionGroup}
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-list-item-details'))
+ assert.equal(wrapper.find(Button).length, 2)
+ 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 transactionGroup = {
+ transactions: [transaction],
+ primaryTransaction: transaction,
+ initialTransaction: transaction,
+ nonce: '0xa4',
+ hasRetried: false,
+ hasCancelled: false,
+ }
+
+ const wrapper = shallow(
+ <TransactionListItemDetails
+ transactionGroup={transactionGroup}
+ showRetry={true}
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-list-item-details'))
+ assert.equal(wrapper.find(Button).length, 3)
+ })
+})
diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js
new file mode 100644
index 000000000..4a3b04998
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js
@@ -0,0 +1,215 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import copyToClipboard from 'copy-to-clipboard'
+import SenderToRecipient from '../../ui/sender-to-recipient'
+import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'
+import TransactionActivityLog from '../transaction-activity-log'
+import TransactionBreakdown from '../transaction-breakdown'
+import Button from '../../ui/button'
+import Tooltip from '../../ui/tooltip'
+import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network'
+
+export default class TransactionListItemDetails extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ onCancel: PropTypes.func,
+ onRetry: PropTypes.func,
+ showCancel: PropTypes.bool,
+ showRetry: PropTypes.bool,
+ cancelDisabled: PropTypes.bool,
+ transactionGroup: PropTypes.object,
+ }
+
+ state = {
+ justCopied: false,
+ cancelDisabled: false,
+ }
+
+ handleEtherscanClick = () => {
+ const { transactionGroup: { primaryTransaction } } = this.props
+ const { hash, metamaskNetworkId } = primaryTransaction
+
+ const prefix = prefixForNetwork(metamaskNetworkId)
+ const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Activity Log',
+ name: 'Clicked "View on Etherscan"',
+ },
+ })
+
+ global.platform.openWindow({ url: etherscanUrl })
+ }
+
+ handleCancel = event => {
+ const { transactionGroup: { initialTransaction: { id } = {} } = {}, onCancel } = this.props
+
+ event.stopPropagation()
+ onCancel(id)
+ }
+
+ handleRetry = event => {
+ const { transactionGroup: { initialTransaction: { id } = {} } = {}, onRetry } = this.props
+
+ event.stopPropagation()
+ onRetry(id)
+ }
+
+ handleCopyTxId = () => {
+ const { transactionGroup} = this.props
+ const { primaryTransaction: transaction } = transactionGroup
+ const { hash } = transaction
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Activity Log',
+ name: 'Copied Transaction ID',
+ },
+ })
+
+ this.setState({ justCopied: true }, () => {
+ copyToClipboard(hash)
+ setTimeout(() => this.setState({ justCopied: false }), 1000)
+ })
+ }
+
+ renderCancel () {
+ const { t } = this.context
+ const {
+ showCancel,
+ cancelDisabled,
+ } = this.props
+
+ if (!showCancel) {
+ return null
+ }
+
+ return cancelDisabled
+ ? (
+ <Tooltip title={t('notEnoughGas')}>
+ <div>
+ <Button
+ type="raised"
+ onClick={this.handleCancel}
+ className="transaction-list-item-details__header-button"
+ disabled
+ >
+ { t('cancel') }
+ </Button>
+ </div>
+ </Tooltip>
+ )
+ : (
+ <Button
+ type="raised"
+ onClick={this.handleCancel}
+ className="transaction-list-item-details__header-button"
+ >
+ { t('cancel') }
+ </Button>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { justCopied } = this.state
+ const {
+ transactionGroup,
+ showRetry,
+ onCancel,
+ onRetry,
+ } = this.props
+ const { primaryTransaction: transaction } = transactionGroup
+ const { txParams: { to, from } = {} } = transaction
+
+ return (
+ <div className="transaction-list-item-details">
+ <div className="transaction-list-item-details__header">
+ <div>{ t('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>
+ )
+ }
+ { this.renderCancel() }
+ <Tooltip title={justCopied ? t('copiedTransactionId') : t('copyTransactionId')}>
+ <Button
+ type="raised"
+ onClick={this.handleCopyTxId}
+ className="transaction-list-item-details__header-button"
+ >
+ <img
+ className="transaction-list-item-details__header-button__copy-icon"
+ src="/images/copy-to-clipboard.svg"
+ />
+ </Button>
+ </Tooltip>
+ <Tooltip title={t('viewOnEtherscan')}>
+ <Button
+ type="raised"
+ onClick={this.handleEtherscanClick}
+ className="transaction-list-item-details__header-button"
+ >
+ <img src="/images/arrow-popout.svg" />
+ </Button>
+ </Tooltip>
+ </div>
+ </div>
+ <div className="transaction-list-item-details__body">
+ <div className="transaction-list-item-details__sender-to-recipient-container">
+ <SenderToRecipient
+ variant={FLAT_VARIANT}
+ addressOnly
+ recipientAddress={to}
+ senderAddress={from}
+ onRecipientClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Activity Log',
+ name: 'Copied "To" Address',
+ },
+ })
+ }}
+ onSenderClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Activity Log',
+ name: 'Copied "From" Address',
+ },
+ })
+ }}
+ />
+ </div>
+ <div className="transaction-list-item-details__cards-container">
+ <TransactionBreakdown
+ transaction={transaction}
+ className="transaction-list-item-details__transaction-breakdown"
+ />
+ <TransactionActivityLog
+ transactionGroup={transactionGroup}
+ className="transaction-list-item-details__transaction-activity-log"
+ onCancel={onCancel}
+ onRetry={onRetry}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/index.js b/ui/app/components/app/transaction-list-item/index.js
index 697cc55e9..697cc55e9 100644
--- a/ui/app/components/transaction-list-item/index.js
+++ b/ui/app/components/app/transaction-list-item/index.js
diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss
index 9e73a546c..9e73a546c 100644
--- a/ui/app/components/transaction-list-item/index.scss
+++ b/ui/app/components/app/transaction-list-item/index.scss
diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js
new file mode 100644
index 000000000..c7d9dd7c7
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js
@@ -0,0 +1,227 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../../ui/identicon'
+import TransactionStatus from '../transaction-status'
+import TransactionAction from '../transaction-action'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
+import TokenCurrencyDisplay from '../../ui/token-currency-display'
+import TransactionListItemDetails from '../transaction-list-item-details'
+import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'
+import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions'
+import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
+import { getStatusKey } from '../../../helpers/utils/transactions.util'
+
+export default class TransactionListItem extends PureComponent {
+ static propTypes = {
+ assetImages: PropTypes.object,
+ history: PropTypes.object,
+ methodData: PropTypes.object,
+ nonceAndDate: PropTypes.string,
+ primaryTransaction: PropTypes.object,
+ retryTransaction: PropTypes.func,
+ setSelectedToken: PropTypes.func,
+ showCancelModal: PropTypes.func,
+ showCancel: PropTypes.bool,
+ hasEnoughCancelGas: PropTypes.bool,
+ showRetry: PropTypes.bool,
+ showFiat: PropTypes.bool,
+ token: PropTypes.object,
+ tokenData: PropTypes.object,
+ transaction: PropTypes.object,
+ transactionGroup: PropTypes.object,
+ value: PropTypes.string,
+ fetchBasicGasAndTimeEstimates: PropTypes.func,
+ fetchGasEstimates: PropTypes.func,
+ }
+
+ static defaultProps = {
+ showFiat: true,
+ }
+
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ state = {
+ showTransactionDetails: false,
+ }
+
+ handleClick = () => {
+ const {
+ transaction,
+ history,
+ } = this.props
+ const { id, status } = transaction
+ const { showTransactionDetails } = this.state
+
+ if (status === UNAPPROVED_STATUS) {
+ history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
+ return
+ }
+
+ if (!showTransactionDetails) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Expand Transaction',
+ },
+ })
+ }
+
+ this.setState({ showTransactionDetails: !showTransactionDetails })
+ }
+
+ handleCancel = id => {
+ const {
+ primaryTransaction: { txParams: { gasPrice } } = {},
+ transaction: { id: initialTransactionId },
+ showCancelModal,
+ } = this.props
+
+ const cancelId = id || initialTransactionId
+ showCancelModal(cancelId, gasPrice)
+ }
+
+ /**
+ * @name handleRetry
+ * @description Resubmits a transaction. Retrying a transaction within a list of transactions with
+ * the same nonce requires keeping the original value while increasing the gas price of the latest
+ * transaction.
+ * @param {number} id - Transaction id
+ */
+ handleRetry = id => {
+ const {
+ primaryTransaction: { txParams: { gasPrice } } = {},
+ transaction: { txParams: { to } = {}, id: initialTransactionId },
+ methodData: { name } = {},
+ setSelectedToken,
+ retryTransaction,
+ fetchBasicGasAndTimeEstimates,
+ fetchGasEstimates,
+ } = this.props
+
+ if (name === TOKEN_METHOD_TRANSFER) {
+ setSelectedToken(to)
+ }
+
+ const retryId = id || initialTransactionId
+
+ return fetchBasicGasAndTimeEstimates()
+ .then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime))
+ .then(retryTransaction(retryId, gasPrice))
+ }
+
+ renderPrimaryCurrency () {
+ const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props
+
+ return token
+ ? (
+ <TokenCurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ token={token}
+ transactionData={data}
+ prefix="-"
+ />
+ ) : (
+ <UserPreferencedCurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ value={value}
+ type={PRIMARY}
+ prefix="-"
+ />
+ )
+ }
+
+ renderSecondaryCurrency () {
+ const { token, value, showFiat } = this.props
+
+ return token || !showFiat
+ ? null
+ : (
+ <UserPreferencedCurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--secondary"
+ value={value}
+ prefix="-"
+ type={SECONDARY}
+ />
+ )
+ }
+
+ render () {
+ const {
+ assetImages,
+ transaction,
+ methodData,
+ nonceAndDate,
+ primaryTransaction,
+ showCancel,
+ hasEnoughCancelGas,
+ showRetry,
+ tokenData,
+ transactionGroup,
+ } = 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={getStatusKey(primaryTransaction)}
+ title={(
+ (primaryTransaction.err && primaryTransaction.err.rpc)
+ ? primaryTransaction.err.rpc.message
+ : primaryTransaction.err && primaryTransaction.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
+ transactionGroup={transactionGroup}
+ onRetry={this.handleRetry}
+ showRetry={showRetry && methodData.done}
+ onCancel={this.handleCancel}
+ showCancel={showCancel}
+ cancelDisabled={!hasEnoughCancelGas}
+ />
+ </div>
+ )
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js
new file mode 100644
index 000000000..de8a3bbba
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js
@@ -0,0 +1,98 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import withMethodData from '../../../helpers/higher-order-components/with-method-data'
+import TransactionListItem from './transaction-list-item.component'
+import { setSelectedToken, showModal, showSidebar, addKnownMethodData } from '../../../store/actions'
+import { hexToDecimal } from '../../../helpers/utils/conversions.util'
+import { getTokenData } from '../../../helpers/utils/transactions.util'
+import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util'
+import { formatDate } from '../../../helpers/utils/util'
+import {
+ fetchBasicGasAndTimeEstimates,
+ fetchGasEstimates,
+ setCustomGasPriceForRetry,
+ setCustomGasLimit,
+} from '../../../ducks/gas/gas.duck'
+import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector } from '../../../selectors/selectors'
+import { isBalanceSufficient } from '../send/send.utils'
+
+const mapStateToProps = (state, ownProps) => {
+ const { metamask: { knownMethodData, accounts } } = state
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const { transactionGroup: { primaryTransaction } = {} } = ownProps
+ const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction
+ const selectedAccountBalance = accounts[getSelectedAddress(state)].balance
+
+ const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({
+ amount: '0x0',
+ gasTotal: getHexGasTotal({
+ gasPrice: increaseLastGasPrice(gasPrice),
+ gasLimit,
+ }),
+ balance: selectedAccountBalance,
+ conversionRate: conversionRateSelector(state),
+ })
+
+ return {
+ knownMethodData,
+ showFiat: (isMainnet || !!showFiatInTestnets),
+ selectedAccountBalance,
+ hasEnoughCancelGas,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
+ fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
+ setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
+ addKnownMethodData: (fourBytePrefix, methodData) => dispatch(addKnownMethodData(fourBytePrefix, methodData)),
+ retryTransaction: (transaction, gasPrice) => {
+ dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice))
+ dispatch(setCustomGasLimit(transaction.txParams.gas))
+ dispatch(showSidebar({
+ transitionName: 'sidebar-left',
+ type: 'customize-gas',
+ props: { transaction },
+ }))
+ },
+ showCancelModal: (transactionId, originalGasPrice) => {
+ return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
+ },
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps
+ const { retryTransaction, ...restDispatchProps } = dispatchProps
+ const { txParams: { nonce, data } = {}, time } = initialTransaction
+ const { txParams: { value } = {} } = primaryTransaction
+
+ const tokenData = data && getTokenData(data)
+ const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
+
+ return {
+ ...stateProps,
+ ...restDispatchProps,
+ ...ownProps,
+ value,
+ nonceAndDate,
+ tokenData,
+ transaction: initialTransaction,
+ primaryTransaction,
+ retryTransaction: (transactionId, gasPrice) => {
+ const { transactionGroup: { transactions = [] } } = ownProps
+ const transaction = transactions.find(tx => tx.id === transactionId) || {}
+ const increasedGasPrice = increaseLastGasPrice(gasPrice)
+ retryTransaction(transaction, increasedGasPrice)
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps),
+ withMethodData,
+)(TransactionListItem)
diff --git a/ui/app/components/transaction-list/index.js b/ui/app/components/app/transaction-list/index.js
index 688994367..688994367 100644
--- a/ui/app/components/transaction-list/index.js
+++ b/ui/app/components/app/transaction-list/index.js
diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/app/transaction-list/index.scss
index a486f4112..a486f4112 100644
--- a/ui/app/components/transaction-list/index.scss
+++ b/ui/app/components/app/transaction-list/index.scss
diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js
new file mode 100644
index 000000000..fc5488884
--- /dev/null
+++ b/ui/app/components/app/transaction-list/transaction-list.component.js
@@ -0,0 +1,126 @@
+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 '../../../helpers/constants/transactions'
+
+export default class TransactionList extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ pendingTransactions: [],
+ completedTransactions: [],
+ }
+
+ static propTypes = {
+ pendingTransactions: PropTypes.array,
+ completedTransactions: PropTypes.array,
+ 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 = (transactionGroup, isEarliestNonce) => {
+ const { transactions = [], hasRetried } = transactionGroup
+ const [earliestTransaction = {}] = transactions
+ const { submittedTime } = earliestTransaction
+ return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried
+ }
+
+ shouldShowCancel (transactionGroup) {
+ const { hasCancelled } = transactionGroup
+ return !hasCancelled
+ }
+
+ renderTransactions () {
+ const { t } = this.context
+ const { pendingTransactions = [], completedTransactions = [] } = this.props
+ const pendingLength = pendingTransactions.length
+
+ return (
+ <div className="transaction-list__transactions">
+ {
+ pendingLength > 0 && (
+ <div className="transaction-list__pending-transactions">
+ <div className="transaction-list__header">
+ { `${t('queue')} (${pendingTransactions.length})` }
+ </div>
+ {
+ pendingTransactions.map((transactionGroup, index) => (
+ this.renderTransaction(transactionGroup, index, true)
+ ))
+ }
+ </div>
+ )
+ }
+ <div className="transaction-list__completed-transactions">
+ <div className="transaction-list__header">
+ { t('history') }
+ </div>
+ {
+ completedTransactions.length > 0
+ ? completedTransactions.map((transactionGroup, index) => (
+ this.renderTransaction(transactionGroup, index)
+ ))
+ : this.renderEmpty()
+ }
+ </div>
+ </div>
+ )
+ }
+
+ renderTransaction (transactionGroup, index, isPendingTx = false) {
+ const { selectedToken, assetImages } = this.props
+ const { transactions = [] } = transactionGroup
+
+ return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT
+ ? (
+ <ShapeShiftTransactionListItem
+ { ...transactions[0] }
+ key={`shapeshift${index}`}
+ />
+ ) : (
+ <TransactionListItem
+ transactionGroup={transactionGroup}
+ key={`${transactionGroup.nonce}:${index}`}
+ showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, index === 0)}
+ showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)}
+ 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/app/transaction-list/transaction-list.container.js b/ui/app/components/app/transaction-list/transaction-list.container.js
new file mode 100644
index 000000000..67a24588b
--- /dev/null
+++ b/ui/app/components/app/transaction-list/transaction-list.container.js
@@ -0,0 +1,44 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import TransactionList from './transaction-list.component'
+import {
+ nonceSortedCompletedTransactionsSelector,
+ nonceSortedPendingTransactionsSelector,
+} from '../../../selectors/transactions'
+import { getSelectedAddress, getAssetImages } from '../../../selectors/selectors'
+import { selectedTokenSelector } from '../../../selectors/tokens'
+import { updateNetworkNonce } from '../../../store/actions'
+
+const mapStateToProps = state => {
+ return {
+ completedTransactions: nonceSortedCompletedTransactionsSelector(state),
+ pendingTransactions: nonceSortedPendingTransactionsSelector(state),
+ 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/app/transaction-status/index.js
index dece41e9c..dece41e9c 100644
--- a/ui/app/components/transaction-status/index.js
+++ b/ui/app/components/app/transaction-status/index.js
diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss
index e7daafeef..e7daafeef 100644
--- a/ui/app/components/transaction-status/index.scss
+++ b/ui/app/components/app/transaction-status/index.scss
diff --git a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js
new file mode 100644
index 000000000..ec1d580bd
--- /dev/null
+++ b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import assert from 'assert'
+import { mount } from 'enzyme'
+import TransactionStatus from '../transaction-status.component'
+import Tooltip from '../../../ui/tooltip-v2'
+
+describe('TransactionStatus Component', () => {
+ it('should render APPROVED properly', () => {
+ const wrapper = mount(
+ <TransactionStatus
+ statusKey="approved"
+ title="test-title"
+ />,
+ { context: { t: str => str.toUpperCase() } }
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.text(), 'APPROVED')
+ assert.equal(wrapper.find(Tooltip).props().title, 'test-title')
+ })
+
+ it('should render SUBMITTED properly', () => {
+ const wrapper = mount(
+ <TransactionStatus
+ statusKey="submitted"
+ />,
+ { context: { t: str => str.toUpperCase() } }
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.text(), 'PENDING')
+ })
+})
diff --git a/ui/app/components/app/transaction-status/transaction-status.component.js b/ui/app/components/app/transaction-status/transaction-status.component.js
new file mode 100644
index 000000000..d3a239539
--- /dev/null
+++ b/ui/app/components/app/transaction-status/transaction-status.component.js
@@ -0,0 +1,63 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Tooltip from '../../ui/tooltip-v2'
+import {
+ UNAPPROVED_STATUS,
+ REJECTED_STATUS,
+ APPROVED_STATUS,
+ SIGNED_STATUS,
+ SUBMITTED_STATUS,
+ CONFIRMED_STATUS,
+ FAILED_STATUS,
+ DROPPED_STATUS,
+ CANCELLED_STATUS,
+} from '../../../helpers/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',
+ [CANCELLED_STATUS]: 'transaction-status--failed',
+}
+
+const statusToTextHash = {
+ [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/app/transaction-view-balance/index.js
index 8824737f7..8824737f7 100644
--- a/ui/app/components/transaction-view-balance/index.js
+++ b/ui/app/components/app/transaction-view-balance/index.js
diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/app/transaction-view-balance/index.scss
index bdcd536b0..bdcd536b0 100644
--- a/ui/app/components/transaction-view-balance/index.scss
+++ b/ui/app/components/app/transaction-view-balance/index.scss
diff --git a/ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js
new file mode 100644
index 000000000..0e2882e9c
--- /dev/null
+++ b/ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js
@@ -0,0 +1,72 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TokenBalance from '../../../ui/token-balance'
+import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
+import { SEND_ROUTE } from '../../../../helpers/constants/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
+const metricsEvent = () => ({})
+
+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, metricsEvent } })
+
+ assert.equal(wrapper.find('.transaction-view-balance').length, 1)
+ assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).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/app/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js
new file mode 100644
index 000000000..8559e2233
--- /dev/null
+++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js
@@ -0,0 +1,145 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Button from '../../ui/button'
+import Identicon from '../../ui/identicon'
+import TokenBalance from '../../ui/token-balance'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
+import { SEND_ROUTE } from '../../../helpers/constants/routes'
+import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
+import Tooltip from '../../ui/tooltip-v2'
+
+export default class TransactionViewBalance extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ showDepositModal: PropTypes.func,
+ selectedToken: PropTypes.object,
+ history: PropTypes.object,
+ network: PropTypes.string,
+ balance: PropTypes.string,
+ assetImage: PropTypes.string,
+ balanceIsCached: PropTypes.bool,
+ showFiat: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ showFiat: true,
+ }
+
+ renderBalance () {
+ const { selectedToken, balance, balanceIsCached, showFiat } = this.props
+
+ return selectedToken
+ ? (
+ <div className="transaction-view-balance__balance">
+ <TokenBalance
+ token={selectedToken}
+ withSymbol
+ className="transaction-view-balance__primary-balance"
+ />
+ </div>
+ ) : (
+ <Tooltip position="top" title={this.context.t('balanceOutdated')} disabled={!balanceIsCached}>
+ <div className="transaction-view-balance__balance">
+ <div className="transaction-view-balance__primary-container">
+ <UserPreferencedCurrencyDisplay
+ className={classnames('transaction-view-balance__primary-balance', {
+ 'transaction-view-balance__cached-balance': balanceIsCached,
+ })}
+ value={balance}
+ type={PRIMARY}
+ ethNumberOfDecimals={4}
+ hideTitle={true}
+ />
+ {
+ balanceIsCached ? <span className="transaction-view-balance__cached-star">*</span> : null
+ }
+ </div>
+ {
+ showFiat && (
+ <UserPreferencedCurrencyDisplay
+ className={classnames({
+ 'transaction-view-balance__cached-secondary-balance': balanceIsCached,
+ 'transaction-view-balance__secondary-balance': !balanceIsCached,
+ })}
+ value={balance}
+ type={SECONDARY}
+ ethNumberOfDecimals={4}
+ hideTitle={true}
+ />
+ )
+ }
+ </div>
+ </Tooltip>
+ )
+ }
+
+ renderButtons () {
+ const { t, metricsEvent } = 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={() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Clicked Deposit',
+ },
+ })
+ showDepositModal()
+ }}
+ >
+ { t('deposit') }
+ </Button>
+ )
+ }
+ <Button
+ type="primary"
+ className="transaction-view-balance__button"
+ onClick={() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Clicked Send',
+ },
+ })
+ 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/app/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js
new file mode 100644
index 000000000..41a4525dc
--- /dev/null
+++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js
@@ -0,0 +1,46 @@
+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,
+ getNativeCurrency,
+ getSelectedTokenAssetImage,
+ getMetaMaskAccounts,
+ isBalanceCached,
+ preferencesSelector,
+ getIsMainnet,
+} from '../../../selectors/selectors'
+import { showModal } from '../../../store/actions'
+
+const mapStateToProps = state => {
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const selectedAddress = getSelectedAddress(state)
+ const { metamask: { network } } = state
+ const accounts = getMetaMaskAccounts(state)
+ const account = accounts[selectedAddress]
+ const { balance } = account
+
+ return {
+ selectedToken: getSelectedToken(state),
+ network,
+ balance,
+ nativeCurrency: getNativeCurrency(state),
+ assetImage: getSelectedTokenAssetImage(state),
+ balanceIsCached: isBalanceCached(state),
+ showFiat: (isMainnet || !!showFiatInTestnets),
+ }
+}
+
+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/app/transaction-view/index.js
index 9eb0c3c83..9eb0c3c83 100644
--- a/ui/app/components/transaction-view/index.js
+++ b/ui/app/components/app/transaction-view/index.js
diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/app/transaction-view/index.scss
index 13187f0e5..13187f0e5 100644
--- a/ui/app/components/transaction-view/index.scss
+++ b/ui/app/components/app/transaction-view/index.scss
diff --git a/ui/app/components/transaction-view/transaction-view.component.js b/ui/app/components/app/transaction-view/transaction-view.component.js
index 7014ca173..7014ca173 100644
--- a/ui/app/components/transaction-view/transaction-view.component.js
+++ b/ui/app/components/app/transaction-view/transaction-view.component.js
diff --git a/ui/app/components/ui-migration-annoucement/index.js b/ui/app/components/app/ui-migration-annoucement/index.js
index c6c8cc619..c6c8cc619 100644
--- a/ui/app/components/ui-migration-annoucement/index.js
+++ b/ui/app/components/app/ui-migration-annoucement/index.js
diff --git a/ui/app/components/ui-migration-annoucement/index.scss b/ui/app/components/app/ui-migration-annoucement/index.scss
index 6138a3079..6138a3079 100644
--- a/ui/app/components/ui-migration-annoucement/index.scss
+++ b/ui/app/components/app/ui-migration-annoucement/index.scss
diff --git a/ui/app/components/ui-migration-annoucement/ui-migration-annoucement.component.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js
index 7a4124972..7a4124972 100644
--- a/ui/app/components/ui-migration-annoucement/ui-migration-annoucement.component.js
+++ b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js
diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js
new file mode 100644
index 000000000..55efd5a44
--- /dev/null
+++ b/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import UiMigrationAnnouncement from './ui-migration-annoucement.component'
+import { setCompletedUiMigration } from '../../../store/actions'
+
+const mapStateToProps = (state) => {
+ const shouldShowAnnouncement = !state.metamask.completedUiMigration
+
+ return {
+ shouldShowAnnouncement,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onClose () {
+ dispatch(setCompletedUiMigration())
+ },
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(UiMigrationAnnouncement)
diff --git a/ui/app/components/user-preferenced-currency-display/index.js b/ui/app/components/app/user-preferenced-currency-display/index.js
index 0deddaecf..0deddaecf 100644
--- a/ui/app/components/user-preferenced-currency-display/index.js
+++ b/ui/app/components/app/user-preferenced-currency-display/index.js
diff --git a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js
new file mode 100644
index 000000000..51b2a3c4f
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js
@@ -0,0 +1,34 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component'
+import CurrencyDisplay from '../../../ui/currency-display'
+
+describe('UserPreferencedCurrencyDisplay Component', () => {
+ describe('rendering', () => {
+ it('should render properly', () => {
+ const wrapper = shallow(
+ <UserPreferencedCurrencyDisplay />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ })
+
+ it('should pass all props to the CurrencyDisplay child component', () => {
+ const wrapper = shallow(
+ <UserPreferencedCurrencyDisplay
+ prop1={true}
+ prop2="test"
+ prop3={1}
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true)
+ assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test')
+ assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1)
+ })
+ })
+})
diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
index 88d63baae..88d63baae 100644
--- a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
+++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
new file mode 100644
index 000000000..4b64b26c0
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
@@ -0,0 +1,47 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { PRIMARY, SECONDARY, ETH } from '../../../helpers/constants/common'
+import CurrencyDisplay from '../../ui/currency-display'
+
+export default class UserPreferencedCurrencyDisplay extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ prefix: PropTypes.string,
+ value: PropTypes.string,
+ numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ hideLabel: PropTypes.bool,
+ hideTitle: PropTypes.bool,
+ style: PropTypes.object,
+ showEthLogo: PropTypes.bool,
+ ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ // Used in container
+ type: PropTypes.oneOf([PRIMARY, SECONDARY]),
+ ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ ethPrefix: PropTypes.string,
+ fiatPrefix: PropTypes.string,
+ // From container
+ currency: PropTypes.string,
+ nativeCurrency: PropTypes.string,
+ }
+
+ renderEthLogo () {
+ const { currency, showEthLogo, ethLogoHeight = 12 } = this.props
+
+ return currency === ETH && showEthLogo && (
+ <img
+ src="/images/eth.svg"
+ height={ethLogoHeight}
+ />
+ )
+ }
+
+ render () {
+ return (
+ <CurrencyDisplay
+ {...this.props}
+ prefixComponent={this.renderEthLogo()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js
new file mode 100644
index 000000000..42d156f92
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js
@@ -0,0 +1,67 @@
+import { connect } from 'react-redux'
+import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'
+import { preferencesSelector, getIsMainnet } from '../../../selectors/selectors'
+import { ETH, PRIMARY, SECONDARY } from '../../../helpers/constants/common'
+
+const mapStateToProps = (state, ownProps) => {
+ const {
+ useNativeCurrencyAsPrimaryCurrency,
+ showFiatInTestnets,
+ } = preferencesSelector(state)
+
+ const isMainnet = getIsMainnet(state)
+
+ return {
+ useNativeCurrencyAsPrimaryCurrency,
+ showFiatInTestnets,
+ isMainnet,
+ nativeCurrency: state.metamask.nativeCurrency,
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, isMainnet, nativeCurrency, ...restStateProps } = stateProps
+ const {
+ type,
+ numberOfDecimals: propsNumberOfDecimals,
+ ethNumberOfDecimals,
+ fiatNumberOfDecimals,
+ ethPrefix,
+ fiatPrefix,
+ prefix: propsPrefix,
+ ...restOwnProps
+ } = ownProps
+
+ let currency, numberOfDecimals, prefix
+
+ if (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency ||
+ type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency) {
+ // Display ETH
+ currency = nativeCurrency || ETH
+ numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
+ prefix = propsPrefix || ethPrefix
+ } else if (type === SECONDARY && useNativeCurrencyAsPrimaryCurrency ||
+ type === PRIMARY && !useNativeCurrencyAsPrimaryCurrency) {
+ // Display Fiat
+ numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2
+ prefix = propsPrefix || fiatPrefix
+ }
+
+ if (!isMainnet && !showFiatInTestnets) {
+ currency = nativeCurrency || ETH
+ numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
+ prefix = propsPrefix || ethPrefix
+ }
+
+ return {
+ ...restStateProps,
+ ...dispatchProps,
+ ...restOwnProps,
+ nativeCurrency,
+ currency,
+ numberOfDecimals,
+ prefix,
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay)
diff --git a/ui/app/components/user-preferenced-currency-input/index.js b/ui/app/components/app/user-preferenced-currency-input/index.js
index 4dc70db3d..4dc70db3d 100644
--- a/ui/app/components/user-preferenced-currency-input/index.js
+++ b/ui/app/components/app/user-preferenced-currency-input/index.js
diff --git a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js
new file mode 100644
index 000000000..3802e16f3
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js
@@ -0,0 +1,32 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component'
+import CurrencyInput from '../../../ui/currency-input'
+
+describe('UserPreferencedCurrencyInput Component', () => {
+ describe('rendering', () => {
+ it('should render properly', () => {
+ const wrapper = shallow(
+ <UserPreferencedCurrencyInput />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(CurrencyInput).length, 1)
+ })
+
+ it('should render useFiat for CurrencyInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => {
+ const wrapper = shallow(
+ <UserPreferencedCurrencyInput
+ useNativeCurrencyAsPrimaryCurrency
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(CurrencyInput).length, 1)
+ assert.equal(wrapper.find(CurrencyInput).props().useFiat, false)
+ wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false })
+ assert.equal(wrapper.find(CurrencyInput).props().useFiat, true)
+ })
+ })
+})
diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js
index 959726443..959726443 100644
--- a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js
+++ b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js
diff --git a/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js
new file mode 100644
index 000000000..7c0ec1734
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js
@@ -0,0 +1,20 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyInput from '../../ui/currency-input'
+
+export default class UserPreferencedCurrencyInput extends PureComponent {
+ static propTypes = {
+ useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
+ }
+
+ render () {
+ const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props
+
+ return (
+ <CurrencyInput
+ {...restProps}
+ useFiat={!useNativeCurrencyAsPrimaryCurrency}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js
new file mode 100644
index 000000000..72f17fde4
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'
+import { preferencesSelector } from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
+
+ return {
+ useNativeCurrencyAsPrimaryCurrency,
+ }
+}
+
+export default connect(mapStateToProps)(UserPreferencedCurrencyInput)
diff --git a/ui/app/components/user-preferenced-token-input/index.js b/ui/app/components/app/user-preferenced-token-input/index.js
index 54167e633..54167e633 100644
--- a/ui/app/components/user-preferenced-token-input/index.js
+++ b/ui/app/components/app/user-preferenced-token-input/index.js
diff --git a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js
new file mode 100644
index 000000000..41cfd51f9
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js
@@ -0,0 +1,32 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import UserPreferencedTokenInput from '../user-preferenced-token-input.component'
+import TokenInput from '../../../ui/token-input'
+
+describe('UserPreferencedCurrencyInput Component', () => {
+ describe('rendering', () => {
+ it('should render properly', () => {
+ const wrapper = shallow(
+ <UserPreferencedTokenInput />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(TokenInput).length, 1)
+ })
+
+ it('should render showFiat for TokenInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => {
+ const wrapper = shallow(
+ <UserPreferencedTokenInput
+ useNativeCurrencyAsPrimaryCurrency
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(TokenInput).length, 1)
+ assert.equal(wrapper.find(TokenInput).props().showFiat, false)
+ wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false })
+ assert.equal(wrapper.find(TokenInput).props().showFiat, true)
+ })
+ })
+})
diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js
index 2f89fba90..2f89fba90 100644
--- a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js
+++ b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js
diff --git a/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js
new file mode 100644
index 000000000..24133188d
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js
@@ -0,0 +1,20 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TokenInput from '../../ui/token-input'
+
+export default class UserPreferencedTokenInput extends PureComponent {
+ static propTypes = {
+ useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
+ }
+
+ render () {
+ const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props
+
+ return (
+ <TokenInput
+ {...restProps}
+ showFiat={!useNativeCurrencyAsPrimaryCurrency}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js
new file mode 100644
index 000000000..4a20b20d9
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import UserPreferencedTokenInput from './user-preferenced-token-input.component'
+import { preferencesSelector } from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
+
+ return {
+ useNativeCurrencyAsPrimaryCurrency,
+ }
+}
+
+export default connect(mapStateToProps)(UserPreferencedTokenInput)
diff --git a/ui/app/components/app/wallet-view.js b/ui/app/components/app/wallet-view.js
new file mode 100644
index 000000000..cec8228b1
--- /dev/null
+++ b/ui/app/components/app/wallet-view.js
@@ -0,0 +1,246 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const inherits = require('util').inherits
+const classnames = require('classnames')
+const { checksumAddress } = require('../../helpers/utils/util')
+import Identicon from '../ui/identicon'
+// const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns
+const Tooltip = require('../ui/tooltip-v2.js').default
+const copyToClipboard = require('copy-to-clipboard')
+const actions = require('../../store/actions')
+import BalanceComponent from '../ui/balance'
+const TokenList = require('./token-list')
+const selectors = require('../../selectors/selectors')
+const { ADD_TOKEN_ROUTE } = require('../../helpers/constants/routes')
+
+import AddTokenButton from './add-token-button'
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(WalletView)
+
+WalletView.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+WalletView.defaultProps = {
+ responsiveDisplayClassname: '',
+}
+
+function mapStateToProps (state) {
+
+ return {
+ network: state.metamask.network,
+ sidebarOpen: state.appState.sidebar.isOpen,
+ identities: state.metamask.identities,
+ accounts: selectors.getMetaMaskAccounts(state),
+ keyrings: state.metamask.keyrings,
+ selectedAddress: selectors.getSelectedAddress(state),
+ selectedAccount: selectors.getSelectedAccount(state),
+ selectedTokenAddress: state.metamask.selectedTokenAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showSendPage: () => dispatch(actions.showSendPage()),
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ unsetSelectedToken: () => dispatch(actions.setSelectedToken()),
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ showAddTokenPage: () => dispatch(actions.showAddTokenPage()),
+ }
+}
+
+inherits(WalletView, Component)
+function WalletView () {
+ Component.call(this)
+ this.state = {
+ hasCopied: false,
+ copyToClipboardPressed: false,
+ }
+}
+
+WalletView.prototype.renderWalletBalance = function () {
+ const {
+ selectedTokenAddress,
+ selectedAccount,
+ unsetSelectedToken,
+ hideSidebar,
+ sidebarOpen,
+ } = this.props
+
+ const selectedClass = selectedTokenAddress
+ ? ''
+ : 'wallet-balance-wrapper--active'
+ const className = `flex-column wallet-balance-wrapper ${selectedClass}`
+
+ return h('div', { className }, [
+ h('div.wallet-balance',
+ {
+ onClick: () => {
+ unsetSelectedToken()
+ selectedTokenAddress && sidebarOpen && hideSidebar()
+ },
+ },
+ [
+ h(BalanceComponent, {
+ balanceValue: selectedAccount ? selectedAccount.balance : '',
+ style: {},
+ }),
+ ]
+ ),
+ ])
+}
+
+WalletView.prototype.renderAddToken = function () {
+ const {
+ sidebarOpen,
+ hideSidebar,
+ history,
+ } = this.props
+ const { metricsEvent } = this.context
+
+ return h(AddTokenButton, {
+ onClick () {
+ history.push(ADD_TOKEN_ROUTE)
+ metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Token Menu',
+ name: 'Clicked "Add Token"',
+ },
+ })
+ if (sidebarOpen) {
+ hideSidebar()
+ }
+ },
+ })
+}
+
+WalletView.prototype.render = function () {
+ const {
+ responsiveDisplayClassname,
+ selectedAddress,
+ keyrings,
+ showAccountDetailModal,
+ hideSidebar,
+ identities,
+ network,
+ } = this.props
+ // temporary logs + fake extra wallets
+
+ const checksummedAddress = checksumAddress(selectedAddress, network)
+
+ if (!selectedAddress) {
+ throw new Error('selectedAddress should not be ' + String(selectedAddress))
+ }
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(selectedAddress)
+ })
+
+ let label = ''
+ let type
+ if (keyring) {
+ type = keyring.type
+ if (type !== 'HD Key Tree') {
+ if (type.toLowerCase().search('hardware') !== -1) {
+ label = this.context.t('hardware')
+ } else {
+ label = this.context.t('imported')
+ }
+ }
+ }
+
+ return h('div.wallet-view.flex-column', {
+ style: {},
+ className: responsiveDisplayClassname,
+ }, [
+
+ // TODO: Separate component: wallet account details
+ h('div.flex-column.wallet-view-account-details', {
+ style: {},
+ }, [
+ h('div.wallet-view__sidebar-close', {
+ onClick: hideSidebar,
+ }),
+
+ h('div.wallet-view__keyring-label.allcaps', label),
+
+ h('div.flex-column.flex-center.wallet-view__name-container', {
+ style: { margin: '0 auto' },
+ onClick: showAccountDetailModal,
+ }, [
+ h(Identicon, {
+ diameter: 54,
+ address: checksummedAddress,
+ }),
+
+ h('span.account-name', {
+ style: {},
+ }, [
+ identities[selectedAddress].name,
+ ]),
+
+ h('button.btn-clear.wallet-view__details-button.allcaps', this.context.t('details')),
+ ]),
+ ]),
+
+ h(Tooltip, {
+ position: 'bottom',
+ title: this.state.hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard'),
+ wrapperClassName: 'wallet-view__tooltip',
+ }, [
+ h('button.wallet-view__address', {
+ className: classnames({
+ 'wallet-view__address__pressed': this.state.copyToClipboardPressed,
+ }),
+ onClick: () => {
+ copyToClipboard(checksummedAddress)
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Home',
+ name: 'Copied Address',
+ },
+ })
+ this.setState({ hasCopied: true })
+ setTimeout(() => this.setState({ hasCopied: false }), 3000)
+ },
+ onMouseDown: () => {
+ this.setState({ copyToClipboardPressed: true })
+ },
+ onMouseUp: () => {
+ this.setState({ copyToClipboardPressed: false })
+ },
+ }, [
+ `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`,
+ h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }),
+ ]),
+ ]),
+
+ this.renderWalletBalance(),
+
+ h(TokenList),
+
+ this.renderAddToken(),
+ ])
+}
+
+// TODO: Extra wallets, for dev testing. Remove when PRing to master.
+// const extraWallet = h('div.flex-column.wallet-balance-wrapper', {}, [
+// h('div.wallet-balance', {}, [
+// h(BalanceComponent, {
+// balanceValue: selectedAccount.balance,
+// style: {},
+// }),
+// ]),
+// ])
diff --git a/ui/app/components/balance/balance.component.js b/ui/app/components/balance/balance.component.js
deleted file mode 100644
index 9d0018add..000000000
--- a/ui/app/components/balance/balance.component.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import TokenBalance from '../token-balance'
-import Identicon from '../identicon'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../constants/common'
-import { formatBalance } from '../../util'
-
-export default class Balance extends PureComponent {
- static propTypes = {
- account: PropTypes.object,
- assetImages: PropTypes.object,
- nativeCurrency: PropTypes.string,
- needsParse: PropTypes.bool,
- network: PropTypes.string,
- showFiat: PropTypes.bool,
- token: PropTypes.object,
- }
-
- static defaultProps = {
- needsParse: true,
- showFiat: true,
- }
-
- renderBalance () {
- const { account, nativeCurrency, needsParse, showFiat } = this.props
- const balanceValue = account && account.balance
- const formattedBalance = balanceValue
- ? formatBalance(balanceValue, 6, needsParse, nativeCurrency)
- : '...'
-
- if (formattedBalance === 'None' || formattedBalance === '...') {
- return (
- <div className="flex-column balance-display">
- <div className="token-amount">
- { formattedBalance }
- </div>
- </div>
- )
- }
-
- return (
- <div className="flex-column balance-display">
- <UserPreferencedCurrencyDisplay
- className="token-amount"
- value={balanceValue}
- type={PRIMARY}
- ethNumberOfDecimals={4}
- />
- {
- showFiat && (
- <UserPreferencedCurrencyDisplay
- value={balanceValue}
- type={SECONDARY}
- ethNumberOfDecimals={4}
- />
- )
- }
- </div>
- )
- }
-
- renderTokenBalance () {
- const { token } = this.props
-
- return (
- <div className="flex-column balance-display">
- <div className="token-amount">
- <TokenBalance token={token} />
- </div>
- </div>
- )
- }
-
- render () {
- const { token, network, assetImages } = this.props
- const address = token && token.address
- const image = assetImages && address ? assetImages[token.address] : undefined
-
- return (
- <div className="balance-container">
- <Identicon
- diameter={50}
- address={address}
- network={network}
- image={image}
- />
- { token ? this.renderTokenBalance() : this.renderBalance() }
- </div>
- )
- }
-}
diff --git a/ui/app/components/balance/balance.container.js b/ui/app/components/balance/balance.container.js
deleted file mode 100644
index 1cd6df5ce..000000000
--- a/ui/app/components/balance/balance.container.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { connect } from 'react-redux'
-import Balance from './balance.component'
-import {
- getNativeCurrency,
- getAssetImages,
- conversionRateSelector,
- getCurrentCurrency,
- getMetaMaskAccounts,
- getIsMainnet,
- preferencesSelector,
-} from '../../selectors'
-
-const mapStateToProps = state => {
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
- const accounts = getMetaMaskAccounts(state)
- const network = state.metamask.network
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- const account = accounts[selectedAddress]
-
- return {
- account,
- network,
- nativeCurrency: getNativeCurrency(state),
- conversionRate: conversionRateSelector(state),
- currentCurrency: getCurrentCurrency(state),
- assetImages: getAssetImages(state),
- showFiat: (isMainnet || !!showFiatInTestnets),
- }
-}
-
-export default connect(mapStateToProps)(Balance)
diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js
deleted file mode 100644
index 14e1a7e49..000000000
--- a/ui/app/components/button-group/button-group.stories.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import { action } from '@storybook/addon-actions'
-import ButtonGroup from './'
-import Button from '../button'
-import { text, boolean } from '@storybook/addon-knobs/react'
-
-storiesOf('ButtonGroup', module)
- .add('with Buttons', () =>
- <ButtonGroup
- style={{ width: '300px' }}
- disabled={boolean('Disabled', false)}
- defaultActiveButtonIndex={1}
- >
- <Button
- onClick={action('cheap')}
- >
- {text('Button1', 'Cheap')}
- </Button>
- <Button
- onClick={action('average')}
- >
- {text('Button2', 'Average')}
- </Button>
- <Button
- onClick={action('fast')}
- >
- {text('Button3', 'Fast')}
- </Button>
- </ButtonGroup>
- )
- .add('with a disabled Button', () =>
- <ButtonGroup
- style={{ width: '300px' }}
- disabled={boolean('Disabled', false)}
- >
- <Button
- onClick={action('enabled')}
- >
- {text('Button1', 'Enabled')}
- </Button>
- <Button
- onClick={action('disabled')}
- disabled
- >
- {text('Button2', 'Disabled')}
- </Button>
- </ButtonGroup>
- )
diff --git a/ui/app/components/button/button.stories.js b/ui/app/components/button/button.stories.js
deleted file mode 100644
index dec084a25..000000000
--- a/ui/app/components/button/button.stories.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import { action } from '@storybook/addon-actions'
-import Button from './'
-import { text } from '@storybook/addon-knobs/react'
-
-storiesOf('Button', module)
- .add('primary', () =>
- <Button
- onClick={action('clicked')}
- type="primary"
- >
- {text('text', 'Click me')}
- </Button>
- )
- .add('secondary', () =>
- <Button
- onClick={action('clicked')}
- type="secondary"
- >
- {text('text', 'Click me')}
- </Button>
- )
- .add('default', () => (
- <Button
- onClick={action('clicked')}
- type="default"
- >
- {text('text', 'Click me')}
- </Button>
- ))
- .add('large primary', () => (
- <Button
- onClick={action('clicked')}
- type="primary"
- large
- >
- {text('text', 'Click me')}
- </Button>
- ))
- .add('large secondary', () => (
- <Button
- onClick={action('clicked')}
- type="secondary"
- large
- >
- {text('text', 'Click me')}
- </Button>
- ))
- .add('large default', () => (
- <Button
- onClick={action('clicked')}
- type="default"
- large
- >
- {text('text', 'Click me')}
- </Button>
- ))
diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js
deleted file mode 100644
index d5915292e..000000000
--- a/ui/app/components/coinbase-form.js
+++ /dev/null
@@ -1,69 +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')
-
-CoinbaseForm.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(CoinbaseForm)
-
-
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- }
-}
-
-inherits(CoinbaseForm, Component)
-
-function CoinbaseForm () {
- Component.call(this)
-}
-
-CoinbaseForm.prototype.render = function () {
- var props = this.props
-
- return h('.flex-column', {
- style: {
- marginTop: '35px',
- padding: '25px',
- width: '100%',
- },
- }, [
- h('.flex-row', {
- style: {
- justifyContent: 'space-around',
- margin: '33px',
- marginTop: '0px',
- },
- }, [
- h('button.btn-green', {
- onClick: this.toCoinbase.bind(this),
- }, this.context.t('continueToCoinbase')),
-
- h('button.btn-red', {
- onClick: () => props.dispatch(actions.goHome()),
- }, this.context.t('cancel')),
- ]),
- ])
-}
-
-CoinbaseForm.prototype.toCoinbase = function () {
- const props = this.props
- const address = props.buyView.buyAddress
- props.dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
-}
-
-CoinbaseForm.prototype.renderLoading = function () {
- return h('img', {
- style: {
- width: '27px',
- marginRight: '-27px',
- },
- src: 'images/loading.svg',
- })
-}
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
deleted file mode 100644
index c7262d2a9..000000000
--- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../../constants/common'
-
-const ConfirmDetailRow = props => {
- const {
- label,
- primaryText,
- secondaryText,
- onHeaderClick,
- primaryValueTextColor,
- headerText,
- headerTextClassName,
- value,
- } = props
-
- return (
- <div className="confirm-detail-row">
- <div className="confirm-detail-row__label">
- { label }
- </div>
- <div className="confirm-detail-row__details">
- <div
- className={classnames('confirm-detail-row__header-text', headerTextClassName)}
- onClick={() => onHeaderClick && onHeaderClick()}
- >
- { headerText }
- </div>
- {
- primaryText
- ? (
- <div
- className="confirm-detail-row__primary"
- style={{ color: primaryValueTextColor }}
- >
- { primaryText }
- </div>
- ) : (
- <UserPreferencedCurrencyDisplay
- className="confirm-detail-row__primary"
- type={PRIMARY}
- value={value}
- showEthLogo
- ethLogoHeight="18"
- style={{ color: primaryValueTextColor }}
- hideLabel
- />
- )
- }
- {
- secondaryText
- ? (
- <div className="confirm-detail-row__secondary">
- { secondaryText }
- </div>
- ) : (
- <UserPreferencedCurrencyDisplay
- className="confirm-detail-row__secondary"
- type={SECONDARY}
- value={value}
- showEthLogo
- hideLabel
- />
- )
- }
- </div>
- </div>
- )
-}
-
-ConfirmDetailRow.propTypes = {
- headerText: PropTypes.string,
- headerTextClassName: PropTypes.string,
- label: PropTypes.string,
- onHeaderClick: PropTypes.func,
- primaryValueTextColor: PropTypes.string,
- primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- secondaryText: PropTypes.string,
- value: PropTypes.string,
-}
-
-export default ConfirmDetailRow
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
deleted file mode 100644
index 1dca81560..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { Tabs, Tab } from '../../tabs'
-import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './'
-import ErrorMessage from '../../error-message'
-
-export default class ConfirmPageContainerContent extends Component {
- static propTypes = {
- action: PropTypes.string,
- dataComponent: PropTypes.node,
- detailsComponent: PropTypes.node,
- errorKey: PropTypes.string,
- errorMessage: PropTypes.string,
- hideSubtitle: PropTypes.bool,
- identiconAddress: PropTypes.string,
- nonce: PropTypes.string,
- assetImage: PropTypes.string,
- subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- subtitleComponent: PropTypes.node,
- summaryComponent: PropTypes.node,
- title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- titleComponent: PropTypes.node,
- warning: PropTypes.string,
- }
-
- renderContent () {
- const { detailsComponent, dataComponent } = this.props
-
- if (detailsComponent && dataComponent) {
- return this.renderTabs()
- } else {
- return detailsComponent || dataComponent
- }
- }
-
- renderTabs () {
- const { detailsComponent, dataComponent } = this.props
-
- return (
- <Tabs>
- <Tab name="Details">
- { detailsComponent }
- </Tab>
- <Tab name="Data">
- { dataComponent }
- </Tab>
- </Tabs>
- )
- }
-
- render () {
- const {
- action,
- errorKey,
- errorMessage,
- title,
- titleComponent,
- subtitle,
- subtitleComponent,
- hideSubtitle,
- identiconAddress,
- nonce,
- assetImage,
- summaryComponent,
- detailsComponent,
- dataComponent,
- warning,
- } = this.props
-
- return (
- <div className="confirm-page-container-content">
- {
- warning && (
- <ConfirmPageContainerWarning warning={warning} />
- )
- }
- {
- summaryComponent || (
- <ConfirmPageContainerSummary
- className={classnames({
- 'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
- })}
- action={action}
- title={title}
- titleComponent={titleComponent}
- subtitle={subtitle}
- subtitleComponent={subtitleComponent}
- hideSubtitle={hideSubtitle}
- identiconAddress={identiconAddress}
- nonce={nonce}
- assetImage={assetImage}
- />
- )
- }
- { this.renderContent() }
- {
- (errorKey || errorMessage) && (
- <div className="confirm-page-container-content__error-container">
- <ErrorMessage
- errorMessage={errorMessage}
- errorKey={errorKey}
- />
- </div>
- )
- }
- </div>
- )
- }
-}
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
deleted file mode 100644
index 89ceb015f..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import Identicon from '../../../identicon'
-
-const ConfirmPageContainerSummary = props => {
- const {
- action,
- title,
- titleComponent,
- subtitle,
- subtitleComponent,
- hideSubtitle,
- className,
- identiconAddress,
- nonce,
- assetImage,
- } = props
-
- return (
- <div className={classnames('confirm-page-container-summary', className)}>
- <div className="confirm-page-container-summary__action-row">
- <div className="confirm-page-container-summary__action">
- { action }
- </div>
- {
- nonce && (
- <div className="confirm-page-container-summary__nonce">
- { `#${nonce}` }
- </div>
- )
- }
- </div>
- <div className="confirm-page-container-summary__title">
- {
- identiconAddress && (
- <Identicon
- className="confirm-page-container-summary__identicon"
- diameter={36}
- address={identiconAddress}
- image={assetImage}
- />
- )
- }
- <div className="confirm-page-container-summary__title-text">
- { titleComponent || title }
- </div>
- </div>
- {
- hideSubtitle || <div className="confirm-page-container-summary__subtitle">
- { subtitleComponent || subtitle }
- </div>
- }
- </div>
- )
-}
-
-ConfirmPageContainerSummary.propTypes = {
- action: PropTypes.string,
- title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- titleComponent: PropTypes.node,
- subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- subtitleComponent: PropTypes.node,
- hideSubtitle: PropTypes.bool,
- 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.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
deleted file mode 100644
index 78639a435..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
+++ /dev/null
@@ -1,68 +0,0 @@
-@import './confirm-page-container-warning/index';
-
-@import './confirm-page-container-summary/index';
-
-.confirm-page-container-content {
- overflow-y: auto;
- flex: 1;
-
- &__error-container {
- padding: 0 16px 16px 16px;
- }
-
- &__details {
- box-sizing: border-box;
- padding: 0 24px;
- }
-
- &__data {
- padding: 16px;
- color: $oslo-gray;
- }
-
- &__data-box {
- background-color: #f9fafa;
- padding: 12px;
- font-size: .75rem;
- margin-bottom: 16px;
- word-wrap: break-word;
- max-height: 200px;
- overflow-y: auto;
-
- &-label {
- text-transform: uppercase;
- padding: 8px 0 12px;
- font-size: 12px;
- }
- }
-
- &__data-field {
- display: flex;
- flex-direction: row;
-
- &-label {
- font-weight: 500;
- padding-right: 16px;
- }
-
- &:not(:last-child) {
- margin-bottom: 5px;
- }
- }
-
- &__gas-fee {
- border-bottom: 1px solid $geyser;
-
- .advanced-gas-inputs__gas-edit-rows {
- margin-bottom: 16px;
- }
- }
-
- &__function-type {
- font-size: .875rem;
- font-weight: 500;
- text-transform: capitalize;
- color: $black;
- padding-left: 5px;
- }
-}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
deleted file mode 100644
index e6fe8f82c..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import {
- ENVIRONMENT_TYPE_POPUP,
- ENVIRONMENT_TYPE_NOTIFICATION,
-} from '../../../../../app/scripts/lib/enums'
-import NetworkDisplay from '../../network-display'
-
-export default class ConfirmPageContainer extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- showEdit: PropTypes.bool,
- onEdit: PropTypes.func,
- children: PropTypes.node,
- }
-
- renderTop () {
- const { onEdit, showEdit } = this.props
- const windowType = window.METAMASK_UI_TYPE
- const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
- windowType !== ENVIRONMENT_TYPE_POPUP
-
- if (!showEdit && isFullScreen) {
- return null
- }
-
- return (
- <div className="confirm-page-container-header__row">
- <div
- className="confirm-page-container-header__back-button-container"
- style={{
- visibility: showEdit ? 'initial' : 'hidden',
- }}
- >
- <img
- src="/images/caret-left.svg"
- />
- <span
- className="confirm-page-container-header__back-button"
- onClick={() => onEdit()}
- >
- { this.context.t('edit') }
- </span>
- </div>
- { !isFullScreen && <NetworkDisplay /> }
- </div>
- )
- }
-
- render () {
- const { children } = this.props
-
- return (
- <div className="confirm-page-container-header">
- { this.renderTop() }
- { children }
- </div>
- )
- }
-}
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
deleted file mode 100644
index 10edf3b16..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container.component.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SenderToRecipient from '../sender-to-recipient'
-import { PageContainerFooter } from '../page-container'
-import { ConfirmPageContainerHeader, ConfirmPageContainerContent, ConfirmPageContainerNavigation } from './'
-
-export default class ConfirmPageContainer extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- // Header
- action: PropTypes.string,
- hideSubtitle: PropTypes.bool,
- onEdit: PropTypes.func,
- showEdit: PropTypes.bool,
- subtitle: PropTypes.string,
- subtitleComponent: PropTypes.node,
- title: PropTypes.string,
- titleComponent: PropTypes.node,
- // Sender to Recipient
- fromAddress: PropTypes.string,
- fromName: PropTypes.string,
- toAddress: PropTypes.string,
- toName: PropTypes.string,
- // Content
- contentComponent: PropTypes.node,
- errorKey: PropTypes.string,
- errorMessage: PropTypes.string,
- fiatTransactionAmount: PropTypes.string,
- fiatTransactionFee: PropTypes.string,
- fiatTransactionTotal: PropTypes.string,
- ethTransactionAmount: PropTypes.string,
- ethTransactionFee: PropTypes.string,
- ethTransactionTotal: PropTypes.string,
- onEditGas: PropTypes.func,
- dataComponent: PropTypes.node,
- detailsComponent: PropTypes.node,
- identiconAddress: PropTypes.string,
- nonce: PropTypes.string,
- assetImage: PropTypes.string,
- summaryComponent: PropTypes.node,
- warning: PropTypes.string,
- unapprovedTxCount: PropTypes.number,
- // Navigation
- totalTx: PropTypes.number,
- positionOfCurrentTx: PropTypes.number,
- nextTxId: PropTypes.string,
- prevTxId: PropTypes.string,
- showNavigation: PropTypes.bool,
- onNextTx: PropTypes.func,
- firstTx: PropTypes.string,
- lastTx: PropTypes.string,
- ofText: PropTypes.string,
- requestsWaitingText: PropTypes.string,
- // Footer
- onCancelAll: PropTypes.func,
- onCancel: PropTypes.func,
- onSubmit: PropTypes.func,
- disabled: PropTypes.bool,
- }
-
- render () {
- const {
- showEdit,
- onEdit,
- fromName,
- fromAddress,
- toName,
- toAddress,
- disabled,
- errorKey,
- errorMessage,
- contentComponent,
- action,
- title,
- titleComponent,
- subtitle,
- subtitleComponent,
- hideSubtitle,
- summaryComponent,
- detailsComponent,
- dataComponent,
- onCancelAll,
- onCancel,
- onSubmit,
- identiconAddress,
- nonce,
- unapprovedTxCount,
- assetImage,
- warning,
- totalTx,
- positionOfCurrentTx,
- nextTxId,
- prevTxId,
- showNavigation,
- onNextTx,
- firstTx,
- lastTx,
- ofText,
- requestsWaitingText,
- } = this.props
- const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
-
- return (
- <div className="page-container">
- <ConfirmPageContainerNavigation
- totalTx={totalTx}
- positionOfCurrentTx={positionOfCurrentTx}
- nextTxId={nextTxId}
- prevTxId={prevTxId}
- showNavigation={showNavigation}
- onNextTx={(txId) => onNextTx(txId)}
- firstTx={firstTx}
- lastTx={lastTx}
- ofText={ofText}
- requestsWaitingText={requestsWaitingText}
- />
- <ConfirmPageContainerHeader
- showEdit={showEdit}
- onEdit={() => onEdit()}
- >
- <SenderToRecipient
- senderName={fromName}
- senderAddress={fromAddress}
- recipientName={toName}
- recipientAddress={toAddress}
- assetImage={renderAssetImage ? assetImage : undefined}
- />
- </ConfirmPageContainerHeader>
- {
- contentComponent || (
- <ConfirmPageContainerContent
- action={action}
- title={title}
- titleComponent={titleComponent}
- subtitle={subtitle}
- subtitleComponent={subtitleComponent}
- hideSubtitle={hideSubtitle}
- summaryComponent={summaryComponent}
- detailsComponent={detailsComponent}
- dataComponent={dataComponent}
- errorMessage={errorMessage}
- 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/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss
deleted file mode 100644
index d41cd4423..000000000
--- a/ui/app/components/confirm-page-container/index.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@import './confirm-page-container-content/index';
-
-@import './confirm-page-container-header/index';
-
-@import './confirm-detail-row/index';
-
-@import './confirm-page-container-navigation/index'; \ No newline at end of file
diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js
deleted file mode 100644
index ad504deb8..000000000
--- a/ui/app/components/copyable.js
+++ /dev/null
@@ -1,53 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-
-const Tooltip = require('./tooltip')
-const copyToClipboard = require('copy-to-clipboard')
-const connect = require('react-redux').connect
-
-Copyable.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(Copyable)
-
-
-inherits(Copyable, Component)
-function Copyable () {
- Component.call(this)
- this.state = {
- copied: false,
- }
-}
-
-Copyable.prototype.render = function () {
- const props = this.props
- const state = this.state
- const { value, children } = props
- const { copied } = state
-
- return h(Tooltip, {
- title: copied ? this.context.t('copiedExclamation') : this.context.t('copy'),
- position: 'bottom',
- }, h('span', {
- style: {
- cursor: 'pointer',
- },
- onClick: (event) => {
- event.preventDefault()
- event.stopPropagation()
- copyToClipboard(value)
- this.debounceRestore()
- },
- }, children))
-}
-
-Copyable.prototype.debounceRestore = function () {
- this.setState({ copied: true })
- clearTimeout(this.timeout)
- this.timeout = setTimeout(() => {
- this.setState({ copied: false })
- }, 850)
-}
diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js
deleted file mode 100644
index 6a743cc4e..000000000
--- a/ui/app/components/currency-display/currency-display.component.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { GWEI } from '../../constants/common'
-
-export default class CurrencyDisplay extends PureComponent {
- static propTypes = {
- className: PropTypes.string,
- displayValue: PropTypes.string,
- prefix: PropTypes.string,
- prefixComponent: PropTypes.node,
- style: PropTypes.object,
- suffix: PropTypes.string,
- // Used in container
- currency: PropTypes.string,
- denomination: PropTypes.oneOf([GWEI]),
- value: PropTypes.string,
- numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- hideLabel: PropTypes.bool,
- hideTitle: PropTypes.bool,
- }
-
- render () {
- const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props
- const text = `${prefix || ''}${displayValue}`
- const title = `${text} ${suffix}`
-
- return (
- <div
- className={classnames('currency-display-component', className)}
- style={style}
- title={!hideTitle && title || null}
- >
- { prefixComponent }
- <span className="currency-display-component__text">{ text }</span>
- {
- suffix && (
- <span className="currency-display-component__suffix">
- { suffix }
- </span>
- )
- }
- </div>
- )
- }
-}
diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js
deleted file mode 100644
index e581f8a5e..000000000
--- a/ui/app/components/currency-display/currency-display.container.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { connect } from 'react-redux'
-import CurrencyDisplay from './currency-display.component'
-import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
-
-const mapStateToProps = state => {
- const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
-
- return {
- currentCurrency,
- conversionRate,
- nativeCurrency,
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { nativeCurrency, currentCurrency, conversionRate, ...restStateProps } = stateProps
- const {
- value,
- numberOfDecimals = 2,
- currency,
- denomination,
- hideLabel,
- displayValue: propsDisplayValue,
- suffix: propsSuffix,
- ...restOwnProps
- } = ownProps
-
- const toCurrency = currency || currentCurrency
-
- const displayValue = propsDisplayValue || formatCurrency(
- getValueFromWeiHex({
- value,
- fromCurrency: nativeCurrency,
- toCurrency, conversionRate,
- numberOfDecimals,
- toDenomination: denomination,
- }),
- toCurrency
- )
- const suffix = propsSuffix || (hideLabel ? undefined : toCurrency.toUpperCase())
-
- return {
- ...restStateProps,
- ...dispatchProps,
- ...restOwnProps,
- displayValue,
- suffix,
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay)
diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js
deleted file mode 100644
index 30e0e919b..000000000
--- a/ui/app/components/currency-input/currency-input.component.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import UnitInput from '../unit-input'
-import CurrencyDisplay from '../currency-display'
-import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
-import { ETH } from '../../constants/common'
-
-/**
- * Component that allows user to enter currency values as a number, and props receive a converted
- * hex value in WEI. props.value, used as a default or forced value, should be a hex value, which
- * gets converted into a decimal value depending on the currency (ETH or Fiat).
- */
-export default class CurrencyInput extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- nativeCurrency: PropTypes.string,
- onChange: PropTypes.func,
- onBlur: PropTypes.func,
- useFiat: PropTypes.bool,
- hideFiat: PropTypes.bool,
- value: PropTypes.string,
- fiatSuffix: PropTypes.string,
- nativeSuffix: PropTypes.string,
- }
-
- constructor (props) {
- super(props)
-
- const { value: hexValue } = props
- const decimalValue = hexValue ? this.getDecimalValue(props) : 0
-
- this.state = {
- decimalValue,
- hexValue,
- isSwapped: false,
- }
- }
-
- componentDidUpdate (prevProps) {
- const { value: prevPropsHexValue } = prevProps
- const { value: propsHexValue } = this.props
- const { hexValue: stateHexValue } = this.state
-
- if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
- const decimalValue = this.getDecimalValue(this.props)
- this.setState({ hexValue: propsHexValue, decimalValue })
- }
- }
-
- getDecimalValue (props) {
- const { value: hexValue, currentCurrency, conversionRate } = props
- const decimalValueString = this.shouldUseFiat()
- ? getValueFromWeiHex({
- value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
- })
- : getValueFromWeiHex({
- value: hexValue, toCurrency: ETH, numberOfDecimals: 6,
- })
-
- return Number(decimalValueString) || 0
- }
-
- shouldUseFiat = () => {
- const { useFiat, hideFiat } = this.props
- const { isSwapped } = this.state || {}
-
- if (hideFiat) {
- return false
- }
-
- return isSwapped ? !useFiat : useFiat
- }
-
- swap = () => {
- const { isSwapped, decimalValue } = this.state
- this.setState({isSwapped: !isSwapped}, () => {
- this.handleChange(decimalValue)
- })
- }
-
- handleChange = decimalValue => {
- const { currentCurrency: fromCurrency, conversionRate, onChange } = this.props
-
- const hexValue = this.shouldUseFiat()
- ? getWeiHexFromDecimalValue({
- value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true,
- })
- : getWeiHexFromDecimalValue({
- value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate,
- })
-
- this.setState({ hexValue, decimalValue })
- onChange(hexValue)
- }
-
- handleBlur = () => {
- this.props.onBlur(this.state.hexValue)
- }
-
- renderConversionComponent () {
- const { currentCurrency, nativeCurrency, hideFiat } = this.props
- const { hexValue } = this.state
- let currency, numberOfDecimals
-
- if (hideFiat) {
- return (
- <div className="currency-input__conversion-component">
- { this.context.t('noConversionRateAvailable') }
- </div>
- )
- }
-
- if (this.shouldUseFiat()) {
- // Display ETH
- currency = nativeCurrency || ETH
- numberOfDecimals = 6
- } else {
- // Display Fiat
- currency = currentCurrency
- numberOfDecimals = 2
- }
-
- return (
- <CurrencyDisplay
- className="currency-input__conversion-component"
- currency={currency}
- value={hexValue}
- numberOfDecimals={numberOfDecimals}
- />
- )
- }
-
- render () {
- const { fiatSuffix, nativeSuffix, ...restProps } = this.props
- const { decimalValue } = this.state
-
- return (
- <UnitInput
- {...restProps}
- suffix={this.shouldUseFiat() ? fiatSuffix : nativeSuffix}
- onChange={this.handleChange}
- onBlur={this.handleBlur}
- value={decimalValue}
- actionComponent={(
- <div
- className="currency-input__swap-component"
- onClick={this.swap}
- />
- )}
- >
- { this.renderConversionComponent() }
- </UnitInput>
- )
- }
-}
diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js
deleted file mode 100644
index 428be4557..000000000
--- a/ui/app/components/currency-input/currency-input.container.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { connect } from 'react-redux'
-import CurrencyInput from './currency-input.component'
-import { ETH } from '../../constants/common'
-import {getIsMainnet, preferencesSelector} from '../../selectors'
-
-const mapStateToProps = state => {
- const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
-
- return {
- nativeCurrency,
- currentCurrency,
- conversionRate,
- hideFiat: (!isMainnet && !showFiatInTestnets),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { nativeCurrency, currentCurrency } = stateProps
-
- return {
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- nativeSuffix: nativeCurrency || ETH,
- fiatSuffix: currentCurrency.toUpperCase(),
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(CurrencyInput)
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
deleted file mode 100644
index fd660ead2..000000000
--- a/ui/app/components/customize-gas-modal/index.js
+++ /dev/null
@@ -1,396 +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 BigNumber = require('bignumber.js')
-const actions = require('../../actions')
-const GasModalCard = require('./gas-modal-card')
-import Button from '../button'
-
-const ethUtil = require('ethereumjs-util')
-
-import {
- updateSendErrors,
-} from '../../ducks/send.duck'
-
-const {
- MIN_GAS_PRICE_DEC,
- MIN_GAS_LIMIT_DEC,
- MIN_GAS_PRICE_GWEI,
-} = require('../send/send.constants')
-
-const {
- isBalanceSufficient,
-} = require('../send/send.utils')
-
-const {
- conversionUtil,
- multiplyCurrencies,
- conversionGreaterThan,
- conversionMax,
- subtractCurrencies,
-} = require('../../conversion-util')
-
-const {
- getGasIsLoading,
- getForceGasMin,
- conversionRateSelector,
- getSendAmount,
- getSelectedToken,
- getSendFrom,
- getCurrentAccountWithSendEtherInfo,
- getSelectedTokenToFiatRate,
- getSendMaxModeState,
-} = require('../../selectors')
-
-const {
- getGasPrice,
- getGasLimit,
-} = require('../send/send.selectors')
-
-function mapStateToProps (state) {
- const selectedToken = getSelectedToken(state)
- const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
- const conversionRate = conversionRateSelector(state)
-
- return {
- gasPrice: getGasPrice(state),
- gasLimit: getGasLimit(state),
- gasIsLoading: getGasIsLoading(state),
- forceGasMin: getForceGasMin(state),
- conversionRate,
- amount: getSendAmount(state),
- maxModeOn: getSendMaxModeState(state),
- balance: currentAccount.balance,
- primaryCurrency: selectedToken && selectedToken.symbol,
- selectedToken,
- amountConversionRate: selectedToken ? getSelectedTokenToFiatRate(state) : conversionRate,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => dispatch(actions.hideModal()),
- setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
- setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
- setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
- updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
- updateSendErrors: error => dispatch(updateSendErrors(error)),
- }
-}
-
-function getFreshState (props) {
- const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
- const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
-
- const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-
- return {
- gasPrice,
- gasLimit,
- gasTotal,
- error: null,
- priceSigZeros: '',
- priceSigDec: '',
- }
-}
-
-inherits(CustomizeGasModal, Component)
-function CustomizeGasModal (props) {
- Component.call(this)
-
- const originalState = getFreshState(props)
- this.state = {
- ...originalState,
- originalState,
- }
-}
-
-CustomizeGasModal.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
-
-CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
- const currentState = getFreshState(this.props)
- const {
- gasPrice: currentGasPrice,
- gasLimit: currentGasLimit,
- } = currentState
- const newState = getFreshState(nextProps)
- const {
- gasPrice: newGasPrice,
- gasLimit: newGasLimit,
- gasTotal: newGasTotal,
- } = newState
- const gasPriceChanged = currentGasPrice !== newGasPrice
- const gasLimitChanged = currentGasLimit !== newGasLimit
-
- if (gasPriceChanged) {
- this.setState({
- gasPrice: newGasPrice,
- gasTotal: newGasTotal,
- priceSigZeros: '',
- priceSigDec: '',
- })
- }
- if (gasLimitChanged) {
- this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal })
- }
- if (gasLimitChanged || gasPriceChanged) {
- this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal })
- }
-}
-
-CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
- const { metricsEvent } = this.context
- const {
- setGasPrice,
- setGasLimit,
- hideModal,
- setGasTotal,
- maxModeOn,
- selectedToken,
- balance,
- updateSendAmount,
- updateSendErrors,
- } = this.props
- const {
- originalState,
- } = this.state
-
- if (maxModeOn && !selectedToken) {
- const maxAmount = subtractCurrencies(
- ethUtil.addHexPrefix(balance),
- ethUtil.addHexPrefix(gasTotal),
- { toNumericBase: 'hex' }
- )
- updateSendAmount(maxAmount)
- }
-
- metricsEvent({
- eventOpts: {
- category: 'Activation',
- action: 'userCloses',
- name: 'closeCustomizeGas',
- },
- pageOpts: {
- section: 'customizeGasModal',
- component: 'customizeGasSaveButton',
- },
- customVariables: {
- gasPriceChange: (new BigNumber(ethUtil.addHexPrefix(gasPrice))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasPrice))).toString(10),
- gasLimitChange: (new BigNumber(ethUtil.addHexPrefix(gasLimit))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasLimit))).toString(10),
- },
- })
-
- setGasPrice(ethUtil.addHexPrefix(gasPrice))
- setGasLimit(ethUtil.addHexPrefix(gasLimit))
- setGasTotal(ethUtil.addHexPrefix(gasTotal))
- updateSendErrors({ insufficientFunds: false })
- hideModal()
-}
-
-CustomizeGasModal.prototype.revert = function () {
- this.setState(this.state.originalState)
-}
-
-CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
- const {
- amount,
- balance,
- selectedToken,
- amountConversionRate,
- conversionRate,
- maxModeOn,
- } = this.props
-
- let error = null
-
- const balanceIsSufficient = isBalanceSufficient({
- amount: selectedToken || maxModeOn ? '0' : amount,
- gasTotal,
- balance,
- selectedToken,
- amountConversionRate,
- conversionRate,
- })
-
- if (!balanceIsSufficient) {
- error = this.context.t('balanceIsInsufficientGas')
- }
-
- const gasLimitTooLow = gasLimit && conversionGreaterThan(
- {
- value: MIN_GAS_LIMIT_DEC,
- fromNumericBase: 'dec',
- conversionRate,
- },
- {
- value: gasLimit,
- fromNumericBase: 'hex',
- },
- )
-
- if (gasLimitTooLow) {
- error = this.context.t('gasLimitTooLow')
- }
-
- this.setState({ error })
- return error
-}
-
-CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
- const { gasPrice } = this.state
-
- const gasLimit = conversionUtil(newGasLimit, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
-
- const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-
- this.validate({ gasTotal, gasLimit })
-
- this.setState({ gasTotal, gasLimit })
-}
-
-CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
- const { gasLimit } = this.state
- const sigZeros = String(newGasPrice).match(/^\d+[.]\d*?(0+)$/)
- const sigDec = String(newGasPrice).match(/^\d+([.])0*$/)
-
- this.setState({
- priceSigZeros: sigZeros && sigZeros[1] || '',
- priceSigDec: sigDec && sigDec[1] || '',
- })
-
- const gasPrice = conversionUtil(newGasPrice, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- fromDenomination: 'GWEI',
- toDenomination: 'WEI',
- })
-
- const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-
- this.validate({ gasTotal })
-
- this.setState({ gasTotal, gasPrice })
-}
-
-CustomizeGasModal.prototype.render = function () {
- const { hideModal, forceGasMin, gasIsLoading } = this.props
- const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
-
- let convertedGasPrice = conversionUtil(gasPrice, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- toDenomination: 'GWEI',
- })
-
- convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
-
- let newGasPrice = gasPrice
- if (forceGasMin) {
- const convertedMinPrice = conversionUtil(forceGasMin, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
- convertedGasPrice = conversionMax(
- { value: convertedMinPrice, fromNumericBase: 'dec' },
- { value: convertedGasPrice, fromNumericBase: 'dec' }
- )
- newGasPrice = conversionMax(
- { value: gasPrice, fromNumericBase: 'hex' },
- { value: forceGasMin, fromNumericBase: 'hex' }
- )
- }
-
- const convertedGasLimit = conversionUtil(gasLimit, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
-
- return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
- h('div.send-v2__customize-gas__content', {
- }, [
- h('div.send-v2__customize-gas__header', {}, [
-
- h('div.send-v2__customize-gas__title', this.context.t('customGas')),
-
- h('div.send-v2__customize-gas__close', {
- onClick: hideModal,
- }),
-
- ]),
-
- h('div.send-v2__customize-gas__body', {}, [
-
- h(GasModalCard, {
- value: convertedGasPrice,
- min: forceGasMin || MIN_GAS_PRICE_GWEI,
- step: 1,
- onChange: value => this.convertAndSetGasPrice(value),
- title: this.context.t('gasPrice'),
- copy: this.context.t('gasPriceCalculation'),
- gasIsLoading,
- }),
-
- h(GasModalCard, {
- value: convertedGasLimit,
- min: 1,
- step: 1,
- onChange: value => this.convertAndSetGasLimit(value),
- title: this.context.t('gasLimit'),
- copy: this.context.t('gasLimitCalculation'),
- gasIsLoading,
- }),
-
- ]),
-
- h('div.send-v2__customize-gas__footer', {}, [
-
- error && h('div.send-v2__customize-gas__error-message', [
- error,
- ]),
-
- h('div.send-v2__customize-gas__revert', {
- onClick: () => this.revert(),
- }, [this.context.t('revert')]),
-
- h('div.send-v2__customize-gas__buttons', [
- h(Button, {
- type: 'default',
- className: 'send-v2__customize-gas__cancel',
- onClick: this.props.hideModal,
- }, [this.context.t('cancel')]),
- h(Button, {
- type: 'primary',
- className: 'send-v2__customize-gas__save',
- onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
- 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
deleted file mode 100644
index bda8b9517..000000000
--- a/ui/app/components/dropdowns/account-details-dropdown.js
+++ /dev/null
@@ -1,131 +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 { getSelectedIdentity } = require('../../selectors')
-const genAccountLink = require('../../../lib/account-link.js')
-const { Menu, Item, CloseArea } = require('./components/menu')
-
-AccountDetailsDropdown.contextTypes = {
- t: PropTypes.func,
- metricsEvent: 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()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Account Options',
- name: 'Clicked Expand View',
- },
- })
- 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.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Account Options',
- name: 'Viewed Account Details',
- },
- })
- this.props.onClose()
- },
- text: this.context.t('accountDetails'),
- icon: h(`img`, { src: 'images/info.svg', style: { height: '15px' } }),
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Account Options',
- name: 'Clicked View on Etherscan',
- },
- })
- 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
deleted file mode 100644
index e6b3e0c0c..000000000
--- a/ui/app/components/dropdowns/components/account-dropdowns.js
+++ /dev/null
@@ -1,473 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const actions = require('../../../actions')
-const genAccountLink = require('../../../../lib/account-link.js')
-const connect = require('react-redux').connect
-const Dropdown = require('./dropdown').Dropdown
-const DropdownMenuItem = require('./dropdown').DropdownMenuItem
-import Identicon from '../../identicon'
-const { checksumAddress } = require('../../../util')
-const copyToClipboard = require('copy-to-clipboard')
-const { formatBalance } = require('../../../util')
-
-
-class AccountDropdowns extends Component {
- constructor (props) {
- super(props)
- this.state = {
- accountSelectorActive: false,
- optionsMenuActive: false,
- }
- // Used for orangeaccount selector icon
- // this.accountSelectorToggleClassName = 'accounts-selector'
- this.accountSelectorToggleClassName = 'fa-angle-down'
- this.optionsMenuToggleClassName = 'fa-ellipsis-h'
- }
-
- renderAccounts () {
- const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props
-
- return Object.keys(identities).map((key, index) => {
- const identity = identities[key]
- const isSelected = identity.address === selected
-
- const balanceValue = accounts[key].balance
- const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, true, ticker) : '...'
- const simpleAddress = identity.address.substring(2).toLowerCase()
-
- const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(simpleAddress) ||
- kr.accounts.includes(identity.address)
- })
-
- return h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- this.props.actions.showAccountDetail(identity.address)
- },
- style: Object.assign(
- {
- marginTop: index === 0 ? '5px' : '',
- fontSize: '24px',
- width: '260px',
- },
- menuItemStyles,
- ),
- },
- [
- h('div.flex-row.flex-center', {}, [
-
- h('span', {
- style: {
- flex: '1 1 0',
- minWidth: '20px',
- minHeight: '30px',
- },
- }, [
- h('span', {
- style: {
- flex: '1 1 auto',
- fontSize: '14px',
- },
- }, isSelected ? h('i.fa.fa-check') : null),
- ]),
-
- h(
- Identicon,
- {
- address: identity.address,
- diameter: 24,
- style: {
- flex: '1 1 auto',
- marginLeft: '10px',
- },
- },
- ),
-
- h('span.flex-column', {
- style: {
- flex: '10 10 auto',
- width: '175px',
- alignItems: 'flex-start',
- justifyContent: 'center',
- marginLeft: '10px',
- position: 'relative',
- },
- }, [
- this.indicateIfLoose(keyring),
- h('span.account-dropdown-name', {
- style: {
- fontSize: '18px',
- maxWidth: '145px',
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- },
- }, identity.name || ''),
-
- h('span.account-dropdown-balance', {
- style: {
- fontSize: '14px',
- fontFamily: 'Avenir',
- fontWeight: 500,
- },
- }, formattedBalance),
- ]),
-
- h('span', {
- style: {
- flex: '3 3 auto',
- },
- }, [
- h('span.account-dropdown-edit-button.allcaps', {
- style: {
- fontSize: '16px',
- },
- onClick: () => {
- actions.showEditAccountModal(identity)
- },
- }, [
- this.context.t('edit'),
- ]),
- ]),
-
- ]),
- ]
- )
- })
- }
-
- indicateIfLoose (keyring) {
- try { // Sometimes keyrings aren't loaded yet:
- const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
- return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null
- } catch (e) { return }
- }
-
- renderAccountSelector () {
- const { actions, useCssTransition, innerStyle, sidebarOpen } = this.props
- const { accountSelectorActive, menuItemStyles } = this.state
-
- return h(
- Dropdown,
- {
- useCssTransition,
- style: {
- marginLeft: '-185px',
- marginTop: '50px',
- minWidth: '180px',
- overflowY: 'auto',
- maxHeight: '300px',
- width: '300px',
- },
- innerStyle,
- isOpen: accountSelectorActive,
- onClickOutside: (event) => {
- const { classList } = event.target
- const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
- if (accountSelectorActive && isNotToggleElement) {
- this.setState({ accountSelectorActive: false })
- }
- },
- },
- [
- ...this.renderAccounts(),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- style: Object.assign(
- {},
- menuItemStyles,
- ),
- onClick: () => actions.showNewAccountPageCreateForm(),
- },
- [
- h(
- 'i.fa.fa-plus.fa-lg',
- {
- style: {
- marginLeft: '8px',
- },
- }
- ),
- h('span', {
- style: {
- marginLeft: '14px',
- fontFamily: 'DIN OT',
- fontSize: '16px',
- lineHeight: '23px',
- },
- }, this.context.t('createAccount')),
- ],
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {
- if (sidebarOpen) {
- actions.hideSidebar()
- }
- },
- onClick: () => actions.showNewAccountPageImportForm(),
- style: Object.assign(
- {},
- menuItemStyles,
- ),
- },
- [
- h(
- 'i.fa.fa-download.fa-lg',
- {
- style: {
- marginLeft: '8px',
- },
- }
- ),
- h('span', {
- style: {
- marginLeft: '20px',
- marginBottom: '5px',
- fontFamily: 'DIN OT',
- fontSize: '16px',
- lineHeight: '23px',
- },
- }, this.context.t('importAccount')),
- ]
- ),
- ]
- )
- }
-
- renderAccountOptions () {
- const { actions, dropdownWrapperStyle, useCssTransition } = this.props
- const { optionsMenuActive, menuItemStyles } = this.state
- const dropdownMenuItemStyle = {
- fontFamily: 'DIN OT',
- fontSize: 16,
- lineHeight: '24px',
- padding: '8px',
- }
-
- return h(
- Dropdown,
- {
- useCssTransition,
- style: Object.assign(
- {
- marginLeft: '-10px',
- position: 'absolute',
- width: '29vh', // affects both mobile and laptop views
- },
- dropdownWrapperStyle,
- ),
- isOpen: optionsMenuActive,
- onClickOutside: (event) => {
- const { classList } = event.target
- const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
- if (optionsMenuActive && isNotToggleElement) {
- this.setState({ optionsMenuActive: false })
- }
- },
- },
- [
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- this.props.actions.showAccountDetailModal()
- },
- style: Object.assign(
- dropdownMenuItemStyle,
- menuItemStyles,
- ),
- },
- this.context.t('accountDetails'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- const { selected, network } = this.props
- const url = genAccountLink(selected, network)
- global.platform.openWindow({ url })
- },
- style: Object.assign(
- dropdownMenuItemStyle,
- menuItemStyles,
- ),
- },
- this.context.t('etherscanView'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- const { selected } = this.props
- copyToClipboard(checksumAddress(selected))
- },
- style: Object.assign(
- dropdownMenuItemStyle,
- menuItemStyles,
- ),
- },
- this.context.t('copyAddress'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => this.props.actions.showExportPrivateKeyModal(),
- style: Object.assign(
- dropdownMenuItemStyle,
- menuItemStyles,
- ),
- },
- this.context.t('exportPrivateKey'),
- ),
- h(
- DropdownMenuItem,
- {
- closeMenu: () => {},
- onClick: () => {
- actions.hideSidebar()
- actions.showAddTokenPage()
- },
- style: Object.assign(
- dropdownMenuItemStyle,
- menuItemStyles,
- ),
- },
- this.context.t('addToken'),
- ),
-
- ]
- )
- }
-
- render () {
- const { style, enableAccountsSelector, enableAccountOptions } = this.props
- const { optionsMenuActive, accountSelectorActive } = this.state
-
- return h(
- 'span',
- {
- style: style,
- },
- [
- enableAccountsSelector && h(
- 'i.fa.fa-angle-down',
- {
- style: {
- cursor: 'pointer',
- },
- onClick: (event) => {
- event.stopPropagation()
- this.setState({
- accountSelectorActive: !accountSelectorActive,
- optionsMenuActive: false,
- })
- },
- },
- this.renderAccountSelector(),
- ),
- enableAccountOptions && h(
- 'i.fa.fa-ellipsis-h',
- {
- style: {
- fontSize: '135%',
- cursor: 'pointer',
- },
- onClick: (event) => {
- event.stopPropagation()
- this.setState({
- accountSelectorActive: false,
- optionsMenuActive: !optionsMenuActive,
- })
- },
- },
- this.renderAccountOptions()
- ),
- ]
- )
- }
-}
-
-AccountDropdowns.defaultProps = {
- enableAccountsSelector: false,
- enableAccountOptions: false,
-}
-
-AccountDropdowns.propTypes = {
- identities: PropTypes.objectOf(PropTypes.object),
- selected: PropTypes.string,
- keyrings: PropTypes.array,
- accounts: PropTypes.object,
- menuItemStyles: PropTypes.object,
- actions: PropTypes.object,
- // actions.showAccountDetail: ,
- useCssTransition: PropTypes.bool,
- innerStyle: PropTypes.object,
- sidebarOpen: PropTypes.bool,
- dropdownWrapperStyle: PropTypes.string,
- // actions.showAccountDetailModal: ,
- network: PropTypes.number,
- // actions.showExportPrivateKeyModal: ,
- style: PropTypes.object,
- ticker: PropTypes.string,
- enableAccountsSelector: PropTypes.bool,
- enableAccountOption: PropTypes.bool,
- enableAccountOptions: PropTypes.bool,
- t: PropTypes.func,
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- actions: {
- hideSidebar: () => dispatch(actions.hideSidebar()),
- showConfigPage: () => dispatch(actions.showConfigPage()),
- showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
- showAccountDetailModal: () => {
- dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
- },
- showEditAccountModal: (identity) => {
- dispatch(actions.showModal({
- name: 'EDIT_ACCOUNT_NAME',
- identity,
- }))
- },
- showNewAccountPageCreateForm: () => dispatch(actions.showNewAccountPage({ form: 'CREATE' })),
- showExportPrivateKeyModal: () => {
- dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
- },
- showAddTokenPage: () => {
- dispatch(actions.showAddTokenPage())
- },
- addNewAccount: () => dispatch(actions.addNewAccount()),
- showNewAccountPageImportForm: () => dispatch(actions.showNewAccountPage({ form: 'IMPORT' })),
- showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
- },
- }
-}
-
-function mapStateToProps (state) {
- return {
- ticker: state.metamask.ticker,
- keyrings: state.metamask.keyrings,
- sidebarOpen: state.appState.sidebar.isOpen,
- }
-}
-
-AccountDropdowns.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns)
-
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
deleted file mode 100644
index 90355a97c..000000000
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ /dev/null
@@ -1,378 +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 { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const actions = require('../../actions')
-const Dropdown = require('./components/dropdown').Dropdown
-const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
-const NetworkDropdownIcon = require('./components/network-dropdown-icon')
-const R = require('ramda')
-const { SETTINGS_ROUTE } = require('../../routes')
-
-// classes from nodes of the toggle element.
-const notToggleElementClassnames = [
- 'menu-icon',
- 'network-name',
- 'network-indicator',
- 'network-caret',
- 'network-component',
-]
-
-function mapStateToProps (state) {
- return {
- provider: state.metamask.provider,
- frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
- networkDropdownOpen: state.appState.networkDropdownOpen,
- network: state.metamask.network,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- setProviderType: (type) => {
- dispatch(actions.setProviderType(type))
- },
- setDefaultRpcTarget: type => {
- dispatch(actions.setDefaultRpcTarget(type))
- },
- setRpcTarget: (target, network, ticker, nickname) => {
- dispatch(actions.setRpcTarget(target, network, ticker, nickname))
- },
- delRpcTarget: (target) => {
- dispatch(actions.delRpcTarget(target))
- },
- showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
- hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
- }
-}
-
-
-inherits(NetworkDropdown, Component)
-function NetworkDropdown () {
- Component.call(this)
-}
-
-NetworkDropdown.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(NetworkDropdown)
-
-
-// TODO: specify default props and proptypes
-NetworkDropdown.prototype.render = function () {
- const props = this.props
- const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
- const rpcListDetail = props.frequentRpcListDetail
- const isOpen = this.props.networkDropdownOpen
- const dropdownMenuItemStyle = {
- fontSize: '16px',
- lineHeight: '20px',
- padding: '12px 0',
- }
-
- return h(Dropdown, {
- isOpen,
- onClickOutside: (event) => {
- const { classList } = event.target
- const isInClassList = className => classList.contains(className)
- const notToggleElementIndex = R.findIndex(isInClassList)(notToggleElementClassnames)
-
- if (notToggleElementIndex === -1) {
- this.props.hideNetworkDropdown()
- }
- },
- containerClassName: 'network-droppo',
- zIndex: 55,
- style: {
- position: 'absolute',
- top: '58px',
- width: '309px',
- zIndex: '55px',
- },
- innerStyle: {
- padding: '18px 8px',
- },
- }, [
-
- h('div.network-dropdown-header', {}, [
- h('div.network-dropdown-title', {}, this.context.t('networks')),
-
- h('div.network-dropdown-divider'),
-
- h('div.network-dropdown-content',
- {},
- this.context.t('defaultNetwork')
- ),
- ]),
-
- h(
- DropdownMenuItem,
- {
- key: 'main',
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.handleClick('mainnet'),
- style: { ...dropdownMenuItemStyle, borderColor: '#038789' },
- },
- [
- providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- backgroundColor: '#29B6AF', // $java
- isSelected: providerType === 'mainnet',
- }),
- h('span.network-name-item', {
- style: {
- color: providerType === 'mainnet' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('mainnet')),
- ]
- ),
-
- h(
- DropdownMenuItem,
- {
- key: 'ropsten',
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.handleClick('ropsten'),
- style: dropdownMenuItemStyle,
- },
- [
- providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- backgroundColor: '#ff4a8d', // $wild-strawberry
- isSelected: providerType === 'ropsten',
- }),
- h('span.network-name-item', {
- style: {
- color: providerType === 'ropsten' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('ropsten')),
- ]
- ),
-
- h(
- DropdownMenuItem,
- {
- key: 'kovan',
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.handleClick('kovan'),
- style: dropdownMenuItemStyle,
- },
- [
- providerType === 'kovan' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- backgroundColor: '#7057ff', // $cornflower-blue
- isSelected: providerType === 'kovan',
- }),
- h('span.network-name-item', {
- style: {
- color: providerType === 'kovan' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('kovan')),
- ]
- ),
-
- h(
- DropdownMenuItem,
- {
- key: 'rinkeby',
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.handleClick('rinkeby'),
- style: dropdownMenuItemStyle,
- },
- [
- providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- backgroundColor: '#f6c343', // $saffron
- isSelected: providerType === 'rinkeby',
- }),
- h('span.network-name-item', {
- style: {
- color: providerType === 'rinkeby' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('rinkeby')),
- ]
- ),
-
- h(
- DropdownMenuItem,
- {
- key: 'default',
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.handleClick('localhost'),
- style: dropdownMenuItemStyle,
- },
- [
- providerType === 'localhost' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- isSelected: providerType === 'localhost',
- innerBorder: '1px solid #9b9b9b',
- }),
- h('span.network-name-item', {
- style: {
- color: providerType === 'localhost' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('localhost')),
- ]
- ),
-
- this.renderCustomOption(props.provider),
- this.renderCommonRpc(rpcListDetail, props.provider),
-
- h(
- DropdownMenuItem,
- {
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => this.props.history.push(SETTINGS_ROUTE),
- style: dropdownMenuItemStyle,
- },
- [
- activeNetwork === 'custom' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
- h(NetworkDropdownIcon, {
- isSelected: activeNetwork === 'custom',
- innerBorder: '1px solid #9b9b9b',
- }),
- h('span.network-name-item', {
- style: {
- color: activeNetwork === 'custom' ? '#ffffff' : '#9b9b9b',
- },
- }, this.context.t('customRPC')),
- ]
- ),
-
- ])
-}
-
-NetworkDropdown.prototype.handleClick = function (newProviderType) {
- const { provider: { type: providerType }, setProviderType } = this.props
- const { metricsEvent } = this.context
-
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Switched Networks',
- },
- customVariables: {
- fromNetwork: providerType,
- toNetwork: newProviderType,
- },
- })
- setProviderType(newProviderType)
-}
-
-NetworkDropdown.prototype.getNetworkName = function () {
- const { provider } = this.props
- const providerName = provider.type
-
- let name
-
- if (providerName === 'mainnet') {
- name = this.context.t('mainnet')
- } else if (providerName === 'ropsten') {
- name = this.context.t('ropsten')
- } else if (providerName === 'kovan') {
- name = this.context.t('kovan')
- } else if (providerName === 'rinkeby') {
- name = this.context.t('rinkeby')
- } else {
- name = provider.nickname || this.context.t('unknownNetwork')
- }
-
- return name
-}
-
-NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) {
- const props = this.props
- const reversedRpcListDetail = rpcListDetail.slice().reverse()
-
- return reversedRpcListDetail.map((entry) => {
- const rpc = entry.rpcUrl
- const ticker = entry.ticker || 'ETH'
- const nickname = entry.nickname || ''
- const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
-
- if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
- return null
- } else {
- const chainId = entry.chainId
- return h(
- DropdownMenuItem,
- {
- key: `common${rpc}`,
- closeMenu: () => this.props.hideNetworkDropdown(),
- onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname),
- style: {
- fontSize: '16px',
- lineHeight: '20px',
- padding: '12px 0',
- },
- },
- [
- 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: currentRpcTarget ? '#ffffff' : '#9b9b9b',
- },
- }, nickname || rpc),
- h('i.fa.fa-times.delete',
- {
- onClick: (e) => {
- e.stopPropagation()
- props.delRpcTarget(rpc)
- },
- }),
- ]
- )
- }
- })
-}
-
-NetworkDropdown.prototype.renderCustomOption = function (provider) {
- const { rpcTarget, type, ticker, nickname } = provider
- const props = this.props
- const network = props.network
-
- if (type !== 'rpc') return null
-
- switch (rpcTarget) {
-
- case 'http://localhost:8545':
- return null
-
- default:
- return h(
- DropdownMenuItem,
- {
- key: rpcTarget,
- onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname),
- closeMenu: () => this.props.hideNetworkDropdown(),
- style: {
- fontSize: '16px',
- lineHeight: '20px',
- padding: '12px 0',
- },
- },
- [
- h('i.fa.fa-check'),
- h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
- h('span.network-name-item', {
- style: {
- color: '#ffffff',
- },
- }, nickname || rpcTarget),
- ]
- )
- }
-}
diff --git a/ui/app/components/dropdowns/tests/network-dropdown.test.js b/ui/app/components/dropdowns/tests/network-dropdown.test.js
deleted file mode 100644
index 88ad56851..000000000
--- a/ui/app/components/dropdowns/tests/network-dropdown.test.js
+++ /dev/null
@@ -1,97 +0,0 @@
-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',
- },
- frequentRpcListDetail: [
- { rpcUrl: '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
deleted file mode 100644
index 8a072b1bc..000000000
--- a/ui/app/components/dropdowns/token-menu-dropdown.js
+++ /dev/null
@@ -1,68 +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 genAccountLink = require('etherscan-link').createAccountLink
-const { Menu, Item, CloseArea } = require('./components/menu')
-
-TokenMenuDropdown.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown)
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showHideTokenConfirmationModal: (token) => {
- dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))
- },
- }
-}
-
-
-inherits(TokenMenuDropdown, Component)
-function TokenMenuDropdown () {
- Component.call(this)
-
- this.onClose = this.onClose.bind(this)
-}
-
-TokenMenuDropdown.prototype.onClose = function (e) {
- e.stopPropagation()
- this.props.onClose()
-}
-
-TokenMenuDropdown.prototype.render = function () {
- const { showHideTokenConfirmationModal } = this.props
-
- return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [
- h(CloseArea, {
- onClick: this.onClose,
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- showHideTokenConfirmationModal(this.props.token)
- this.props.onClose()
- },
- text: this.context.t('hideToken'),
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- const url = genAccountLink(this.props.token.address, this.props.network)
- global.platform.openWindow({ url })
- this.props.onClose()
- },
- text: this.context.t('viewOnEtherscan'),
- }),
- ])
-}
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
deleted file mode 100644
index a9167e3b2..000000000
--- a/ui/app/components/ens-input.js
+++ /dev/null
@@ -1,181 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const extend = require('xtend')
-const debounce = require('debounce')
-const copyToClipboard = require('copy-to-clipboard')
-const ENS = require('ethjs-ens')
-const networkMap = require('ethjs-ens/lib/network-map.json')
-const ensRE = /.+\..+$/
-const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
-const connect = require('react-redux').connect
-const ToAutoComplete = require('./send/to-autocomplete').default
-const log = require('loglevel')
-const { isValidENSAddress } = require('../util')
-
-EnsInput.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(EnsInput)
-
-
-inherits(EnsInput, Component)
-function EnsInput () {
- Component.call(this)
-}
-
-EnsInput.prototype.onChange = function (recipient) {
-
- const network = this.props.network
- const networkHasEnsSupport = getNetworkEnsSupport(network)
-
- this.props.onChange({ toAddress: recipient })
-
- if (!networkHasEnsSupport) return
-
- if (recipient.match(ensRE) === null) {
- return this.setState({
- loadingEns: false,
- ensResolution: null,
- ensFailure: null,
- toError: null,
- })
- }
-
- this.setState({
- loadingEns: true,
- })
- this.checkName(recipient)
-}
-
-EnsInput.prototype.render = function () {
- const props = this.props
- const opts = extend(props, {
- list: 'addresses',
- onChange: this.onChange.bind(this),
- qrScanner: true,
- })
- return h('div', {
- style: { width: '100%', position: 'relative' },
- }, [
- h(ToAutoComplete, { ...opts }),
- this.ensIcon(),
- ])
-}
-
-EnsInput.prototype.componentDidMount = function () {
- const network = this.props.network
- const networkHasEnsSupport = getNetworkEnsSupport(network)
- this.setState({ ensResolution: ZERO_ADDRESS })
-
- if (networkHasEnsSupport) {
- const provider = global.ethereumProvider
- this.ens = new ENS({ provider, network })
- this.checkName = debounce(this.lookupEnsName.bind(this), 200)
- }
-}
-
-EnsInput.prototype.lookupEnsName = function (recipient) {
- const { ensResolution } = this.state
-
- log.info(`ENS attempting to resolve name: ${recipient}`)
- this.ens.lookup(recipient.trim())
- .then((address) => {
- if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName'))
- if (address !== ensResolution) {
- this.setState({
- loadingEns: false,
- ensResolution: address,
- nickname: recipient.trim(),
- hoverText: address + '\n' + this.context.t('clickCopy'),
- ensFailure: false,
- toError: null,
- })
- }
- })
- .catch((reason) => {
- const setStateObj = {
- loadingEns: false,
- ensResolution: recipient,
- ensFailure: true,
- toError: null,
- }
- if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
- setStateObj.hoverText = this.context.t('ensNameNotFound')
- setStateObj.toError = 'ensNameNotFound'
- setStateObj.ensFailure = false
- } else {
- log.error(reason)
- setStateObj.hoverText = reason.message
- }
-
- return this.setState(setStateObj)
- })
-}
-
-EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
- const state = this.state || {}
- const ensResolution = state.ensResolution
- // If an address is sent without a nickname, meaning not from ENS or from
- // the user's own accounts, a default of a one-space string is used.
- const nickname = state.nickname || ' '
- if (prevProps.network !== this.props.network) {
- const provider = global.ethereumProvider
- this.ens = new ENS({ provider, network: this.props.network })
- this.onChange(ensResolution)
- }
- if (prevState && ensResolution && this.props.onChange &&
- ensResolution !== prevState.ensResolution) {
- this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
- }
-}
-
-EnsInput.prototype.ensIcon = function (recipient) {
- const { hoverText } = this.state || {}
- return h('span.#ensIcon', {
- title: hoverText,
- style: {
- position: 'absolute',
- top: '16px',
- left: '-25px',
- },
- }, this.ensIconContents(recipient))
-}
-
-EnsInput.prototype.ensIconContents = function (recipient) {
- const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
-
- if (toError) return
-
- if (loadingEns) {
- return h('img', {
- src: 'images/loading.svg',
- style: {
- width: '30px',
- height: '30px',
- transform: 'translateY(-6px)',
- },
- })
- }
-
- if (ensFailure) {
- return h('i.fa.fa-warning.fa-lg.warning')
- }
-
- if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
- return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
- style: { color: 'green' },
- onClick: (event) => {
- event.preventDefault()
- event.stopPropagation()
- copyToClipboard(ensResolution)
- },
- })
- }
-}
-
-function getNetworkEnsSupport (network) {
- return Boolean(networkMap[network])
-}
diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js
deleted file mode 100644
index 2f6395a2d..000000000
--- a/ui/app/components/eth-balance.js
+++ /dev/null
@@ -1,102 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const { inherits } = require('util')
-const {
- formatBalance,
- generateBalanceObject,
-} = require('../util')
-const Tooltip = require('./tooltip.js')
-const FiatValue = require('./fiat-value.js')
-
-module.exports = connect(mapStateToProps)(EthBalanceComponent)
-function mapStateToProps (state) {
- return {
- ticker: state.metamask.ticker,
- }
-}
-
-inherits(EthBalanceComponent, Component)
-function EthBalanceComponent () {
- Component.call(this)
-}
-
-EthBalanceComponent.prototype.render = function () {
- const props = this.props
- const { ticker, value, style, width, needsParse = true } = props
-
- const formattedValue = value ? formatBalance(value, 6, needsParse, ticker) : '...'
-
- return (
-
- h('.ether-balance.ether-balance-amount', {
- style,
- }, [
- h('div', {
- style: {
- display: 'inline',
- width,
- },
- }, this.renderBalance(formattedValue)),
- ])
-
- )
-}
-EthBalanceComponent.prototype.renderBalance = function (value) {
- if (value === 'None') return value
- if (value === '...') return value
-
- const {
- conversionRate,
- shorten,
- incoming,
- currentCurrency,
- hideTooltip,
- styleOveride = {},
- showFiat = true,
- } = this.props
- const { fontSize, color, fontFamily, lineHeight } = styleOveride
-
- const { shortBalance, balance, label } = generateBalanceObject(value, shorten ? 1 : 3)
- const balanceToRender = shorten ? shortBalance : balance
-
- const [ethNumber, ethSuffix] = value.split(' ')
- const containerProps = hideTooltip ? {} : {
- position: 'bottom',
- title: `${ethNumber} ${ethSuffix}`,
- }
-
- return (
- h(hideTooltip ? 'div' : Tooltip,
- containerProps,
- h('div.flex-column', [
- h('.flex-row', {
- style: {
- alignItems: 'flex-end',
- lineHeight: lineHeight || '13px',
- fontFamily: fontFamily || 'Montserrat Light',
- textRendering: 'geometricPrecision',
- },
- }, [
- h('div', {
- style: {
- width: '100%',
- textAlign: 'right',
- fontSize: fontSize || 'inherit',
- color: color || 'inherit',
- },
- }, incoming ? `+${balanceToRender}` : balanceToRender),
- h('div', {
- style: {
- color: color || '#AEAEAE',
- fontSize: fontSize || '12px',
- marginLeft: '5px',
- },
- }, label),
- ]),
-
- showFiat ? h(FiatValue, { value: this.props.value, conversionRate, currentCurrency }) : null,
- ])
- )
- )
-}
diff --git a/ui/app/components/export-text-container/export-text-container.component.js b/ui/app/components/export-text-container/export-text-container.component.js
deleted file mode 100644
index c2546fa9b..000000000
--- a/ui/app/components/export-text-container/export-text-container.component.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const copyToClipboard = require('copy-to-clipboard')
-const { exportAsFile } = require('../../util')
-
-class ExportTextContainer extends Component {
- render () {
- const { text = '', filename = '' } = this.props
- const { t } = this.context
-
- return (
- h('.export-text-container', [
- h('.export-text-container__text-container', [
- h('.export-text-container__text', text),
- ]),
- h('.export-text-container__buttons-container', [
- h('.export-text-container__button.export-text-container__button--copy', {
- onClick: () => copyToClipboard(text),
- }, [
- h('img', { src: 'images/copy-to-clipboard.svg' }),
- h('.export-text-container__button-text', t('copyToClipboard')),
- ]),
- h('.export-text-container__button', {
- onClick: () => exportAsFile(filename, text),
- }, [
- h('img', { src: 'images/download.svg' }),
- h('.export-text-container__button-text', t('saveAsCsvFile')),
- ]),
- ]),
- ])
- )
- }
-}
-
-ExportTextContainer.propTypes = {
- text: PropTypes.string,
- filename: PropTypes.string,
-}
-
-ExportTextContainer.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = ExportTextContainer
diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js
deleted file mode 100644
index 56465fc9d..000000000
--- a/ui/app/components/fiat-value.js
+++ /dev/null
@@ -1,66 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const formatBalance = require('../util').formatBalance
-
-module.exports = FiatValue
-
-inherits(FiatValue, Component)
-function FiatValue () {
- Component.call(this)
-}
-
-FiatValue.prototype.render = function () {
- const props = this.props
- const { conversionRate, currentCurrency, style } = props
- const renderedCurrency = currentCurrency || ''
-
- const value = formatBalance(props.value, 6)
-
- if (value === 'None') return value
- var fiatDisplayNumber, fiatTooltipNumber
- var splitBalance = value.split(' ')
-
- if (conversionRate !== 0) {
- fiatTooltipNumber = Number(splitBalance[0]) * conversionRate
- fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
- } else {
- fiatDisplayNumber = 'N/A'
- fiatTooltipNumber = 'Unknown'
- }
-
- return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase(), style)
-}
-
-function fiatDisplay (fiatDisplayNumber, fiatSuffix, styleOveride = {}) {
- const { fontSize, color, fontFamily, lineHeight } = styleOveride
-
- if (fiatDisplayNumber !== 'N/A') {
- return h('.flex-row', {
- style: {
- alignItems: 'flex-end',
- lineHeight: lineHeight || '13px',
- fontFamily: fontFamily || 'Montserrat Light',
- textRendering: 'geometricPrecision',
- },
- }, [
- h('div', {
- style: {
- width: '100%',
- textAlign: 'right',
- fontSize: fontSize || '12px',
- color: color || '#333333',
- },
- }, fiatDisplayNumber),
- h('div', {
- style: {
- color: color || '#AEAEAE',
- marginLeft: '5px',
- fontSize: fontSize || '12px',
- },
- }, fiatSuffix),
- ])
- } else {
- return h('div')
- }
-}
diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
deleted file mode 100644
index a71d37b43..000000000
--- a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { connect } from 'react-redux'
-import { showModal } from '../../../actions'
-import {
- decGWEIToHexWEI,
- decimalToHex,
- hexWEIToDecGWEI,
-} from '../../../helpers/conversions.util'
-import AdvancedGasInputs from './advanced-gas-inputs.component'
-
-function convertGasPriceForInputs (gasPriceInHexWEI) {
- return Number(hexWEIToDecGWEI(gasPriceInHexWEI))
-}
-
-function convertGasLimitForInputs (gasLimitInHexWEI) {
- return parseInt(gasLimitInHexWEI, 16)
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })),
- showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const {customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit} = ownProps
- return {
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- customGasPrice: convertGasPriceForInputs(customGasPrice),
- customGasLimit: convertGasLimitForInputs(customGasLimit),
- updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)),
- updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)),
- }
-}
-
-export default connect(null, mapDispatchToProps, mergeProps)(AdvancedGasInputs)
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
deleted file mode 100644
index a3a3f96d8..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
+++ /dev/null
@@ -1,219 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import Loading from '../../../loading-screen'
-import GasPriceChart from '../../gas-price-chart'
-import debounce from 'lodash.debounce'
-
-export default class AdvancedTabContent extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- updateCustomGasPrice: PropTypes.func,
- updateCustomGasLimit: PropTypes.func,
- customGasPrice: PropTypes.number,
- customGasLimit: PropTypes.number,
- gasEstimatesLoading: PropTypes.bool,
- millisecondsRemaining: PropTypes.number,
- transactionFee: PropTypes.string,
- timeRemaining: PropTypes.string,
- gasChartProps: PropTypes.object,
- insufficientBalance: PropTypes.bool,
- customPriceIsSafe: PropTypes.bool,
- isSpeedUp: PropTypes.bool,
- }
-
- constructor (props) {
- super(props)
-
- this.debouncedGasLimitReset = debounce((dVal) => {
- if (dVal < 21000) {
- props.updateCustomGasLimit(21000)
- }
- }, 1000, { trailing: true })
- this.onChangeGasLimit = (val) => {
- props.updateCustomGasLimit(val)
- this.debouncedGasLimitReset(val)
- }
- }
-
- gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) {
- const { t } = this.context
- let errorText
- let errorType
- let isInError = true
-
-
- if (insufficientBalance) {
- errorText = t('insufficientBalance')
- errorType = 'error'
- } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) {
- errorText = t('zeroGasPriceOnSpeedUpError')
- errorType = 'error'
- } else if (labelKey === 'gasPrice' && !customPriceIsSafe) {
- errorText = t('gasPriceExtremelyLow')
- errorType = 'warning'
- } else {
- isInError = false
- }
-
- return {
- isInError,
- errorText,
- errorType,
- }
- }
-
- gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) {
- const {
- isInError,
- errorText,
- errorType,
- } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value })
-
- return (
- <div className="advanced-tab__gas-edit-row__input-wrapper">
- <input
- className={classnames('advanced-tab__gas-edit-row__input', {
- 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
- 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
- })}
- type="number"
- value={value}
- onChange={event => onChange(Number(event.target.value))}
- />
- <div className={classnames('advanced-tab__gas-edit-row__input-arrows', {
- 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
- 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
- })}>
- <div
- className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
- onClick={() => onChange(value + 1)}
- >
- <i className="fa fa-sm fa-angle-up" />
- </div>
- <div
- className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
- onClick={() => onChange(Math.max(value - 1, 0))}
- >
- <i className="fa fa-sm fa-angle-down" />
- </div>
- </div>
- { isInError
- ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}>
- { errorText }
- </div>
- : null }
- </div>
- )
- }
-
- infoButton (onClick) {
- return <i className="fa fa-info-circle" onClick={onClick} />
- }
-
- renderDataSummary (transactionFee, timeRemaining) {
- return (
- <div className="advanced-tab__transaction-data-summary">
- <div className="advanced-tab__transaction-data-summary__titles">
- <span>{ this.context.t('newTransactionFee') }</span>
- <span>~{ this.context.t('transactionTime') }</span>
- </div>
- <div className="advanced-tab__transaction-data-summary__container">
- <div className="advanced-tab__transaction-data-summary__fee">
- {transactionFee}
- </div>
- <div className="time-remaining">{timeRemaining}</div>
- </div>
- </div>
- )
- }
-
- renderGasEditRow (gasInputArgs) {
- return (
- <div className="advanced-tab__gas-edit-row">
- <div className="advanced-tab__gas-edit-row__label">
- { this.context.t(gasInputArgs.labelKey) }
- { this.infoButton(() => {}) }
- </div>
- { this.gasInput(gasInputArgs) }
- </div>
- )
- }
-
- renderGasEditRows ({
- customGasPrice,
- updateCustomGasPrice,
- customGasLimit,
- updateCustomGasLimit,
- insufficientBalance,
- customPriceIsSafe,
- isSpeedUp,
- }) {
- return (
- <div className="advanced-tab__gas-edit-rows">
- { this.renderGasEditRow({
- labelKey: 'gasPrice',
- value: customGasPrice,
- onChange: updateCustomGasPrice,
- insufficientBalance,
- customPriceIsSafe,
- showGWEI: true,
- isSpeedUp,
- }) }
- { this.renderGasEditRow({
- labelKey: 'gasLimit',
- value: customGasLimit,
- onChange: this.onChangeGasLimit,
- insufficientBalance,
- customPriceIsSafe,
- }) }
- </div>
- )
- }
-
- render () {
- const { t } = this.context
- const {
- updateCustomGasPrice,
- updateCustomGasLimit,
- timeRemaining,
- customGasPrice,
- customGasLimit,
- insufficientBalance,
- gasChartProps,
- gasEstimatesLoading,
- customPriceIsSafe,
- isSpeedUp,
- transactionFee,
- } = this.props
-
- return (
- <div className="advanced-tab">
- { this.renderDataSummary(transactionFee, timeRemaining) }
- <div className="advanced-tab__fee-chart">
- { this.renderGasEditRows({
- customGasPrice,
- updateCustomGasPrice,
- customGasLimit,
- updateCustomGasLimit,
- insufficientBalance,
- customPriceIsSafe,
- isSpeedUp,
- }) }
- <div className="advanced-tab__fee-chart__title">{ t('liveGasPricePredictions') }</div>
- {!gasEstimatesLoading
- ? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />
- : <Loading />
- }
- <div className="advanced-tab__fee-chart__speed-buttons">
- <span>{ t('slower') }</span>
- <span>{ t('faster') }</span>
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
deleted file mode 100644
index 53cb84791..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
+++ /dev/null
@@ -1,203 +0,0 @@
-@import './time-remaining/index';
-
-.advanced-tab {
- display: flex;
- flex-flow: column;
-
- &__transaction-data-summary,
- &__fee-chart-title {
- padding-left: 24px;
- padding-right: 24px;
- }
-
- &__transaction-data-summary {
- display: flex;
- flex-flow: column;
- color: $mid-gray;
- margin-top: 12px;
- padding-left: 18px;
- padding-right: 18px;
-
- &__titles,
- &__container {
- display: flex;
- flex-flow: row;
- justify-content: space-between;
- font-size: 12px;
- color: #888EA3;
- }
-
- &__container {
- font-size: 16px;
- margin-top: 0px;
- }
-
- &__fee {
- font-size: 16px;
- color: #313A5E;
- }
- }
-
- &__fee-chart {
- margin-top: 8px;
- height: 265px;
- background: #F8F9FB;
- border-bottom: 1px solid #d2d8dd;
- border-top: 1px solid #d2d8dd;
- position: relative;
-
- &__title {
- font-size: 12px;
- color: #313A5E;
- margin-left: 22px;
- }
-
- &__speed-buttons {
- position: absolute;
- bottom: 13px;
- display: flex;
- justify-content: space-between;
- padding-left: 20px;
- padding-right: 19px;
- width: 100%;
- font-size: 10px;
- color: #888EA3;
- }
- }
-
- &__slider-container {
- padding-left: 27px;
- padding-right: 27px;
- }
-
- &__gas-edit-rows {
- height: 73px;
- display: flex;
- flex-flow: row;
- justify-content: space-between;
- margin-left: 20px;
- margin-right: 10px;
- margin-top: 9px;
- }
-
- &__gas-edit-row {
- display: flex;
- flex-flow: column;
-
- &__label {
- color: #313B5E;
- font-size: 14px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .fa-info-circle {
- color: $silver;
- margin-left: 10px;
- cursor: pointer;
- }
-
- .fa-info-circle:hover {
- color: $mid-gray;
- }
- }
-
- &__error-text {
- font-size: 12px;
- color: red;
- }
-
- &__warning-text {
- font-size: 12px;
- color: orange;
- }
-
- &__input-wrapper {
- position: relative;
- }
-
- &__input {
- border: 1px solid $dusty-gray;
- border-radius: 4px;
- color: $mid-gray;
- font-size: 16px;
- height: 24px;
- width: 155px;
- padding-left: 8px;
- padding-top: 2px;
- margin-top: 7px;
- }
-
- &__input--error {
- border: 1px solid $red;
- }
-
- &__input--warning {
- border: 1px solid $orange;
- }
-
- &__input-arrows {
- position: absolute;
- top: 7px;
- right: 0px;
- width: 17px;
- height: 24px;
- border: 1px solid #dadada;
- border-top-right-radius: 4px;
- display: flex;
- flex-direction: column;
- color: #9b9b9b;
- font-size: .8em;
- border-bottom-right-radius: 4px;
- cursor: pointer;
-
- &__i-wrap {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- cursor: pointer;
- }
-
- &__i-wrap:hover {
- background: #4EADE7;
- color: $white;
- }
-
- i:hover {
- background: #4EADE7;
- }
-
- i {
- font-size: 10px;
- }
- }
-
- &__input-arrows--error {
- border: 1px solid $red;
- }
-
- &__input-arrows--warning {
- border: 1px solid $orange;
- }
-
- input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- -moz-appearance: none;
- display: none;
- }
-
- input[type="number"]:hover::-webkit-inner-spin-button {
- -webkit-appearance: none;
- -moz-appearance: none;
- display: none;
- }
-
- &__gwei-symbol {
- position: absolute;
- top: 8px;
- right: 10px;
- color: $dusty-gray;
- }
- }
-} \ No newline at end of file
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
deleted file mode 100644
index 2500ee267..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
+++ /dev/null
@@ -1,364 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import shallow from '../../../../../../lib/shallow-with-context'
-import sinon from 'sinon'
-import AdvancedTabContent from '../advanced-tab-content.component.js'
-
-import GasPriceChart from '../../../gas-price-chart'
-import Loading from '../../../../loading-screen'
-
-const propsMethodSpies = {
- updateCustomGasPrice: sinon.spy(),
- updateCustomGasLimit: sinon.spy(),
-}
-
-sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow')
-sinon.spy(AdvancedTabContent.prototype, 'gasInput')
-sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows')
-sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary')
-sinon.spy(AdvancedTabContent.prototype, 'gasInputError')
-
-describe('AdvancedTabContent Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<AdvancedTabContent
- updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice}
- updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit}
- customGasPrice={11}
- customGasLimit={23456}
- timeRemaining={21500}
- transactionFee={'$0.25'}
- insufficientBalance={false}
- customPriceIsSafe={true}
- isSpeedUp={false}
- />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
- })
-
- afterEach(() => {
- propsMethodSpies.updateCustomGasPrice.resetHistory()
- propsMethodSpies.updateCustomGasLimit.resetHistory()
- AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
- AdvancedTabContent.prototype.gasInput.resetHistory()
- AdvancedTabContent.prototype.renderGasEditRows.resetHistory()
- AdvancedTabContent.prototype.renderDataSummary.resetHistory()
- })
-
- describe('render()', () => {
- it('should render the advanced-tab root node', () => {
- assert(wrapper.hasClass('advanced-tab'))
- })
-
- it('should render the expected four children of the advanced-tab div', () => {
- const advancedTabChildren = wrapper.children()
- assert.equal(advancedTabChildren.length, 2)
-
- assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
- assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
-
- const feeChartDiv = advancedTabChildren.at(1)
-
- assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
- assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title'))
- assert(feeChartDiv.childAt(2).is(GasPriceChart))
- assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons'))
- })
-
- it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => {
- wrapper.setProps({ gasEstimatesLoading: true })
- const advancedTabChildren = wrapper.children()
- assert.equal(advancedTabChildren.length, 2)
-
- assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
- assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
-
- const feeChartDiv = advancedTabChildren.at(1)
-
- assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
- assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title'))
- assert(feeChartDiv.childAt(2).is(Loading))
- assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons'))
- })
-
- it('should call renderDataSummary with the expected params', () => {
- assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
- const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args
- assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500])
- })
-
- it('should call renderGasEditRows with the expected params', () => {
- assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
- const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
- assert.deepEqual(renderGasEditRowArgs, [{
- customGasPrice: 11,
- customGasLimit: 23456,
- insufficientBalance: false,
- customPriceIsSafe: true,
- updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
- updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit,
- isSpeedUp: false,
- }])
- })
- })
-
- describe('renderDataSummary()', () => {
- let dataSummary
-
- beforeEach(() => {
- dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining'))
- })
-
- it('should render the transaction-data-summary root node', () => {
- assert(dataSummary.hasClass('advanced-tab__transaction-data-summary'))
- })
-
- it('should render titles of the data', () => {
- const titlesNode = dataSummary.children().at(0)
- assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles'))
- assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee')
- assert.equal(titlesNode.children().at(1).text(), '~transactionTime')
- })
-
- it('should render the data', () => {
- const dataNode = dataSummary.children().at(1)
- assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container'))
- assert.equal(dataNode.children().at(0).text(), 'mockTotalFee')
- assert(dataNode.children().at(1).hasClass('time-remaining'))
- assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining')
- })
- })
-
- describe('renderGasEditRow()', () => {
- let gasEditRow
-
- beforeEach(() => {
- AdvancedTabContent.prototype.gasInput.resetHistory()
- gasEditRow = shallow(wrapper.instance().renderGasEditRow({
- labelKey: 'mockLabelKey',
- someArg: 'argA',
- }))
- })
-
- it('should render the gas-edit-row root node', () => {
- assert(gasEditRow.hasClass('advanced-tab__gas-edit-row'))
- })
-
- it('should render a label and an input', () => {
- const gasEditRowChildren = gasEditRow.children()
- assert.equal(gasEditRowChildren.length, 2)
- assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label'))
- assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper'))
- })
-
- it('should render the label key and info button', () => {
- const gasRowLabelChildren = gasEditRow.children().at(0).children()
- assert.equal(gasRowLabelChildren.length, 2)
- assert(gasRowLabelChildren.at(0), 'mockLabelKey')
- assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle'))
- })
-
- it('should call this.gasInput with the correct args', () => {
- const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args
- assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ])
- })
- })
-
- describe('renderGasEditRows()', () => {
- let gasEditRows
- let tempOnChangeGasLimit
-
- beforeEach(() => {
- tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit
- wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit'
- AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
- gasEditRows = shallow(wrapper.instance().renderGasEditRows(
- 'mockGasPrice',
- () => 'mockUpdateCustomGasPriceReturn',
- 'mockGasLimit',
- () => 'mockUpdateCustomGasLimitReturn',
- false
- ))
- })
-
- afterEach(() => {
- wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit
- })
-
- it('should render the gas-edit-rows root node', () => {
- assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows'))
- })
-
- it('should render two rows', () => {
- const gasEditRowsChildren = gasEditRows.children()
- assert.equal(gasEditRowsChildren.length, 2)
- assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row'))
- assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row'))
- })
-
- it('should call this.renderGasEditRow twice, with the expected args', () => {
- const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
- assert.equal(renderGasEditRowSpyArgs.length, 2)
- assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{
- labelKey: 'gasPrice',
- value: 'mockGasLimit',
- onChange: () => 'mockOnChangeGasLimit',
- insufficientBalance: false,
- customPriceIsSafe: true,
- showGWEI: true,
- }].map(String))
- assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{
- labelKey: 'gasPrice',
- value: 'mockGasPrice',
- onChange: () => 'mockUpdateCustomGasPriceReturn',
- insufficientBalance: false,
- customPriceIsSafe: true,
- showGWEI: true,
- }].map(String))
- })
- })
-
- describe('infoButton()', () => {
- let infoButton
-
- beforeEach(() => {
- AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
- infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn'))
- })
-
- it('should render the i element', () => {
- assert(infoButton.hasClass('fa-info-circle'))
- })
-
- it('should pass the onClick argument to the i tag onClick prop', () => {
- assert(infoButton.props().onClick(), 'mockOnClickReturn')
- })
- })
-
- describe('gasInput()', () => {
- let gasInput
-
- beforeEach(() => {
- AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
- AdvancedTabContent.prototype.gasInputError.resetHistory()
- gasInput = shallow(wrapper.instance().gasInput({
- labelKey: 'gasPrice',
- value: 321,
- onChange: value => value + 7,
- insufficientBalance: false,
- showGWEI: true,
- customPriceIsSafe: true,
- isSpeedUp: false,
- }))
- })
-
- it('should render the input-wrapper root node', () => {
- assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper'))
- })
-
- it('should render two children, including an input', () => {
- assert.equal(gasInput.children().length, 2)
- assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input'))
- })
-
- it('should call the passed onChange method with the value of the input onChange event', () => {
- const inputOnChange = gasInput.find('input').props().onChange
- assert.equal(inputOnChange({ target: { value: 8} }), 15)
- })
-
- it('should have two input arrows', () => {
- const upArrow = gasInput.find('.fa-angle-up')
- assert.equal(upArrow.length, 1)
- const downArrow = gasInput.find('.fa-angle-down')
- assert.equal(downArrow.length, 1)
- })
-
- it('should call onChange with the value incremented decremented when its onchange method is called', () => {
- const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0)
- assert.equal(upArrow.props().onClick(), 329)
- const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1)
- assert.equal(downArrow.props().onClick(), 327)
- })
-
- it('should call gasInputError with the expected params', () => {
- assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1)
- const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args
- assert.deepEqual(gasInputErrorArgs, [{
- labelKey: 'gasPrice',
- insufficientBalance: false,
- customPriceIsSafe: true,
- value: 321,
- isSpeedUp: false,
- }])
- })
- })
-
- describe('gasInputError()', () => {
- let gasInputError
-
- beforeEach(() => {
- AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
- gasInputError = wrapper.instance().gasInputError({
- labelKey: '',
- insufficientBalance: false,
- customPriceIsSafe: true,
- isSpeedUp: false,
- })
- })
-
- it('should return an insufficientBalance error', () => {
- const gasInputError = wrapper.instance().gasInputError({
- labelKey: 'gasPrice',
- insufficientBalance: true,
- customPriceIsSafe: true,
- isSpeedUp: false,
- value: 1,
- })
- assert.deepEqual(gasInputError, {
- isInError: true,
- errorText: 'insufficientBalance',
- errorType: 'error',
- })
- })
-
- it('should return a zero gas on retry error', () => {
- const gasInputError = wrapper.instance().gasInputError({
- labelKey: 'gasPrice',
- insufficientBalance: false,
- customPriceIsSafe: false,
- isSpeedUp: true,
- value: 0,
- })
- assert.deepEqual(gasInputError, {
- isInError: true,
- errorText: 'zeroGasPriceOnSpeedUpError',
- errorType: 'error',
- })
- })
-
- it('should return a low gas warning', () => {
- const gasInputError = wrapper.instance().gasInputError({
- labelKey: 'gasPrice',
- insufficientBalance: false,
- customPriceIsSafe: false,
- isSpeedUp: false,
- value: 1,
- })
- assert.deepEqual(gasInputError, {
- isInError: true,
- errorText: 'gasPriceExtremelyLow',
- errorType: 'warning',
- })
- })
-
- it('should return isInError false if there is no error', () => {
- gasInputError = wrapper.instance().gasInputError({
- labelKey: 'gasPrice',
- insufficientBalance: false,
- customPriceIsSafe: true,
- value: 1,
- })
- assert.equal(gasInputError.isInError, false)
- })
- })
-
-})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js
deleted file mode 100644
index d8490272f..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import shallow from '../../../../../../../lib/shallow-with-context'
-import TimeRemaining from '../time-remaining.component.js'
-
-describe('TimeRemaining Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<TimeRemaining
- milliseconds={495000}
- />)
- })
-
- describe('render()', () => {
- it('should render the time-remaining root node', () => {
- assert(wrapper.hasClass('time-remaining'))
- })
-
- it('should render minutes and seconds numbers and labels', () => {
- const timeRemainingChildren = wrapper.children()
- assert.equal(timeRemainingChildren.length, 4)
- assert.equal(timeRemainingChildren.at(0).text(), 8)
- assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand')
- assert.equal(timeRemainingChildren.at(2).text(), 15)
- assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand')
- })
- })
-
-})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js
deleted file mode 100644
index 05b8f700b..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import Loading from '../../../loading-screen'
-import GasPriceButtonGroup from '../../gas-price-button-group'
-
-export default class BasicTabContent extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- gasPriceButtonGroupProps: PropTypes.object,
- }
-
- render () {
- const { t } = this.context
- const { gasPriceButtonGroupProps } = this.props
-
- return (
- <div className="basic-tab-content">
- <div className="basic-tab-content__title">{ t('estimatedProcessingTimes') }</div>
- <div className="basic-tab-content__blurb">{ t('selectAHigherGasFee') }</div>
- {!gasPriceButtonGroupProps.loading
- ? <GasPriceButtonGroup
- className="gas-price-button-group--alt"
- showCheck={true}
- {...gasPriceButtonGroupProps}
- />
- : <Loading />
- }
- <div className="basic-tab-content__footer-blurb">{ t('acceleratingATransaction') }</div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js
deleted file mode 100644
index 47864fcab..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import shallow from '../../../../../../lib/shallow-with-context'
-import BasicTabContent from '../basic-tab-content.component'
-
-import GasPriceButtonGroup from '../../../gas-price-button-group/'
-import Loading from '../../../../loading-screen'
-
-const mockGasPriceButtonGroupProps = {
- buttonDataLoading: false,
- className: 'gas-price-button-group',
- gasButtonInfo: [
- {
- feeInPrimaryCurrency: '$0.52',
- feeInSecondaryCurrency: '0.0048 ETH',
- timeEstimate: '~ 1 min 0 sec',
- priceInHexWei: '0xa1b2c3f',
- },
- {
- feeInPrimaryCurrency: '$0.39',
- feeInSecondaryCurrency: '0.004 ETH',
- timeEstimate: '~ 1 min 30 sec',
- priceInHexWei: '0xa1b2c39',
- },
- {
- feeInPrimaryCurrency: '$0.30',
- feeInSecondaryCurrency: '0.00354 ETH',
- timeEstimate: '~ 2 min 1 sec',
- priceInHexWei: '0xa1b2c30',
- },
- ],
- handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice),
- noButtonActiveByDefault: true,
- showCheck: true,
-}
-
-describe('BasicTabContent Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<BasicTabContent
- gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
- />)
- })
-
- describe('render', () => {
- it('should have a title', () => {
- assert(wrapper.find('.basic-tab-content').childAt(0).hasClass('basic-tab-content__title'))
- })
-
- it('should render a GasPriceButtonGroup compenent', () => {
- assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
- })
-
- it('should pass correct props to GasPriceButtonGroup', () => {
- const {
- buttonDataLoading,
- className,
- gasButtonInfo,
- handleGasPriceSelection,
- noButtonActiveByDefault,
- showCheck,
- } = wrapper.find(GasPriceButtonGroup).props()
- assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
- assert.equal(buttonDataLoading, mockGasPriceButtonGroupProps.buttonDataLoading)
- assert.equal(className, mockGasPriceButtonGroupProps.className)
- assert.equal(noButtonActiveByDefault, mockGasPriceButtonGroupProps.noButtonActiveByDefault)
- assert.equal(showCheck, mockGasPriceButtonGroupProps.showCheck)
- assert.deepEqual(gasButtonInfo, mockGasPriceButtonGroupProps.gasButtonInfo)
- assert.equal(JSON.stringify(handleGasPriceSelection), JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection))
- })
-
- it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => {
- wrapper.setProps({
- gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, loading: true },
- })
-
- assert.equal(wrapper.find(GasPriceButtonGroup).length, 0)
- assert.equal(wrapper.find(Loading).length, 1)
- })
- })
-})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
deleted file mode 100644
index 174bd8ea8..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainer from '../../page-container'
-import { Tabs, Tab } from '../../tabs'
-import AdvancedTabContent from './advanced-tab-content'
-import BasicTabContent from './basic-tab-content'
-
-export default class GasModalPageContainer extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- hideModal: PropTypes.func,
- hideBasic: PropTypes.bool,
- updateCustomGasPrice: PropTypes.func,
- updateCustomGasLimit: PropTypes.func,
- customGasPrice: PropTypes.number,
- customGasLimit: PropTypes.number,
- fetchBasicGasAndTimeEstimates: PropTypes.func,
- fetchGasEstimates: PropTypes.func,
- gasPriceButtonGroupProps: PropTypes.object,
- infoRowProps: PropTypes.shape({
- originalTotalFiat: PropTypes.string,
- originalTotalEth: PropTypes.string,
- newTotalFiat: PropTypes.string,
- newTotalEth: PropTypes.string,
- }),
- onSubmit: PropTypes.func,
- customModalGasPriceInHex: PropTypes.string,
- customModalGasLimitInHex: PropTypes.string,
- cancelAndClose: PropTypes.func,
- transactionFee: PropTypes.string,
- blockTime: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- customPriceIsSafe: PropTypes.bool,
- isSpeedUp: PropTypes.bool,
- disableSave: PropTypes.bool,
- }
-
- state = {}
-
- componentDidMount () {
- const promise = this.props.hideBasic
- ? Promise.resolve(this.props.blockTime)
- : this.props.fetchBasicGasAndTimeEstimates()
- .then(basicEstimates => basicEstimates.blockTime)
-
- promise
- .then(blockTime => {
- this.props.fetchGasEstimates(blockTime)
- })
- }
-
- renderBasicTabContent (gasPriceButtonGroupProps) {
- return (
- <BasicTabContent
- gasPriceButtonGroupProps={gasPriceButtonGroupProps}
- />
- )
- }
-
- renderAdvancedTabContent ({
- convertThenUpdateCustomGasPrice,
- convertThenUpdateCustomGasLimit,
- customGasPrice,
- customGasLimit,
- newTotalFiat,
- gasChartProps,
- currentTimeEstimate,
- insufficientBalance,
- gasEstimatesLoading,
- customPriceIsSafe,
- isSpeedUp,
- transactionFee,
- }) {
- return (
- <AdvancedTabContent
- updateCustomGasPrice={convertThenUpdateCustomGasPrice}
- updateCustomGasLimit={convertThenUpdateCustomGasLimit}
- customGasPrice={customGasPrice}
- customGasLimit={customGasLimit}
- timeRemaining={currentTimeEstimate}
- transactionFee={transactionFee}
- totalFee={newTotalFiat}
- gasChartProps={gasChartProps}
- insufficientBalance={insufficientBalance}
- gasEstimatesLoading={gasEstimatesLoading}
- customPriceIsSafe={customPriceIsSafe}
- isSpeedUp={isSpeedUp}
- />
- )
- }
-
- renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) {
- return (
- <div className="gas-modal-content__info-row-wrapper">
- <div className="gas-modal-content__info-row">
- <div className="gas-modal-content__info-row__send-info">
- <span className="gas-modal-content__info-row__send-info__label">{this.context.t('sendAmount')}</span>
- <span className="gas-modal-content__info-row__send-info__value">{sendAmount}</span>
- </div>
- <div className="gas-modal-content__info-row__transaction-info">
- <span className={'gas-modal-content__info-row__transaction-info__label'}>{this.context.t('transactionFee')}</span>
- <span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span>
- </div>
- <div className="gas-modal-content__info-row__total-info">
- <span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span>
- <span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span>
- </div>
- <div className="gas-modal-content__info-row__fiat-total-info">
- <span className="gas-modal-content__info-row__fiat-total-info__value">{newTotalFiat}</span>
- </div>
- </div>
- </div>
- )
- }
-
- renderTabs ({
- originalTotalFiat,
- originalTotalEth,
- newTotalFiat,
- newTotalEth,
- sendAmount,
- transactionFee,
- },
- {
- gasPriceButtonGroupProps,
- hideBasic,
- ...advancedTabProps
- }) {
- let tabsToRender = [
- { name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) },
- { name: 'advanced', content: this.renderAdvancedTabContent({ transactionFee, ...advancedTabProps }) },
- ]
-
- if (hideBasic) {
- tabsToRender = tabsToRender.slice(1)
- }
-
- return (
- <Tabs>
- {tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}>
- <div className="gas-modal-content">
- { content }
- { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
- </div>
- </Tab>
- )}
- </Tabs>
- )
- }
-
- render () {
- const {
- cancelAndClose,
- infoRowProps,
- onSubmit,
- customModalGasPriceInHex,
- customModalGasLimitInHex,
- disableSave,
- ...tabProps
- } = this.props
-
- return (
- <div className="gas-modal-page-container">
- <PageContainer
- title={this.context.t('customGas')}
- subtitle={this.context.t('customGasSubTitle')}
- tabsComponent={this.renderTabs(infoRowProps, tabProps)}
- disabled={disableSave}
- onCancel={() => cancelAndClose()}
- onClose={() => cancelAndClose()}
- onSubmit={() => {
- onSubmit(customModalGasLimitInHex, customModalGasPriceInHex)
- }}
- submitText={this.context.t('save')}
- headerCloseText={'Close'}
- hideCancel={true}
- />
- </div>
- )
- }
-}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
deleted file mode 100644
index 6692fb363..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
+++ /dev/null
@@ -1,291 +0,0 @@
-import { connect } from 'react-redux'
-import { pipe, partialRight } from 'ramda'
-import GasModalPageContainer from './gas-modal-page-container.component'
-import {
- hideModal,
- setGasLimit,
- setGasPrice,
- createSpeedUpTransaction,
- hideSidebar,
-} from '../../../actions'
-import {
- setCustomGasPrice,
- setCustomGasLimit,
- resetCustomData,
- setCustomTimeEstimate,
- fetchGasEstimates,
- fetchBasicGasAndTimeEstimates,
-} from '../../../ducks/gas.duck'
-import {
- hideGasButtonGroup,
-} from '../../../ducks/send.duck'
-import {
- updateGasAndCalculate,
-} from '../../../ducks/confirm-transaction.duck'
-import {
- getCurrentCurrency,
- conversionRateSelector as getConversionRate,
- getSelectedToken,
- getCurrentEthBalance,
-} from '../../../selectors.js'
-import {
- formatTimeEstimate,
- getFastPriceEstimateInHexWEI,
- getBasicGasEstimateLoadingStatus,
- getGasEstimatesLoadingStatus,
- getCustomGasLimit,
- getCustomGasPrice,
- getDefaultActiveButtonIndex,
- getEstimatedGasPrices,
- getEstimatedGasTimes,
- getRenderableBasicEstimateData,
- getBasicGasEstimateBlockTime,
- isCustomPriceSafe,
-} from '../../../selectors/custom-gas'
-import {
- submittedPendingTransactionsSelector,
-} from '../../../selectors/transactions'
-import {
- formatCurrency,
-} from '../../../helpers/confirm-transaction/util'
-import {
- addHexWEIsToDec,
- decEthToConvertedCurrency as ethTotalToConvertedCurrency,
- decGWEIToHexWEI,
- hexWEIToDecGWEI,
-} from '../../../helpers/conversions.util'
-import {
- formatETHFee,
-} from '../../../helpers/formatters'
-import {
- calcGasTotal,
- isBalanceSufficient,
-} from '../../send/send.utils'
-import { addHexPrefix } from 'ethereumjs-util'
-import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
-import {getIsMainnet, preferencesSelector} from '../../../selectors'
-
-const mapStateToProps = (state, ownProps) => {
- const { transaction = {} } = ownProps
- const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
- const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
-
- const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id)
- const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
- const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
- const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
-
- const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
-
- const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex)
-
- const currentCurrency = getCurrentCurrency(state)
- const conversionRate = getConversionRate(state)
-
- const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate)
-
- const hideBasic = state.appState.modal.modalState.props.hideBasic
-
- const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex)
-
- const gasPrices = getEstimatedGasPrices(state)
- const estimatedTimes = getEstimatedGasTimes(state)
- const balance = getCurrentEthBalance(state)
-
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
- const showFiat = Boolean(isMainnet || showFiatInTestnets)
-
- const insufficientBalance = !isBalanceSufficient({
- amount: value,
- gasTotal,
- balance,
- conversionRate,
- })
-
- return {
- hideBasic,
- isConfirm: isConfirm(state),
- customModalGasPriceInHex,
- customModalGasLimitInHex,
- customGasPrice,
- customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
- newTotalFiat,
- currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes),
- blockTime: getBasicGasEstimateBlockTime(state),
- customPriceIsSafe: isCustomPriceSafe(state),
- gasPriceButtonGroupProps: {
- buttonDataLoading,
- defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
- gasButtonInfo,
- },
- gasChartProps: {
- currentPrice: customGasPrice,
- gasPrices,
- estimatedTimes,
- gasPricesMax: gasPrices[gasPrices.length - 1],
- estimatedTimesMax: estimatedTimes[0],
- },
- infoRowProps: {
- originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate),
- originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal),
- newTotalFiat: showFiat ? newTotalFiat : '',
- newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
- transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
- sendAmount: addHexWEIsToRenderableEth(value, '0x0'),
- },
- isSpeedUp: transaction.status === 'submitted',
- txId: transaction.id,
- insufficientBalance,
- gasEstimatesLoading,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice)))
-
- return {
- cancelAndClose: () => {
- dispatch(resetCustomData())
- dispatch(hideModal())
- },
- hideModal: () => dispatch(hideModal()),
- updateCustomGasPrice,
- convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)),
- convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))),
- setGasData: (newLimit, newPrice) => {
- dispatch(setGasLimit(newLimit))
- dispatch(setGasPrice(newPrice))
- },
- updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => {
- updateCustomGasPrice(gasPrice)
- dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16))))
- return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
- },
- createSpeedUpTransaction: (txId, gasPrice) => {
- return dispatch(createSpeedUpTransaction(txId, gasPrice))
- },
- hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
- setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
- hideSidebar: () => dispatch(hideSidebar()),
- fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
- fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps
- const {
- updateCustomGasPrice: dispatchUpdateCustomGasPrice,
- hideGasButtonGroup: dispatchHideGasButtonGroup,
- setGasData: dispatchSetGasData,
- updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
- createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
- hideSidebar: dispatchHideSidebar,
- cancelAndClose: dispatchCancelAndClose,
- hideModal: dispatchHideModal,
- ...otherDispatchProps
- } = dispatchProps
-
- return {
- ...stateProps,
- ...otherDispatchProps,
- ...ownProps,
- onSubmit: (gasLimit, gasPrice) => {
- if (isConfirm) {
- dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice)
- dispatchHideModal()
- } else if (isSpeedUp) {
- dispatchCreateSpeedUpTransaction(txId, gasPrice)
- dispatchHideSidebar()
- dispatchCancelAndClose()
- } else {
- dispatchSetGasData(gasLimit, gasPrice)
- dispatchHideGasButtonGroup()
- dispatchCancelAndClose()
- }
- },
- gasPriceButtonGroupProps: {
- ...gasPriceButtonGroupProps,
- handleGasPriceSelection: dispatchUpdateCustomGasPrice,
- },
- cancelAndClose: () => {
- dispatchCancelAndClose()
- if (isSpeedUp) {
- dispatchHideSidebar()
- }
- },
- disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer)
-
-function isConfirm (state) {
- return Boolean(Object.keys(state.confirmTransaction.txData).length)
-}
-
-function calcCustomGasPrice (customGasPriceInHex) {
- return Number(hexWEIToDecGWEI(customGasPriceInHex))
-}
-
-function calcCustomGasLimit (customGasLimitInHex) {
- return parseInt(customGasLimitInHex, 16)
-}
-
-function getTxParams (state, transactionId) {
- const { confirmTransaction: { txData }, metamask: { send } } = state
- const pendingTransactions = submittedPendingTransactionsSelector(state)
- const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId)
- const { txParams: pendingTxParams } = pendingTransaction || {}
- return txData.txParams || pendingTxParams || {
- from: send.from,
- gas: send.gasLimit || '0x5208',
- gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true),
- to: send.to,
- value: getSelectedToken(state) ? '0x0' : send.amount,
- }
-}
-
-function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
- return pipe(
- addHexWEIsToDec,
- formatETHFee
- )(aHexWEI, bHexWEI)
-}
-
-function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) {
- return pipe(
- addHexWEIsToDec,
- partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]),
- partialRight(formatCurrency, [convertedCurrency]),
- )(aHexWEI, bHexWEI)
-}
-
-function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) {
- const minGasPrice = gasPrices[0]
- const maxGasPrice = gasPrices[gasPrices.length - 1]
- let priceForEstimation = currentGasPrice
- if (currentGasPrice < minGasPrice) {
- priceForEstimation = minGasPrice
- } else if (currentGasPrice > maxGasPrice) {
- priceForEstimation = maxGasPrice
- }
-
- const {
- closestLowerValueIndex,
- closestHigherValueIndex,
- closestHigherValue,
- closestLowerValue,
- } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation })
-
- const newTimeEstimate = extrapolateY({
- higherY: estimatedTimes[closestHigherValueIndex],
- lowerY: estimatedTimes[closestLowerValueIndex],
- higherX: closestHigherValue,
- lowerX: closestLowerValue,
- xForExtrapolation: priceForEstimation,
- })
-
- return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice)
-}
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
deleted file mode 100644
index 1761ad2b0..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import shallow from '../../../../../lib/shallow-with-context'
-import sinon from 'sinon'
-import GasModalPageContainer from '../gas-modal-page-container.component.js'
-import timeout from '../../../../../lib/test-timeout'
-
-import PageContainer from '../../../page-container'
-
-import { Tab } from '../../../tabs'
-
-const mockBasicGasEstimates = {
- blockTime: 'mockBlockTime',
-}
-
-const propsMethodSpies = {
- cancelAndClose: sinon.spy(),
- onSubmit: sinon.spy(),
- fetchBasicGasAndTimeEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
- fetchGasEstimates: sinon.spy(),
-}
-
-const mockGasPriceButtonGroupProps = {
- buttonDataLoading: false,
- className: 'gas-price-button-group',
- gasButtonInfo: [
- {
- feeInPrimaryCurrency: '$0.52',
- feeInSecondaryCurrency: '0.0048 ETH',
- timeEstimate: '~ 1 min 0 sec',
- priceInHexWei: '0xa1b2c3f',
- },
- {
- feeInPrimaryCurrency: '$0.39',
- feeInSecondaryCurrency: '0.004 ETH',
- timeEstimate: '~ 1 min 30 sec',
- priceInHexWei: '0xa1b2c39',
- },
- {
- feeInPrimaryCurrency: '$0.30',
- feeInSecondaryCurrency: '0.00354 ETH',
- timeEstimate: '~ 2 min 1 sec',
- priceInHexWei: '0xa1b2c30',
- },
- ],
- handleGasPriceSelection: 'mockSelectionFunction',
- noButtonActiveByDefault: true,
- showCheck: true,
- newTotalFiat: 'mockNewTotalFiat',
- newTotalEth: 'mockNewTotalEth',
-}
-const mockInfoRowProps = {
- originalTotalFiat: 'mockOriginalTotalFiat',
- originalTotalEth: 'mockOriginalTotalEth',
- newTotalFiat: 'mockNewTotalFiat',
- newTotalEth: 'mockNewTotalEth',
- sendAmount: 'mockSendAmount',
- transactionFee: 'mockTransactionFee',
-}
-
-const GP = GasModalPageContainer.prototype
-describe('GasModalPageContainer Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<GasModalPageContainer
- cancelAndClose={propsMethodSpies.cancelAndClose}
- onSubmit={propsMethodSpies.onSubmit}
- fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
- fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
- updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
- updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
- customGasPrice={21}
- customGasLimit={54321}
- gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
- infoRowProps={mockInfoRowProps}
- currentTimeEstimate={'1 min 31 sec'}
- customGasPriceInHex={'mockCustomGasPriceInHex'}
- customGasLimitInHex={'mockCustomGasLimitInHex'}
- insufficientBalance={false}
- disableSave={false}
- />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
- })
-
- afterEach(() => {
- propsMethodSpies.cancelAndClose.resetHistory()
- })
-
- describe('componentDidMount', () => {
- it('should call props.fetchBasicGasAndTimeEstimates', () => {
- propsMethodSpies.fetchBasicGasAndTimeEstimates.resetHistory()
- assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 0)
- wrapper.instance().componentDidMount()
- assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 1)
- })
-
- it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasAndTimeEstimates', async () => {
- propsMethodSpies.fetchGasEstimates.resetHistory()
- assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0)
- wrapper.instance().componentDidMount()
- await timeout(250)
- assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1)
- assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime')
- })
- })
-
- describe('render', () => {
- it('should render a PageContainer compenent', () => {
- assert.equal(wrapper.find(PageContainer).length, 1)
- })
-
- it('should pass correct props to PageContainer', () => {
- const {
- title,
- subtitle,
- disabled,
- } = wrapper.find(PageContainer).props()
- assert.equal(title, 'customGas')
- assert.equal(subtitle, 'customGasSubTitle')
- assert.equal(disabled, false)
- })
-
- it('should pass the correct onCancel and onClose methods to PageContainer', () => {
- const {
- onCancel,
- onClose,
- } = wrapper.find(PageContainer).props()
- assert.equal(propsMethodSpies.cancelAndClose.callCount, 0)
- onCancel()
- assert.equal(propsMethodSpies.cancelAndClose.callCount, 1)
- onClose()
- assert.equal(propsMethodSpies.cancelAndClose.callCount, 2)
- })
-
- it('should pass the correct renderTabs property to PageContainer', () => {
- sinon.stub(GP, 'renderTabs').returns('mockTabs')
- const renderTabsWrapperTester = shallow(<GasModalPageContainer
- fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
- fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
- />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
- const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props()
- assert.equal(tabsComponent, 'mockTabs')
- GasModalPageContainer.prototype.renderTabs.restore()
- })
- })
-
- describe('renderTabs', () => {
- beforeEach(() => {
- sinon.spy(GP, 'renderBasicTabContent')
- sinon.spy(GP, 'renderAdvancedTabContent')
- sinon.spy(GP, 'renderInfoRows')
- })
-
- afterEach(() => {
- GP.renderBasicTabContent.restore()
- GP.renderAdvancedTabContent.restore()
- GP.renderInfoRows.restore()
- })
-
- it('should render a Tabs component with "Basic" and "Advanced" tabs', () => {
- const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
- gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
- otherProps: 'mockAdvancedTabProps',
- })
- const renderedTabs = shallow(renderTabsResult)
- assert.equal(renderedTabs.props().className, 'tabs')
-
- const tabs = renderedTabs.find(Tab)
- assert.equal(tabs.length, 2)
-
- assert.equal(tabs.at(0).props().name, 'basic')
- assert.equal(tabs.at(1).props().name, 'advanced')
-
- assert.equal(tabs.at(0).childAt(0).props().className, 'gas-modal-content')
- assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content')
- })
-
- it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => {
- assert.equal(GP.renderBasicTabContent.callCount, 0)
- assert.equal(GP.renderAdvancedTabContent.callCount, 0)
-
- wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
-
- assert.equal(GP.renderBasicTabContent.callCount, 1)
- assert.equal(GP.renderAdvancedTabContent.callCount, 1)
-
- assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps)
- assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { transactionFee: 'mockTransactionFee', otherProps: 'mockAdvancedTabProps' })
- })
-
- it('should call renderInfoRows with the expected props', () => {
- assert.equal(GP.renderInfoRows.callCount, 0)
-
- wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
-
- assert.equal(GP.renderInfoRows.callCount, 2)
-
- assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
- assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
- })
-
- it('should not render the basic tab if hideBasic is true', () => {
- const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
- gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
- otherProps: 'mockAdvancedTabProps',
- hideBasic: true,
- })
-
- const renderedTabs = shallow(renderTabsResult)
- const tabs = renderedTabs.find(Tab)
- assert.equal(tabs.length, 1)
- assert.equal(tabs.at(0).props().name, 'advanced')
- })
- })
-
- describe('renderBasicTabContent', () => {
- it('should render', () => {
- const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps)
-
- assert.deepEqual(
- renderBasicTabContentResult.props.gasPriceButtonGroupProps,
- mockGasPriceButtonGroupProps
- )
- })
- })
-
- describe('renderAdvancedTabContent', () => {
- it('should render with the correct props', () => {
- const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({
- convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice',
- convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit',
- customGasPrice: 123,
- customGasLimit: 456,
- newTotalFiat: '$0.30',
- currentTimeEstimate: '1 min 31 sec',
- gasEstimatesLoading: 'mockGasEstimatesLoading',
- })
- const advancedTabContentProps = renderAdvancedTabContentResult.props
- assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice')
- assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit')
- assert.equal(advancedTabContentProps.customGasPrice, 123)
- assert.equal(advancedTabContentProps.customGasLimit, 456)
- assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec')
- assert.equal(advancedTabContentProps.totalFee, '$0.30')
- assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading')
- })
- })
-
- describe('renderInfoRows', () => {
- it('should render the info rows with the passed data', () => {
- const baseClassName = 'gas-modal-content__info-row'
- const renderedInfoRowsContainer = shallow(wrapper.instance().renderInfoRows(
- 'mockNewTotalFiat',
- ' mockNewTotalEth',
- ' mockSendAmount',
- ' mockTransactionFee'
- ))
-
- assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName))
-
- const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children()
- assert.equal(renderedInfoRows.length, 4)
- assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`))
- assert(renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`))
- assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`))
- assert(renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`))
-
- assert.equal(renderedInfoRows.at(0).text(), 'sendAmount mockSendAmount')
- assert.equal(renderedInfoRows.at(1).text(), 'transactionFee mockTransactionFee')
- assert.equal(renderedInfoRows.at(2).text(), 'newTotal mockNewTotalEth')
- assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat')
- })
- })
-})
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
deleted file mode 100644
index fb6a01fff..000000000
--- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
+++ /dev/null
@@ -1,425 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-let mergeProps
-
-const actionSpies = {
- hideModal: sinon.spy(),
- setGasLimit: sinon.spy(),
- setGasPrice: sinon.spy(),
-}
-
-const gasActionSpies = {
- setCustomGasPrice: sinon.spy(),
- setCustomGasLimit: sinon.spy(),
- resetCustomData: sinon.spy(),
-}
-
-const confirmTransactionActionSpies = {
- updateGasAndCalculate: sinon.spy(),
-}
-
-const sendActionSpies = {
- hideGasButtonGroup: sinon.spy(),
-}
-
-proxyquire('../gas-modal-page-container.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- mergeProps = mp
- return () => ({})
- },
- },
- '../../../selectors/custom-gas': {
- getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`,
- getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`,
- getDefaultActiveButtonIndex: (a, b) => a + b,
- },
- '../../../actions': actionSpies,
- '../../../ducks/gas.duck': gasActionSpies,
- '../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies,
- '../../../ducks/send.duck': sendActionSpies,
- '../../../selectors.js': {
- getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
- },
-})
-
-describe('gas-modal-page-container container', () => {
-
- describe('mapStateToProps()', () => {
- it('should map the correct properties to props', () => {
- const baseMockState = {
- appState: {
- modal: {
- modalState: {
- props: {
- hideBasic: true,
- },
- },
- },
- },
- metamask: {
- send: {
- gasLimit: '16',
- gasPrice: '32',
- amount: '64',
- },
- currentCurrency: 'abc',
- conversionRate: 50,
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 12,
- safeLow: 2,
- },
- customData: {
- limit: 'aaaaaaaa',
- price: 'ffffffff',
- },
- gasEstimatesLoading: false,
- priceAndTimeEstimates: [
- { gasprice: 3, expectedTime: 31 },
- { gasprice: 4, expectedTime: 62 },
- { gasprice: 5, expectedTime: 93 },
- { gasprice: 6, expectedTime: 124 },
- ],
- },
- confirmTransaction: {
- txData: {
- txParams: {
- gas: '0x1600000',
- gasPrice: '0x3200000',
- value: '0x640000000000000',
- },
- },
- },
- }
- const baseExpectedResult = {
- isConfirm: true,
- customGasPrice: 4.294967295,
- customGasLimit: 2863311530,
- currentTimeEstimate: '~1 min 11 sec',
- newTotalFiat: '637.41',
- blockTime: 12,
- customModalGasLimitInHex: 'aaaaaaaa',
- customModalGasPriceInHex: 'ffffffff',
- customPriceIsSafe: true,
- gasChartProps: {
- 'currentPrice': 4.294967295,
- estimatedTimes: [31, 62, 93, 124],
- estimatedTimesMax: '31',
- gasPrices: [3, 4, 5, 6],
- gasPricesMax: 6,
- },
- gasPriceButtonGroupProps: {
- buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4',
- defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff',
- gasButtonInfo: 'mockRenderableBasicEstimateData:4',
- },
- gasEstimatesLoading: false,
- hideBasic: true,
- infoRowProps: {
- originalTotalFiat: '637.41',
- originalTotalEth: '12.748189 ETH',
- newTotalFiat: '637.41',
- newTotalEth: '12.748189 ETH',
- sendAmount: '0.45036 ETH',
- transactionFee: '12.297829 ETH',
- },
- insufficientBalance: true,
- isSpeedUp: false,
- txId: 34,
- }
- const baseMockOwnProps = { transaction: { id: 34 } }
- const tests = [
- { mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps },
- {
- mockState: Object.assign({}, baseMockState, {
- metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' },
- }),
- expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }),
- mockOwnProps: baseMockOwnProps,
- },
- {
- mockState: baseMockState,
- mockOwnProps: Object.assign({}, baseMockOwnProps, {
- transaction: { id: 34, status: 'submitted' },
- }),
- expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }),
- },
- {
- mockState: Object.assign({}, baseMockState, {
- metamask: {
- ...baseMockState.metamask,
- preferences: {
- ...baseMockState.metamask.preferences,
- showFiatInTestnets: false,
- },
- provider: {
- ...baseMockState.metamask.provider,
- type: 'rinkeby',
- },
- },
- }),
- mockOwnProps: baseMockOwnProps,
- expectedResult: {
- ...baseExpectedResult,
- infoRowProps: {
- ...baseExpectedResult.infoRowProps,
- newTotalFiat: '',
- },
- },
- },
- {
- mockState: Object.assign({}, baseMockState, {
- metamask: {
- ...baseMockState.metamask,
- preferences: {
- ...baseMockState.metamask.preferences,
- showFiatInTestnets: true,
- },
- provider: {
- ...baseMockState.metamask.provider,
- type: 'rinkeby',
- },
- },
- }),
- mockOwnProps: baseMockOwnProps,
- expectedResult: baseExpectedResult,
- },
- {
- mockState: Object.assign({}, baseMockState, {
- metamask: {
- ...baseMockState.metamask,
- preferences: {
- ...baseMockState.metamask.preferences,
- showFiatInTestnets: true,
- },
- provider: {
- ...baseMockState.metamask.provider,
- type: 'mainnet',
- },
- },
- }),
- mockOwnProps: baseMockOwnProps,
- expectedResult: baseExpectedResult,
- },
- ]
-
- let result
- tests.forEach(({ mockState, mockOwnProps, expectedResult}) => {
- result = mapStateToProps(mockState, mockOwnProps)
- assert.deepEqual(result, expectedResult)
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- afterEach(() => {
- actionSpies.hideModal.resetHistory()
- gasActionSpies.setCustomGasPrice.resetHistory()
- gasActionSpies.setCustomGasLimit.resetHistory()
- })
-
- describe('hideGasButtonGroup()', () => {
- it('should dispatch a hideGasButtonGroup action', () => {
- mapDispatchToPropsObject.hideGasButtonGroup()
- assert(dispatchSpy.calledOnce)
- assert(sendActionSpies.hideGasButtonGroup.calledOnce)
- })
- })
-
- describe('cancelAndClose()', () => {
- it('should dispatch a hideModal action', () => {
- mapDispatchToPropsObject.cancelAndClose()
- assert(dispatchSpy.calledTwice)
- assert(actionSpies.hideModal.calledOnce)
- assert(gasActionSpies.resetCustomData.calledOnce)
- })
- })
-
- describe('updateCustomGasPrice()', () => {
- it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => {
- mapDispatchToPropsObject.updateCustomGasPrice('ffff')
- assert(dispatchSpy.calledOnce)
- assert(gasActionSpies.setCustomGasPrice.calledOnce)
- assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff')
- })
- })
-
- describe('convertThenUpdateCustomGasPrice()', () => {
- it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => {
- mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff')
- assert(dispatchSpy.calledOnce)
- assert(gasActionSpies.setCustomGasPrice.calledOnce)
- assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600')
- })
- })
-
-
- describe('convertThenUpdateCustomGasLimit()', () => {
- it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => {
- mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16)
- assert(dispatchSpy.calledOnce)
- assert(gasActionSpies.setCustomGasLimit.calledOnce)
- assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10')
- })
- })
-
- describe('setGasData()', () => {
- it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => {
- mapDispatchToPropsObject.setGasData('ffff', 'aaaa')
- assert(dispatchSpy.calledTwice)
- assert(actionSpies.setGasPrice.calledOnce)
- assert(actionSpies.setGasLimit.calledOnce)
- assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff')
- assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa')
- })
- })
-
- describe('updateConfirmTxGasAndCalculate()', () => {
- it('should dispatch a updateGasAndCalculate action with the correct props', () => {
- mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa')
- assert.equal(dispatchSpy.callCount, 3)
- assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce)
- assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' })
- })
- })
-
- })
-
- describe('mergeProps', () => {
- let stateProps
- let dispatchProps
- let ownProps
-
- beforeEach(() => {
- stateProps = {
- gasPriceButtonGroupProps: {
- someGasPriceButtonGroupProp: 'foo',
- anotherGasPriceButtonGroupProp: 'bar',
- },
- isConfirm: true,
- someOtherStateProp: 'baz',
- }
- dispatchProps = {
- updateCustomGasPrice: sinon.spy(),
- hideGasButtonGroup: sinon.spy(),
- setGasData: sinon.spy(),
- updateConfirmTxGasAndCalculate: sinon.spy(),
- someOtherDispatchProp: sinon.spy(),
- createSpeedUpTransaction: sinon.spy(),
- hideSidebar: sinon.spy(),
- hideModal: sinon.spy(),
- cancelAndClose: sinon.spy(),
- }
- ownProps = { someOwnProp: 123 }
- })
-
- afterEach(() => {
- dispatchProps.updateCustomGasPrice.resetHistory()
- dispatchProps.hideGasButtonGroup.resetHistory()
- dispatchProps.setGasData.resetHistory()
- dispatchProps.updateConfirmTxGasAndCalculate.resetHistory()
- dispatchProps.someOtherDispatchProp.resetHistory()
- dispatchProps.createSpeedUpTransaction.resetHistory()
- dispatchProps.hideSidebar.resetHistory()
- dispatchProps.hideModal.resetHistory()
- })
- it('should return the expected props when isConfirm is true', () => {
- const result = mergeProps(stateProps, dispatchProps, ownProps)
-
- assert.equal(result.isConfirm, true)
- assert.equal(result.someOtherStateProp, 'baz')
- assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
- assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
- assert.equal(result.someOwnProp, 123)
-
- assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
- assert.equal(dispatchProps.setGasData.callCount, 0)
- assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
- assert.equal(dispatchProps.hideModal.callCount, 0)
-
- result.onSubmit()
-
- assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1)
- assert.equal(dispatchProps.setGasData.callCount, 0)
- assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
- assert.equal(dispatchProps.hideModal.callCount, 1)
-
- assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
- result.gasPriceButtonGroupProps.handleGasPriceSelection()
- assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
-
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
- result.someOtherDispatchProp()
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
- })
-
- it('should return the expected props when isConfirm is false', () => {
- const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps)
-
- assert.equal(result.isConfirm, false)
- assert.equal(result.someOtherStateProp, 'baz')
- assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
- assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
- assert.equal(result.someOwnProp, 123)
-
- assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
- assert.equal(dispatchProps.setGasData.callCount, 0)
- assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
- assert.equal(dispatchProps.cancelAndClose.callCount, 0)
-
- result.onSubmit('mockNewLimit', 'mockNewPrice')
-
- assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
- assert.equal(dispatchProps.setGasData.callCount, 1)
- assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice'])
- assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1)
- assert.equal(dispatchProps.cancelAndClose.callCount, 1)
-
- assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
- result.gasPriceButtonGroupProps.handleGasPriceSelection()
- assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
-
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
- result.someOtherDispatchProp()
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
- })
-
- it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => {
- const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps)
-
- result.onSubmit()
-
- assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
- assert.equal(dispatchProps.setGasData.callCount, 0)
- assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
- assert.equal(dispatchProps.cancelAndClose.callCount, 1)
-
- assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1)
- assert.equal(dispatchProps.hideSidebar.callCount, 1)
- })
- })
-
-})
diff --git a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js
deleted file mode 100644
index 8ad063b21..000000000
--- a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ButtonGroup from '../../button-group'
-import Button from '../../button'
-
-const GAS_OBJECT_PROPTYPES_SHAPE = {
- label: PropTypes.string,
- feeInPrimaryCurrency: PropTypes.string,
- feeInSecondaryCurrency: PropTypes.string,
- timeEstimate: PropTypes.string,
- priceInHexWei: PropTypes.string,
-}
-
-export default class GasPriceButtonGroup extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- buttonDataLoading: PropTypes.bool,
- className: PropTypes.string,
- defaultActiveButtonIndex: PropTypes.number,
- gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)),
- handleGasPriceSelection: PropTypes.func,
- newActiveButtonIndex: PropTypes.number,
- noButtonActiveByDefault: PropTypes.bool,
- showCheck: PropTypes.bool,
- }
-
- renderButtonContent ({
- labelKey,
- feeInPrimaryCurrency,
- feeInSecondaryCurrency,
- timeEstimate,
- }, {
- className,
- showCheck,
- }) {
- return (<div>
- { labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> }
- { timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> }
- { feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> }
- { feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> }
- { showCheck && <div className="button-check-wrapper"><i className="fa fa-check fa-sm" /></div> }
- </div>)
- }
-
- renderButton ({
- priceInHexWei,
- ...renderableGasInfo
- }, {
- buttonDataLoading,
- handleGasPriceSelection,
- ...buttonContentPropsAndFlags
- }, index) {
- return (
- <Button
- onClick={() => handleGasPriceSelection(priceInHexWei)}
- key={`gas-price-button-${index}`}
- >
- {this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)}
- </Button>
- )
- }
-
- render () {
- const {
- gasButtonInfo,
- defaultActiveButtonIndex = 1,
- newActiveButtonIndex,
- noButtonActiveByDefault = false,
- buttonDataLoading,
- ...buttonPropsAndFlags
- } = this.props
-
- return (
- !buttonDataLoading
- ? <ButtonGroup
- className={buttonPropsAndFlags.className}
- defaultActiveButtonIndex={defaultActiveButtonIndex}
- newActiveButtonIndex={newActiveButtonIndex}
- noButtonActiveByDefault={noButtonActiveByDefault}
- >
- { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) }
- </ButtonGroup>
- : <div className={`${buttonPropsAndFlags.className}__loading-container`}>{ this.context.t('loading') }</div>
- )
- }
-}
diff --git a/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js
deleted file mode 100644
index 79f74f8e4..000000000
--- a/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import shallow from '../../../../../lib/shallow-with-context'
-import sinon from 'sinon'
-import GasPriceButtonGroup from '../gas-price-button-group.component'
-
-import ButtonGroup from '../../../button-group/'
-
-const mockGasPriceButtonGroupProps = {
- buttonDataLoading: false,
- className: 'gas-price-button-group',
- gasButtonInfo: [
- {
- feeInPrimaryCurrency: '$0.52',
- feeInSecondaryCurrency: '0.0048 ETH',
- timeEstimate: '~ 1 min 0 sec',
- priceInHexWei: '0xa1b2c3f',
- },
- {
- feeInPrimaryCurrency: '$0.39',
- feeInSecondaryCurrency: '0.004 ETH',
- timeEstimate: '~ 1 min 30 sec',
- priceInHexWei: '0xa1b2c39',
- },
- {
- feeInPrimaryCurrency: '$0.30',
- feeInSecondaryCurrency: '0.00354 ETH',
- timeEstimate: '~ 2 min 1 sec',
- priceInHexWei: '0xa1b2c30',
- },
- ],
- handleGasPriceSelection: sinon.spy(),
- noButtonActiveByDefault: true,
- defaultActiveButtonIndex: 2,
- showCheck: true,
-}
-
-const mockButtonPropsAndFlags = Object.assign({}, {
- className: mockGasPriceButtonGroupProps.className,
- handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection,
- showCheck: mockGasPriceButtonGroupProps.showCheck,
-})
-
-sinon.spy(GasPriceButtonGroup.prototype, 'renderButton')
-sinon.spy(GasPriceButtonGroup.prototype, 'renderButtonContent')
-
-describe('GasPriceButtonGroup Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<GasPriceButtonGroup
- {...mockGasPriceButtonGroupProps}
- />)
- })
-
- afterEach(() => {
- GasPriceButtonGroup.prototype.renderButton.resetHistory()
- GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
- mockGasPriceButtonGroupProps.handleGasPriceSelection.resetHistory()
- })
-
- describe('render', () => {
- it('should render a ButtonGroup', () => {
- assert(wrapper.is(ButtonGroup))
- })
-
- it('should render the correct props on the ButtonGroup', () => {
- const {
- className,
- defaultActiveButtonIndex,
- noButtonActiveByDefault,
- } = wrapper.props()
- assert.equal(className, 'gas-price-button-group')
- assert.equal(defaultActiveButtonIndex, 2)
- assert.equal(noButtonActiveByDefault, true)
- })
-
- function renderButtonArgsTest (i, mockButtonPropsAndFlags) {
- assert.deepEqual(
- GasPriceButtonGroup.prototype.renderButton.getCall(i).args,
- [
- Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[i]),
- mockButtonPropsAndFlags,
- i,
- ]
- )
- }
-
- it('should call this.renderButton 3 times, with the correct args', () => {
- assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3)
- renderButtonArgsTest(0, mockButtonPropsAndFlags)
- renderButtonArgsTest(1, mockButtonPropsAndFlags)
- renderButtonArgsTest(2, mockButtonPropsAndFlags)
- })
-
- it('should show loading if buttonDataLoading', () => {
- wrapper.setProps({ buttonDataLoading: true })
- assert(wrapper.is('div'))
- assert(wrapper.hasClass('gas-price-button-group__loading-container'))
- assert.equal(wrapper.text(), 'loading')
- })
- })
-
- describe('renderButton', () => {
- let wrappedRenderButtonResult
-
- beforeEach(() => {
- GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
- const renderButtonResult = GasPriceButtonGroup.prototype.renderButton(
- Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]),
- mockButtonPropsAndFlags
- )
- wrappedRenderButtonResult = shallow(renderButtonResult)
- })
-
- it('should render a button', () => {
- assert.equal(wrappedRenderButtonResult.type(), 'button')
- })
-
- it('should call the correct method when clicked', () => {
- assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 0)
- wrappedRenderButtonResult.props().onClick()
- assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 1)
- assert.deepEqual(
- mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args,
- [mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei]
- )
- })
-
- it('should call this.renderButtonContent with the correct args', () => {
- assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 1)
- const {
- feeInPrimaryCurrency,
- feeInSecondaryCurrency,
- timeEstimate,
- } = mockGasPriceButtonGroupProps.gasButtonInfo[0]
- const {
- showCheck,
- className,
- } = mockGasPriceButtonGroupProps
- assert.deepEqual(
- GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args,
- [
- {
- feeInPrimaryCurrency,
- feeInSecondaryCurrency,
- timeEstimate,
- },
- {
- showCheck,
- className,
- },
- ]
- )
- })
- })
-
- describe('renderButtonContent', () => {
- it('should render a label if passed a labelKey', () => {
- const renderButtonContentResult = wrapper.instance().renderButtonContent({
- labelKey: 'mockLabelKey',
- }, {
- className: 'someClass',
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
- assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey')
- })
-
- it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => {
- const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
- feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
- }, {
- className: 'someClass',
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
- assert.equal(wrappedRenderButtonContentResult.find('.someClass__primary-currency').text(), 'mockFeeInPrimaryCurrency')
- })
-
- it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => {
- const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
- feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
- }, {
- className: 'someClass',
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
- assert.equal(wrappedRenderButtonContentResult.find('.someClass__secondary-currency').text(), 'mockFeeInSecondaryCurrency')
- })
-
- it('should render a timeEstimate if passed a timeEstimate', () => {
- const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
- timeEstimate: 'mockTimeEstimate',
- }, {
- className: 'someClass',
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
- assert.equal(wrappedRenderButtonContentResult.find('.someClass__time-estimate').text(), 'mockTimeEstimate')
- })
-
- it('should render a check if showCheck is true', () => {
- const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {
- className: 'someClass',
- showCheck: true,
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.find('.fa-check').length, 1)
- })
-
- it('should render all elements if all args passed', () => {
- const renderButtonContentResult = wrapper.instance().renderButtonContent({
- labelKey: 'mockLabel',
- feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
- feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
- timeEstimate: 'mockTimeEstimate',
- }, {
- className: 'someClass',
- showCheck: true,
- })
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.children().length, 5)
- })
-
-
- it('should render no elements if all args passed', () => {
- const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {})
- const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
- assert.equal(wrappedRenderButtonContentResult.children().length, 0)
- })
- })
-})
diff --git a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
deleted file mode 100644
index 74eddae42..000000000
--- a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
+++ /dev/null
@@ -1,218 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-import shallow from '../../../../../lib/shallow-with-context'
-import * as d3 from 'd3'
-
-function timeout (time) {
- return new Promise((resolve, reject) => {
- setTimeout(resolve, time)
- })
-}
-
-const propsMethodSpies = {
- updateCustomGasPrice: sinon.spy(),
-}
-
-const selectReturnSpies = {
- empty: sinon.spy(),
- remove: sinon.spy(),
- style: sinon.spy(),
- select: d3.select,
- attr: sinon.spy(),
- on: sinon.spy(),
- datum: sinon.stub().returns({ x: 'mockX' }),
-}
-
-const mockSelectReturn = {
- ...d3.select('div'),
- node: () => ({
- getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }),
- }),
- ...selectReturnSpies,
-}
-
-const gasPriceChartUtilsSpies = {
- appendOrUpdateCircle: sinon.spy(),
- generateChart: sinon.stub().returns({ mockChart: true }),
- generateDataUIObj: sinon.spy(),
- getAdjacentGasPrices: sinon.spy(),
- getCoordinateData: sinon.stub().returns({ x: 'mockCoordinateX', width: 'mockWidth' }),
- getNewXandTimeEstimate: sinon.spy(),
- handleChartUpdate: sinon.spy(),
- hideDataUI: sinon.spy(),
- setSelectedCircle: sinon.spy(),
- setTickPosition: sinon.spy(),
- handleMouseMove: sinon.spy(),
-}
-
-const testProps = {
- gasPrices: [1.5, 2.5, 4, 8],
- estimatedTimes: [100, 80, 40, 10],
- gasPricesMax: 9,
- estimatedTimesMax: '100',
- currentPrice: 6,
- updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
-}
-
-const GasPriceChart = proxyquire('../gas-price-chart.component.js', {
- './gas-price-chart.utils.js': gasPriceChartUtilsSpies,
- 'd3': {
- ...d3,
- select: function (...args) {
- const result = d3.select(...args)
- return result.empty()
- ? mockSelectReturn
- : result
- },
- event: {
- clientX: 'mockClientX',
- },
- },
-}).default
-
-sinon.spy(GasPriceChart.prototype, 'renderChart')
-
-describe('GasPriceChart Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<GasPriceChart {...testProps} />)
- })
-
- describe('render()', () => {
- it('should render', () => {
- assert(wrapper.hasClass('gas-price-chart'))
- })
-
- it('should render the chart div', () => {
- assert(wrapper.childAt(0).hasClass('gas-price-chart__root'))
- assert.equal(wrapper.childAt(0).props().id, 'chart')
- })
- })
-
- describe('componentDidMount', () => {
- it('should call this.renderChart with the components props', () => {
- assert(GasPriceChart.prototype.renderChart.callCount, 1)
- wrapper.instance().componentDidMount()
- assert(GasPriceChart.prototype.renderChart.callCount, 2)
- assert.deepEqual(GasPriceChart.prototype.renderChart.getCall(1).args, [{...testProps}])
- })
- })
-
- describe('componentDidUpdate', () => {
- it('should call handleChartUpdate if props.currentPrice has changed', () => {
- gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
- wrapper.instance().componentDidUpdate({ currentPrice: 7 })
- assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 1)
- })
-
- it('should call handleChartUpdate with the correct props', () => {
- gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
- wrapper.instance().componentDidUpdate({ currentPrice: 7 })
- assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
- chart: { mockChart: true },
- gasPrices: [1.5, 2.5, 4, 8],
- newPrice: 6,
- cssId: '#set-circle',
- }])
- })
-
- it('should not call handleChartUpdate if props.currentPrice has not changed', () => {
- gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
- wrapper.instance().componentDidUpdate({ currentPrice: 6 })
- assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 0)
- })
- })
-
- describe('renderChart', () => {
- it('should call setTickPosition 4 times, with the expected props', async () => {
- await timeout(0)
- gasPriceChartUtilsSpies.setTickPosition.resetHistory()
- assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 0)
- wrapper.instance().renderChart(testProps)
- await timeout(0)
- assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 4)
- assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(0).args, ['y', 0, -5, 8])
- assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(1).args, ['y', 1, -3, -5])
- assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3])
- assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8])
- })
-
- it('should call handleChartUpdate with the correct props', async () => {
- await timeout(0)
- gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
- wrapper.instance().renderChart(testProps)
- await timeout(0)
- assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
- chart: { mockChart: true },
- gasPrices: [1.5, 2.5, 4, 8],
- newPrice: 6,
- cssId: '#set-circle',
- }])
- })
-
- it('should add three events to the chart', async () => {
- await timeout(0)
- selectReturnSpies.on.resetHistory()
- assert.equal(selectReturnSpies.on.callCount, 0)
- wrapper.instance().renderChart(testProps)
- await timeout(0)
- assert.equal(selectReturnSpies.on.callCount, 3)
-
- const firstOnEventArgs = selectReturnSpies.on.getCall(0).args
- assert.equal(firstOnEventArgs[0], 'mouseout')
- const secondOnEventArgs = selectReturnSpies.on.getCall(1).args
- assert.equal(secondOnEventArgs[0], 'click')
- const thirdOnEventArgs = selectReturnSpies.on.getCall(2).args
- assert.equal(thirdOnEventArgs[0], 'mousemove')
- })
-
- it('should hide the data UI on mouseout', async () => {
- await timeout(0)
- selectReturnSpies.on.resetHistory()
- wrapper.instance().renderChart(testProps)
- gasPriceChartUtilsSpies.hideDataUI.resetHistory()
- await timeout(0)
- const mouseoutEventArgs = selectReturnSpies.on.getCall(0).args
- assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 0)
- mouseoutEventArgs[1]()
- assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 1)
- assert.deepEqual(gasPriceChartUtilsSpies.hideDataUI.getCall(0).args, [{ mockChart: true }, '#overlayed-circle'])
- })
-
- it('should updateCustomGasPrice on click', async () => {
- await timeout(0)
- selectReturnSpies.on.resetHistory()
- wrapper.instance().renderChart(testProps)
- propsMethodSpies.updateCustomGasPrice.resetHistory()
- await timeout(0)
- const mouseoutEventArgs = selectReturnSpies.on.getCall(1).args
- assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 0)
- mouseoutEventArgs[1]()
- assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 1)
- assert.equal(propsMethodSpies.updateCustomGasPrice.getCall(0).args[0], 'mockX')
- })
-
- it('should handle mousemove', async () => {
- await timeout(0)
- selectReturnSpies.on.resetHistory()
- wrapper.instance().renderChart(testProps)
- gasPriceChartUtilsSpies.handleMouseMove.resetHistory()
- await timeout(0)
- const mouseoutEventArgs = selectReturnSpies.on.getCall(2).args
- assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 0)
- mouseoutEventArgs[1]()
- assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 1)
- assert.deepEqual(gasPriceChartUtilsSpies.handleMouseMove.getCall(0).args, [{
- xMousePos: 'mockClientX',
- chartXStart: 'mockCoordinateX',
- chartWidth: 'mockWidth',
- gasPrices: testProps.gasPrices,
- estimatedTimes: testProps.estimatedTimes,
- chart: { mockChart: true },
- }])
- })
- })
-})
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
deleted file mode 100644
index 6847a6919..000000000
--- a/ui/app/components/hex-to-decimal/hex-to-decimal.component.js
+++ /dev/null
@@ -1,21 +0,0 @@
-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/identicon/identicon.component.js b/ui/app/components/identicon/identicon.component.js
deleted file mode 100644
index b892e5ae5..000000000
--- a/ui/app/components/identicon/identicon.component.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { toDataUrl } from '../../../lib/blockies'
-import contractMap from 'eth-contract-metadata'
-import { checksumAddress } from '../../../app/util'
-import Jazzicon from '../jazzicon'
-
-const getStyles = diameter => (
- {
- height: diameter,
- width: diameter,
- borderRadius: diameter / 2,
- }
-)
-
-export default class Identicon extends PureComponent {
- static propTypes = {
- address: PropTypes.string,
- className: PropTypes.string,
- diameter: PropTypes.number,
- image: PropTypes.string,
- useBlockie: PropTypes.bool,
- }
-
- static defaultProps = {
- diameter: 46,
- }
-
- renderImage () {
- const { className, diameter, image } = this.props
-
- return (
- <img
- className={classnames('identicon', className)}
- src={image}
- style={getStyles(diameter)}
- />
- )
- }
-
- renderJazzicon () {
- const { address, className, diameter } = this.props
-
- return (
- <Jazzicon
- address={address}
- diameter={diameter}
- className={classnames('identicon', className)}
- style={getStyles(diameter)}
- />
- )
- }
-
- renderBlockie () {
- const { address, className, diameter } = this.props
-
- return (
- <div
- className={classnames('identicon', className)}
- style={getStyles(diameter)}
- >
- <img
- src={toDataUrl(address)}
- height={diameter}
- width={diameter}
- />
- </div>
- )
- }
-
- render () {
- const { className, address, image, diameter, useBlockie } = this.props
-
- if (image) {
- return this.renderImage()
- }
-
- if (address) {
- const checksummedAddress = checksumAddress(address)
-
- if (contractMap[checksummedAddress] && contractMap[checksummedAddress].logo) {
- return this.renderJazzicon()
- }
-
- return useBlockie
- ? this.renderBlockie()
- : this.renderJazzicon()
- }
-
- return (
- <img
- className={classnames('balance-icon', className)}
- src="./images/eth_logo.svg"
- style={getStyles(diameter)}
- />
- )
- }
-}
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
deleted file mode 100644
index 96cc74c79..000000000
--- a/ui/app/components/index.scss
+++ /dev/null
@@ -1,81 +0,0 @@
-@import './account-menu/index';
-
-@import './add-token-button/index';
-
-@import './app-header/index';
-
-@import './breadcrumbs/index';
-
-@import './button-group/index';
-
-@import './card/index';
-
-@import './confirm-page-container/index';
-
-@import './currency-input/index';
-
-@import './currency-display/index';
-
-@import './error-message/index';
-
-@import './export-text-container/index';
-
-@import './identicon/index';
-
-@import './info-box/index';
-
-@import './menu-bar/index';
-
-@import './modal/index';
-
-@import './modals/index';
-
-@import './network-display/index';
-
-@import './page-container/index';
-
-@import './pages/index';
-
-@import './provider-page-container/index';
-
-@import './selected-account/index';
-
-@import './sender-to-recipient/index';
-
-@import './tabs/index';
-
-@import './token-balance/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';
-
-@import './unit-input/index';
-
-@import './gas-customization/gas-modal-page-container/index';
-
-@import './gas-customization/gas-modal-page-container/index';
-
-@import './gas-customization/gas-modal-page-container/index';
-
-@import './gas-customization/index';
-
-@import './gas-customization/gas-price-button-group/index';
-
-@import './ui-migration-annoucement/index';
diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js
deleted file mode 100644
index eec5e3740..000000000
--- a/ui/app/components/input-number.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const {
- addCurrencies,
- conversionGTE,
- conversionLTE,
- subtractCurrencies,
-} = require('../conversion-util')
-
-module.exports = InputNumber
-
-inherits(InputNumber, Component)
-function InputNumber () {
- Component.call(this)
-
- this.setValue = this.setValue.bind(this)
-}
-
-function isValidInput (text) {
- const re = /^([1-9]\d*|0)(\.|\.\d*)?$/
- return re.test(text)
-}
-
-function removeLeadingZeroes (str) {
- return str.replace(/^0*(?=\d)/, '')
-}
-
-InputNumber.prototype.setValue = function (newValue) {
- newValue = removeLeadingZeroes(newValue)
- if (newValue && !isValidInput(newValue)) return
- const { fixed, min = -1, max = Infinity, onChange } = this.props
-
- newValue = fixed ? newValue.toFixed(4) : newValue
- const newValueGreaterThanMin = conversionGTE(
- { value: newValue || '0', fromNumericBase: 'dec' },
- { value: min, fromNumericBase: 'hex' },
- )
-
- const newValueLessThanMax = conversionLTE(
- { value: newValue || '0', fromNumericBase: 'dec' },
- { value: max, fromNumericBase: 'hex' },
- )
- if (newValueGreaterThanMin && newValueLessThanMax) {
- onChange(newValue)
- } else if (!newValueGreaterThanMin) {
- onChange(min)
- } else if (!newValueLessThanMax) {
- onChange(max)
- }
-}
-
-InputNumber.prototype.render = function () {
- const { unitLabel, step = 1, placeholder, value } = this.props
-
- return h('div.customize-gas-input-wrapper', {}, [
- h('input', {
- className: 'customize-gas-input',
- value,
- placeholder,
- type: 'number',
- onChange: e => {
- this.setValue(e.target.value)
- },
- min: 0,
- }),
- h('span.gas-tooltip-input-detail', {}, [unitLabel]),
- h('div.gas-tooltip-input-arrows', {}, [
- h('div.gas-tooltip-input-arrow-wrapper', {
- onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
- }, [
- 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/jazzicon/jazzicon.component.js b/ui/app/components/jazzicon/jazzicon.component.js
deleted file mode 100644
index fcb1c59b1..000000000
--- a/ui/app/components/jazzicon/jazzicon.component.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import isNode from 'detect-node'
-import { findDOMNode } from 'react-dom'
-import jazzicon from 'jazzicon'
-import iconFactoryGenerator from '../../../lib/icon-factory'
-const iconFactory = iconFactoryGenerator(jazzicon)
-
-/**
- * Wrapper around the jazzicon library to return a React component, as the library returns an
- * HTMLDivElement which needs to be appended.
- */
-export default class Jazzicon extends PureComponent {
- static propTypes = {
- address: PropTypes.string.isRequired,
- className: PropTypes.string,
- diameter: PropTypes.number,
- style: PropTypes.object,
- }
-
- static defaultProps = {
- diameter: 46,
- }
-
- componentDidMount () {
- if (!isNode) {
- this.appendJazzicon()
- }
- }
-
- componentDidUpdate (prevProps) {
- const { address: prevAddress } = prevProps
- const { address } = this.props
-
- if (!isNode && address !== prevAddress) {
- this.removeExistingChildren()
- this.appendJazzicon()
- }
- }
-
- removeExistingChildren () {
- // eslint-disable-next-line react/no-find-dom-node
- const container = findDOMNode(this)
- const { children } = container
-
- for (let i = 0; i < children.length; i++) {
- container.removeChild(children[i])
- }
- }
-
- appendJazzicon () {
- // eslint-disable-next-line react/no-find-dom-node
- const container = findDOMNode(this)
- const { address, diameter } = this.props
- const image = iconFactory.iconForAddress(address, diameter)
- container.appendChild(image)
- }
-
- render () {
- const { className, style } = this.props
-
- return (
- <div
- className={className}
- style={style}
- />
- )
- }
-}
diff --git a/ui/app/components/loading-network-screen/loading-network-screen.component.js b/ui/app/components/loading-network-screen/loading-network-screen.component.js
deleted file mode 100644
index bf1c141e0..000000000
--- a/ui/app/components/loading-network-screen/loading-network-screen.component.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Spinner from '../spinner'
-import Button from '../button'
-
-export default class LoadingNetworkScreen extends PureComponent {
- state = {
- showErrorScreen: false,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- loadingMessage: PropTypes.string,
- cancelTime: PropTypes.number,
- provider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- providerId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- showNetworkDropdown: PropTypes.func,
- setProviderArgs: PropTypes.array,
- lastSelectedProvider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- setProviderType: PropTypes.func,
- isLoadingNetwork: PropTypes.bool,
- }
-
- componentDidMount = () => {
- this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
- }
-
- getConnectingLabel = function (loadingMessage) {
- if (loadingMessage) {
- return loadingMessage
- }
- const { provider, providerId } = this.props
- const providerName = provider.type
-
- let name
-
- if (providerName === 'mainnet') {
- name = this.context.t('connectingToMainnet')
- } else if (providerName === 'ropsten') {
- name = this.context.t('connectingToRopsten')
- } else if (providerName === 'kovan') {
- name = this.context.t('connectingToKovan')
- } else if (providerName === 'rinkeby') {
- name = this.context.t('connectingToRinkeby')
- } else {
- name = this.context.t('connectingTo', [providerId])
- }
-
- return name
- }
-
- renderMessage = () => {
- return <span>{ this.getConnectingLabel(this.props.loadingMessage) }</span>
- }
-
- renderLoadingScreenContent = () => {
- return <div className="loading-overlay__screen-content">
- <Spinner color="#F7C06C" />
- {this.renderMessage()}
- </div>
- }
-
- renderErrorScreenContent = () => {
- const { showNetworkDropdown, setProviderArgs, setProviderType } = this.props
-
- return <div className="loading-overlay__error-screen">
- <span className="loading-overlay__emoji">&#128542;</span>
- <span>{ this.context.t('somethingWentWrong') }</span>
- <div className="loading-overlay__error-buttons">
- <Button
- type="default"
- onClick={() => {
- window.clearTimeout(this.cancelCallTimeout)
- showNetworkDropdown()
- }}
- >
- { this.context.t('switchNetworks') }
- </Button>
-
- <Button
- type="primary"
- onClick={() => {
- this.setState({ showErrorScreen: false })
- setProviderType(...setProviderArgs)
- window.clearTimeout(this.cancelCallTimeout)
- this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
- }}
- >
- { this.context.t('tryAgain') }
- </Button>
- </div>
- </div>
- }
-
- cancelCall = () => {
- const { isLoadingNetwork } = this.props
-
- if (isLoadingNetwork) {
- this.setState({ showErrorScreen: true })
- }
- }
-
- componentDidUpdate = (prevProps) => {
- const { provider } = this.props
- const { provider: prevProvider } = prevProps
- if (provider.type !== prevProvider.type) {
- window.clearTimeout(this.cancelCallTimeout)
- this.setState({ showErrorScreen: false })
- this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000)
- }
- }
-
- componentWillUnmount = () => {
- window.clearTimeout(this.cancelCallTimeout)
- }
-
- render () {
- const { lastSelectedProvider, setProviderType } = this.props
-
- return (
- <div className="loading-overlay">
- <div
- className="page-container__header-close"
- onClick={() => setProviderType(lastSelectedProvider || 'ropsten')}
- />
- <div className="loading-overlay__container">
- { this.state.showErrorScreen
- ? this.renderErrorScreenContent()
- : this.renderLoadingScreenContent()
- }
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/loading-network-screen/loading-network-screen.container.js b/ui/app/components/loading-network-screen/loading-network-screen.container.js
deleted file mode 100644
index d0623e574..000000000
--- a/ui/app/components/loading-network-screen/loading-network-screen.container.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { connect } from 'react-redux'
-import LoadingNetworkScreen from './loading-network-screen.component'
-import actions from '../../actions'
-import { getNetworkIdentifier } from '../../selectors'
-
-const mapStateToProps = state => {
- const {
- loadingMessage,
- currentView,
- } = state.appState
- const {
- provider,
- lastSelectedProvider,
- network,
- } = state.metamask
- const { rpcTarget, chainId, ticker, nickname, type } = provider
-
- const setProviderArgs = type === 'rpc'
- ? [rpcTarget, chainId, ticker, nickname]
- : [provider.type]
-
- return {
- isLoadingNetwork: network === 'loading' && currentView.name !== 'config',
- loadingMessage,
- lastSelectedProvider,
- setProviderArgs,
- provider,
- providerId: getNetworkIdentifier(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setProviderType: (type) => {
- dispatch(actions.setProviderType(type))
- },
- showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(LoadingNetworkScreen)
diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js
deleted file mode 100644
index 29c56953d..000000000
--- a/ui/app/components/menu-bar/menu-bar.component.js
+++ /dev/null
@@ -1,79 +0,0 @@
-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,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- hideSidebar: PropTypes.func,
- sidebarOpen: PropTypes.bool,
- showSidebar: PropTypes.func,
- }
-
- state = { accountDetailsMenuOpen: false }
-
- render () {
- const { t } = this.context
- const { 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={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Opened Hamburger',
- },
- })
- sidebarOpen ? hideSidebar() : showSidebar()
- }}
- />
- </Tooltip>
- <SelectedAccount />
-
- <Tooltip
- title={t('accountOptions')}
- position="bottom"
- >
- <div
- className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser"
- onClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Opened Account Options',
- },
- })
- 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
deleted file mode 100644
index 0305f17d3..000000000
--- a/ui/app/components/menu-bar/menu-bar.container.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { connect } from 'react-redux'
-import { WALLET_VIEW_SIDEBAR } from '../sidebars/sidebar.constants'
-import MenuBar from './menu-bar.component'
-import { showSidebar, hideSidebar } from '../../actions'
-
-const mapStateToProps = state => {
- const { appState: { sidebar: { isOpen } } } = state
-
- return {
- sidebarOpen: isOpen,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showSidebar: () => {
- dispatch(showSidebar({
- transitionName: 'sidebar-right',
- type: WALLET_VIEW_SIDEBAR,
- }))
- },
- hideSidebar: () => dispatch(hideSidebar()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
diff --git a/ui/app/components/modal/index.scss b/ui/app/components/modal/index.scss
deleted file mode 100644
index 2beb14633..000000000
--- a/ui/app/components/modal/index.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-@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.component.js b/ui/app/components/modal/modal.component.js
deleted file mode 100644
index c73f8d903..000000000
--- a/ui/app/components/modal/modal.component.js
+++ /dev/null
@@ -1,83 +0,0 @@
-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,
- submitDisabled: PropTypes.bool,
- // 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,
- submitDisabled,
- 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}
- disabled={submitDisabled}
- 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
deleted file mode 100644
index 2ced3f32d..000000000
--- a/ui/app/components/modal/tests/modal.component.test.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { mount, 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)
- })
-
- it('should disable the submit button if submitDisabled is true', () => {
- const handleCancel = sinon.spy()
- const handleSubmit = sinon.spy()
- const wrapper = mount(
- <Modal
- onCancel={handleCancel}
- cancelText="Cancel"
- onSubmit={handleSubmit}
- submitText="Submit"
- submitDisabled={true}
- headerText="My Header"
- onClose={handleCancel}
- />
- )
-
- const buttons = wrapper.find(Button)
- assert.equal(buttons.length, 2)
- const cancelButton = buttons.at(0)
- const submitButton = buttons.at(1)
-
- assert.equal(handleCancel.callCount, 0)
- cancelButton.simulate('click')
- assert.equal(handleCancel.callCount, 1)
-
- assert.equal(submitButton.props().disabled, true)
- assert.equal(handleSubmit.callCount, 0)
- submitButton.simulate('click')
- 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
deleted file mode 100644
index 67d8eb0fd..000000000
--- a/ui/app/components/modals/account-details-modal.js
+++ /dev/null
@@ -1,101 +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 AccountModalContainer = require('./account-modal-container')
-const { getSelectedIdentity } = require('../../selectors')
-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,
- selectedIdentity: getSelectedIdentity(state),
- keyrings: state.metamask.keyrings,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- // Is this supposed to be used somewhere?
- showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
- showExportPrivateKeyModal: () => {
- dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
- },
- hideModal: () => dispatch(actions.hideModal()),
- setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
- }
-}
-
-inherits(AccountDetailsModal, Component)
-function AccountDetailsModal () {
- Component.call(this)
-}
-
-AccountDetailsModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal)
-
-
-// Not yet pixel perfect todos:
- // fonts of qr-header
-
-AccountDetailsModal.prototype.render = function () {
- const {
- selectedIdentity,
- network,
- showExportPrivateKeyModal,
- setAccountLabel,
- keyrings,
- } = this.props
- const { name, address } = selectedIdentity
-
- const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(address)
- })
-
- let exportPrivateKeyFeatureEnabled = true
- // This feature is disabled for hardware wallets
- if (keyring && keyring.type.search('Hardware') !== -1) {
- exportPrivateKeyFeatureEnabled = false
- }
-
- return h(AccountModalContainer, {}, [
- h(EditableLabel, {
- className: 'account-modal__name',
- defaultValue: name,
- onSubmit: label => setAccountLabel(address, label),
- }),
-
- h(QrView, {
- Qr: {
- data: address,
- network: network,
- },
- }),
-
- h('div.account-modal-divider'),
-
- 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, {
- 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
deleted file mode 100644
index 2a6c655e1..000000000
--- a/ui/app/components/modals/account-modal-container.js
+++ /dev/null
@@ -1,80 +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 { getSelectedIdentity } = require('../../selectors')
-import Identicon from '../identicon'
-
-function mapStateToProps (state, ownProps) {
- return {
- selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- }
-}
-
-inherits(AccountModalContainer, Component)
-function AccountModalContainer () {
- Component.call(this)
-}
-
-AccountModalContainer.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContainer)
-
-
-AccountModalContainer.prototype.render = function () {
- const {
- selectedIdentity,
- showBackButton = false,
- backButtonAction,
- } = this.props
- let { children } = this.props
-
- if (children.constructor !== Array) {
- children = [children]
- }
-
- return h('div', { style: { borderRadius: '4px' }}, [
- h('div.account-modal-container', [
-
- h('div', [
-
- // Needs a border; requires changes to svg
- h(Identicon, {
- address: selectedIdentity.address,
- diameter: 64,
- style: {},
- }),
-
- ]),
-
- showBackButton && h('div.account-modal-back', {
- onClick: backButtonAction,
- }, [
-
- h('i.fa.fa-angle-left.fa-lg'),
-
- h('span.account-modal-back__text', ' ' + this.context.t('back')),
-
- ]),
-
- h('div.account-modal-close', {
- onClick: this.props.hideModal,
- }),
-
- ...children,
-
- ]),
- ])
-}
diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js
deleted file mode 100644
index c70510b5f..000000000
--- a/ui/app/components/modals/buy-options-modal.js
+++ /dev/null
@@ -1,101 +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 { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- address: state.metamask.selectedAddress,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- toCoinbase: (address) => {
- dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
- },
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- showAccountDetailModal: () => {
- dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
- },
- toFaucet: network => dispatch(actions.buyEth({ network })),
- }
-}
-
-inherits(BuyOptions, Component)
-function BuyOptions () {
- Component.call(this)
-}
-
-BuyOptions.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions)
-
-
-BuyOptions.prototype.renderModalContentOption = function (title, header, onClick) {
- return h('div.buy-modal-content-option', {
- onClick,
- }, [
- h('div.buy-modal-content-option-title', {}, title),
- h('div.buy-modal-content-option-subtitle', {}, header),
- ])
-}
-
-BuyOptions.prototype.render = function () {
- const { network, toCoinbase, address, toFaucet } = this.props
- const isTestNetwork = ['3', '4', '42'].find(n => n === network)
- const networkName = getNetworkDisplayName(network)
-
- return h('div', {}, [
- h('div.buy-modal-content.transfers-subview', {
- }, [
- h('div.buy-modal-content-title-wrapper.flex-column.flex-center', {
- style: {},
- }, [
- h('div.buy-modal-content-title', {
- style: {},
- }, this.context.t('transfers')),
- h('div', {}, this.context.t('howToDeposit')),
- ]),
-
- h('div.buy-modal-content-options.flex-column.flex-center', {}, [
-
- isTestNetwork
- ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network))
- : this.renderModalContentOption('Coinbase', this.context.t('depositFiat'), () => toCoinbase(address)),
-
- // h('div.buy-modal-content-option', {}, [
- // h('div.buy-modal-content-option-title', {}, 'Shapeshift'),
- // h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'),
- // ]),,
-
- this.renderModalContentOption(
- this.context.t('directDeposit'),
- this.context.t('depositFromAccount'),
- () => this.goToAccountDetailsModal()
- ),
-
- ]),
-
- h('button', {
- style: {
- background: 'white',
- },
- onClick: () => { this.props.hideModal() },
- }, h('div.buy-modal-content-footer#buy-modal-content-footer-text', {}, this.context.t('cancel'))),
- ]),
- ])
-}
-
-BuyOptions.prototype.goToAccountDetailsModal = function () {
- this.props.hideModal()
- this.props.showAccountDetailModal()
-}
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
deleted file mode 100644
index b973f221c..000000000
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } 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">
- <UserPreferencedCurrencyDisplay
- className="cancel-transaction-gas-fee__eth"
- value={value}
- type={PRIMARY}
- />
- <UserPreferencedCurrencyDisplay
- className="cancel-transaction-gas-fee__fiat"
- value={value}
- type={SECONDARY}
- />
- </div>
- )
- }
-}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js
deleted file mode 100644
index 8fd7b2679..000000000
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js
+++ /dev/null
@@ -1,76 +0,0 @@
-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,
- }
-
- state = {
- busy: false,
- }
-
- componentDidUpdate () {
- const { transactionStatus, showTransactionConfirmedModal } = this.props
-
- if (transactionStatus !== SUBMITTED_STATUS) {
- showTransactionConfirmedModal()
- return
- }
- }
-
- handleSubmit = async () => {
- const { createCancelTransaction, hideModal } = this.props
-
- this.setState({ busy: true })
-
- await createCancelTransaction()
- this.setState({ busy: false }, () => hideModal())
- }
-
- handleCancel = () => {
- this.props.hideModal()
- }
-
- render () {
- const { t } = this.context
- const { newGasFee } = this.props
- const { busy } = this.state
-
- return (
- <Modal
- headerText={t('attemptToCancel')}
- onClose={this.handleCancel}
- onSubmit={this.handleSubmit}
- onCancel={this.handleCancel}
- submitText={t('yesLetsTry')}
- cancelText={t('nevermind')}
- submitType="secondary"
- submitDisabled={busy}
- >
- <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
deleted file mode 100644
index 10931a001..000000000
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js
+++ /dev/null
@@ -1,60 +0,0 @@
-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,
- defaultNewGasPrice,
- newGasFee,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- createCancelTransaction: (txId, customGasPrice) => {
- return dispatch(createCancelTransaction(txId, customGasPrice))
- },
- showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps
- const { createCancelTransaction, ...restDispatchProps } = dispatchProps
-
- return {
- ...restStateProps,
- ...restDispatchProps,
- ...ownProps,
- createCancelTransaction: () => createCancelTransaction(transactionId, defaultNewGasPrice),
- }
-}
-
-export default compose(
- withModalProps,
- connect(mapStateToProps, mapDispatchToProps, mergeProps),
-)(CancelTransaction)
diff --git a/ui/app/components/modals/cancel-transaction/index.scss b/ui/app/components/modals/cancel-transaction/index.scss
deleted file mode 100644
index 62e8e36fd..000000000
--- a/ui/app/components/modals/cancel-transaction/index.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@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/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js
deleted file mode 100644
index 3a801a062..000000000
--- a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import withModalProps from '../../../higher-order-components/with-modal-props'
-import ClearApprovedOriginsComponent from './clear-approved-origins.component'
-import { clearApprovedOrigins } from '../../../actions'
-
-const mapDispatchToProps = dispatch => {
- return {
- clearApprovedOrigins: () => dispatch(clearApprovedOrigins()),
- }
-}
-
-export default compose(
- withModalProps,
- connect(null, mapDispatchToProps)
-)(ClearApprovedOriginsComponent)
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
deleted file mode 100644
index 195c55421..000000000
--- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import Modal from '../../modal'
-import { addressSummary } from '../../../util'
-import Identicon from '../../identicon'
-import genAccountLink from '../../../../lib/account-link'
-
-export default class ConfirmRemoveAccount extends Component {
- static propTypes = {
- hideModal: PropTypes.func.isRequired,
- removeAccount: PropTypes.func.isRequired,
- identity: PropTypes.object.isRequired,
- network: PropTypes.string.isRequired,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- handleRemove = () => {
- this.props.removeAccount(this.props.identity.address)
- .then(() => this.props.hideModal())
- }
-
- handleCancel = () => {
- this.props.hideModal()
- }
-
- renderSelectedAccount () {
- const { identity } = this.props
- return (
- <div className="confirm-remove-account__account">
- <div className="confirm-remove-account__account__identicon">
- <Identicon
- address={identity.address}
- diameter={32}
- />
- </div>
- <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="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="confirm-remove-account__account__link">
- <a
- className=""
- href={genAccountLink(identity.address, this.props.network)}
- target={'_blank'}
- title={this.context.t('etherscanView')}
- >
- <img src="images/popout.svg" />
- </a>
- </div>
- </div>
- )
- }
-
- render () {
- const { t } = this.context
-
- return (
- <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="confirm-remove-account__link"
- rel="noopener noreferrer"
- target="_blank" href="https://metamask.zendesk.com/hc/en-us/articles/360015289932">
- { t('learnMore') }
- </a>
- </div>
- </div>
- </Modal>
- )
- }
-}
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
deleted file mode 100644
index 45c6654ab..000000000
--- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import ConfirmRemoveAccount from './confirm-remove-account.component'
-import withModalProps from '../../../higher-order-components/with-modal-props'
-import { removeAccount } from '../../../actions'
-
-const mapStateToProps = state => {
- return {
- network: state.metamask.network,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- removeAccount: (address) => dispatch(removeAccount(address)),
- }
-}
-
-export default compose(
- withModalProps,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmRemoveAccount)
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
deleted file mode 100644
index c8a7b8478..000000000
--- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js
+++ /dev/null
@@ -1,16 +0,0 @@
-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'
-import { resetAccount } from '../../../actions'
-
-const mapDispatchToProps = dispatch => {
- return {
- resetAccount: () => dispatch(resetAccount()),
- }
-}
-
-export default compose(
- withModalProps,
- connect(null, mapDispatchToProps)
-)(ConfirmResetAccount)
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
deleted file mode 100644
index 4e2e20660..000000000
--- a/ui/app/components/modals/customize-gas/customize-gas.component.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import BigNumber from 'bignumber.js'
-import GasModalCard from '../../customize-gas-modal/gas-modal-card'
-import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
-import Button from '../../button'
-
-import {
- getDecimalGasLimit,
- getDecimalGasPrice,
- getPrefixedHexGasLimit,
- getPrefixedHexGasPrice,
-} from './customize-gas.util'
-
-export default class CustomizeGas extends Component {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- txData: PropTypes.object.isRequired,
- hideModal: PropTypes.func,
- validate: PropTypes.func,
- onSubmit: PropTypes.func,
- }
-
- state = {
- gasPrice: 0,
- gasLimit: 0,
- originalGasPrice: 0,
- originalGasLimit: 0,
- }
-
- componentDidMount () {
- const { txData = {} } = this.props
- const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData
-
- const gasLimit = getDecimalGasLimit(hexGasLimit)
- const gasPrice = getDecimalGasPrice(hexGasPrice)
-
- this.setState({
- gasPrice,
- gasLimit,
- originalGasPrice: gasPrice,
- originalGasLimit: gasLimit,
- })
- }
-
- handleRevert () {
- const { originalGasPrice, originalGasLimit } = this.state
-
- this.setState({
- gasPrice: originalGasPrice,
- gasLimit: originalGasLimit,
- })
- }
-
- handleSave () {
- const { onSubmit, hideModal } = this.props
- const { gasLimit, gasPrice } = this.state
- const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice)
- const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit)
-
- Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit }))
- .then(() => hideModal())
- }
-
- validate () {
- const { gasLimit, gasPrice } = this.state
- return this.props.validate({
- gasPrice: getPrefixedHexGasPrice(gasPrice),
- gasLimit: getPrefixedHexGasLimit(gasLimit),
- })
- }
-
- render () {
- const { t, metricsEvent } = this.context
- const { hideModal } = this.props
- const { gasPrice, gasLimit, originalGasPrice, originalGasLimit } = this.state
- const { valid, errorKey } = this.validate()
-
- return (
- <div className="customize-gas">
- <div className="customize-gas__content">
- <div className="customize-gas__header">
- <div className="customize-gas__title">
- { this.context.t('customGas') }
- </div>
- <div
- className="customize-gas__close"
- onClick={() => hideModal()}
- />
- </div>
- <div className="customize-gas__body">
- <GasModalCard
- value={gasPrice}
- min={MIN_GAS_PRICE_GWEI}
- step={1}
- onChange={value => this.setState({ gasPrice: value })}
- title={t('gasPrice')}
- copy={t('gasPriceCalculation')}
- />
- <GasModalCard
- value={gasLimit}
- min={1}
- step={1}
- onChange={value => this.setState({ gasLimit: value })}
- title={t('gasLimit')}
- copy={t('gasLimitCalculation')}
- />
- </div>
- <div className="customize-gas__footer">
- { !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> }
- <div
- className="customize-gas__revert"
- onClick={() => this.handleRevert()}
- >
- { t('revert') }
- </div>
- <div className="customize-gas__buttons">
- <Button
- type="default"
- className="customize-gas__cancel"
- onClick={() => hideModal()}
- style={{ marginRight: '10px' }}
- >
- { t('cancel') }
- </Button>
- <Button
- type="primary"
- className="customize-gas__save"
- onClick={() => {
- metricsEvent({
- eventOpts: {
- category: 'Activation',
- action: 'userCloses',
- name: 'closeCustomizeGas',
- },
- pageOpts: {
- section: 'customizeGasModal',
- component: 'customizeGasSaveButton',
- },
- customVariables: {
- gasPriceChange: (new BigNumber(gasPrice)).minus(new BigNumber(originalGasPrice)).toString(10),
- gasLimitChange: (new BigNumber(gasLimit)).minus(new BigNumber(originalGasLimit)).toString(10),
- },
- })
- this.handleSave()
- }}
- style={{ marginRight: '10px' }}
- disabled={!valid}
- >
- { t('save') }
- </Button>
- </div>
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/modals/customize-gas/customize-gas.container.js
deleted file mode 100644
index 46a799795..000000000
--- a/ui/app/components/modals/customize-gas/customize-gas.container.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { connect } from 'react-redux'
-import CustomizeGas from './customize-gas.component'
-import { hideModal } from '../../../actions'
-
-const mapStateToProps = state => {
- const { appState: { modal: { modalState: { props } } } } = state
- const { txData, onSubmit, validate } = props
-
- return {
- txData,
- onSubmit,
- validate,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- hideModal: () => dispatch(hideModal()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas)
diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/modals/customize-gas/customize-gas.util.js
deleted file mode 100644
index 6ba4a7705..000000000
--- a/ui/app/components/modals/customize-gas/customize-gas.util.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import ethUtil from 'ethereumjs-util'
-import { conversionUtil } from '../../../conversion-util'
-
-export function getDecimalGasLimit (hexGasLimit) {
- return conversionUtil(hexGasLimit, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
-}
-
-export function getDecimalGasPrice (hexGasPrice) {
- return conversionUtil(hexGasPrice, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- toDenomination: 'GWEI',
- })
-}
-
-export function getPrefixedHexGasLimit (gasLimit) {
- return ethUtil.addHexPrefix(conversionUtil(gasLimit, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- }))
-}
-
-export function getPrefixedHexGasPrice (gasPrice) {
- return ethUtil.addHexPrefix(conversionUtil(gasPrice, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- fromDenomination: 'GWEI',
- toDenomination: 'WEI',
- }))
-}
diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js
deleted file mode 100644
index 09137d39a..000000000
--- a/ui/app/components/modals/deposit-ether-modal.js
+++ /dev/null
@@ -1,220 +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 { 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
-let COINBASE_ROW_TEXT
-let SHAPESHIFT_ROW_TITLE
-let SHAPESHIFT_ROW_TEXT
-let FAUCET_ROW_TITLE
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- address: state.metamask.selectedAddress,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- toCoinbase: (address) => {
- dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
- },
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- hideWarning: () => {
- dispatch(actions.hideWarning())
- },
- showAccountDetailModal: () => {
- dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
- },
- toFaucet: network => dispatch(actions.buyEth({ network })),
- }
-}
-
-inherits(DepositEtherModal, Component)
-function DepositEtherModal (props, context) {
- Component.call(this)
-
- // need to set after i18n locale has loaded
- DIRECT_DEPOSIT_ROW_TITLE = context.t('directDepositEther')
- DIRECT_DEPOSIT_ROW_TEXT = context.t('directDepositEtherExplainer')
- COINBASE_ROW_TITLE = context.t('buyCoinbase')
- COINBASE_ROW_TEXT = context.t('buyCoinbaseExplainer')
- SHAPESHIFT_ROW_TITLE = context.t('depositShapeShift')
- SHAPESHIFT_ROW_TEXT = context.t('depositShapeShiftExplainer')
- FAUCET_ROW_TITLE = context.t('testFaucet')
-
- this.state = {
- buyingWithShapeshift: false,
- }
-}
-
-DepositEtherModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(DepositEtherModal)
-
-
-DepositEtherModal.prototype.facuetRowText = function (networkName) {
- return this.context.t('getEtherFromFaucet', [networkName])
-}
-
-DepositEtherModal.prototype.renderRow = function ({
- logo,
- title,
- text,
- buttonLabel,
- onButtonClick,
- hide,
- className,
- hideButton,
- hideTitle,
- onBackClick,
- showBackButton,
-}) {
- if (hide) {
- return null
- }
-
- return h('div', {
- className: className || 'deposit-ether-modal__buy-row',
- }, [
-
- onBackClick && showBackButton && h('div.deposit-ether-modal__buy-row__back', {
- onClick: onBackClick,
- }, [
-
- h('i.fa.fa-arrow-left.cursor-pointer'),
-
- ]),
-
- h('div.deposit-ether-modal__buy-row__logo-container', [logo]),
-
- h('div.deposit-ether-modal__buy-row__description', [
-
- !hideTitle && h('div.deposit-ether-modal__buy-row__description__title', [title]),
-
- h('div.deposit-ether-modal__buy-row__description__text', [text]),
-
- ]),
-
- !hideButton && h('div.deposit-ether-modal__buy-row__button', [
- h(Button, {
- type: 'primary',
- className: 'deposit-ether-modal__deposit-button',
- large: true,
- onClick: onButtonClick,
- }, [buttonLabel]),
- ]),
-
- ])
-}
-
-DepositEtherModal.prototype.render = function () {
- const { network, toCoinbase, address, toFaucet } = this.props
- const { buyingWithShapeshift } = this.state
-
- const isTestNetwork = ['3', '4', '42'].find(n => n === network)
- const networkName = getNetworkDisplayName(network)
-
- return h('div.page-container.page-container--full-width.page-container--full-height', {}, [
-
- h('div.page-container__header', [
-
- h('div.page-container__title', [this.context.t('depositEther')]),
-
- h('div.page-container__subtitle', [
- this.context.t('needEtherInWallet'),
- ]),
-
- h('div.page-container__header-close', {
- onClick: () => {
- this.setState({ buyingWithShapeshift: false })
- this.props.hideWarning()
- this.props.hideModal()
- },
- }),
-
- ]),
-
- h('.page-container__content', {}, [
-
- h('div.deposit-ether-modal__buy-rows', [
-
- this.renderRow({
- logo: h('img.deposit-ether-modal__logo', {
- src: './images/deposit-eth.svg',
- }),
- title: DIRECT_DEPOSIT_ROW_TITLE,
- text: DIRECT_DEPOSIT_ROW_TEXT,
- buttonLabel: this.context.t('viewAccount'),
- onButtonClick: () => this.goToAccountDetailsModal(),
- hide: buyingWithShapeshift,
- }),
-
- this.renderRow({
- logo: h('i.fa.fa-tint.fa-2x'),
- title: FAUCET_ROW_TITLE,
- text: this.facuetRowText(networkName),
- buttonLabel: this.context.t('getEther'),
- onButtonClick: () => toFaucet(network),
- hide: !isTestNetwork || buyingWithShapeshift,
- }),
-
- this.renderRow({
- logo: h('div.deposit-ether-modal__logo', {
- style: {
- backgroundImage: 'url(\'./images/coinbase logo.png\')',
- height: '40px',
- },
- }),
- title: COINBASE_ROW_TITLE,
- text: COINBASE_ROW_TEXT,
- buttonLabel: this.context.t('continueToCoinbase'),
- onButtonClick: () => toCoinbase(address),
- hide: isTestNetwork || buyingWithShapeshift,
- }),
-
- this.renderRow({
- logo: h('div.deposit-ether-modal__logo', {
- style: {
- backgroundImage: 'url(\'./images/shapeshift logo.png\')',
- },
- }),
- title: SHAPESHIFT_ROW_TITLE,
- text: SHAPESHIFT_ROW_TEXT,
- buttonLabel: this.context.t('shapeshiftBuy'),
- onButtonClick: () => this.setState({ buyingWithShapeshift: true }),
- hide: isTestNetwork,
- hideButton: buyingWithShapeshift,
- hideTitle: buyingWithShapeshift,
- onBackClick: () => this.setState({ buyingWithShapeshift: false }),
- showBackButton: this.state.buyingWithShapeshift,
- className: buyingWithShapeshift && 'deposit-ether-modal__buy-row__shapeshift-buy',
- }),
-
- buyingWithShapeshift && h(ShapeshiftForm),
-
- ]),
-
- ]),
- ])
-}
-
-DepositEtherModal.prototype.goToAccountDetailsModal = function () {
- this.props.hideWarning()
- this.props.hideModal()
- this.props.showAccountDetailModal()
-}
diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js
deleted file mode 100644
index edced8725..000000000
--- a/ui/app/components/modals/edit-account-name-modal.js
+++ /dev/null
@@ -1,83 +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 { getSelectedAccount } = require('../../selectors')
-
-function mapStateToProps (state) {
- return {
- selectedAccount: getSelectedAccount(state),
- identity: state.appState.modal.modalState.props.identity,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- setAccountLabel: (account, label) => {
- dispatch(actions.setAccountLabel(account, label))
- },
- }
-}
-
-inherits(EditAccountNameModal, Component)
-function EditAccountNameModal (props) {
- Component.call(this)
-
- this.state = {
- inputText: props.identity.name,
- }
-}
-
-EditAccountNameModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameModal)
-
-
-EditAccountNameModal.prototype.render = function () {
- const { hideModal, setAccountLabel, identity } = this.props
-
- return h('div', {}, [
- h('div.flex-column.edit-account-name-modal-content', {
- }, [
-
- h('div.edit-account-name-modal-cancel', {
- onClick: () => {
- hideModal()
- },
- }, [
- h('i.fa.fa-times'),
- ]),
-
- h('div.edit-account-name-modal-title', {
- }, [this.context.t('editAccountName')]),
-
- h('input.edit-account-name-modal-input', {
- placeholder: identity.name,
- onChange: (event) => {
- this.setState({ inputText: event.target.value })
- },
- value: this.state.inputText,
- }, []),
-
- h('button.btn-clear.edit-account-name-modal-save-button.allcaps', {
- onClick: () => {
- if (this.state.inputText.length !== 0) {
- setAccountLabel(identity.address, this.state.inputText)
- hideModal()
- }
- },
- disabled: this.state.inputText.length === 0,
- }, [
- this.context.t('save'),
- ]),
-
- ]),
- ])
-}
diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js
deleted file mode 100644
index d3e3c9a56..000000000
--- a/ui/app/components/modals/export-private-key-modal.js
+++ /dev/null
@@ -1,177 +0,0 @@
-const log = require('loglevel')
-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 { stripHexPrefix } = require('ethereumjs-util')
-const actions = require('../../actions')
-const AccountModalContainer = require('./account-modal-container')
-const { getSelectedIdentity } = require('../../selectors')
-const ReadOnlyInput = require('../readonly-input')
-const copyToClipboard = require('copy-to-clipboard')
-const { checksumAddress } = require('../../util')
-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) => {
- return dispatch(actions.exportAccount(password, address))
- .then((res) => {
- dispatch(actions.hideWarning())
- return res
- })
- },
- showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
- hideModal: () => dispatch(actions.hideModal()),
- }
-}
-
-inherits(ExportPrivateKeyModal, Component)
-function ExportPrivateKeyModal () {
- Component.call(this)
-
- this.state = {
- password: '',
- privateKey: null,
- showWarning: true,
- }
-}
-
-ExportPrivateKeyModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
-
-
-ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
- const { exportAccount } = this.props
-
- exportAccount(password, address)
- .then(privateKey => this.setState({
- privateKey,
- showWarning: false,
- }))
- .catch((e) => log.error(e))
-}
-
-ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
- return h('span.private-key-password-label', privateKey
- ? this.context.t('copyPrivateKey')
- : this.context.t('typePassword')
- )
-}
-
-ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
- const plainKey = privateKey && stripHexPrefix(privateKey)
-
- return privateKey
- ? h(ReadOnlyInput, {
- wrapperClass: 'private-key-password-display-wrapper',
- inputClass: 'private-key-password-display-textarea',
- textarea: true,
- value: plainKey,
- onClick: () => copyToClipboard(plainKey),
- })
- : h('input.private-key-password-input', {
- type: 'password',
- onChange: event => this.setState({ password: event.target.value }),
- })
-}
-
-ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
- return h('div.export-private-key-buttons', {}, [
- !privateKey && h(Button, {
- type: 'default',
- large: true,
- className: 'export-private-key__button export-private-key__button--cancel',
- onClick: () => hideModal(),
- }, this.context.t('cancel')),
-
- (privateKey
- ? (
- 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'))
- )
- ),
-
- ])
-}
-
-ExportPrivateKeyModal.prototype.render = function () {
- const {
- selectedIdentity,
- warning,
- showAccountDetailModal,
- hideModal,
- previousModalState,
- } = this.props
- const { name, address } = selectedIdentity
-
- const {
- privateKey,
- showWarning,
- } = this.state
-
- return h(AccountModalContainer, {
- selectedIdentity,
- showBackButton: previousModalState === 'ACCOUNT_DETAILS',
- backButtonAction: () => showAccountDetailModal(),
- }, [
-
- h('span.account-name', name),
-
- h(ReadOnlyInput, {
- wrapperClass: 'ellip-address-wrapper',
- inputClass: 'qr-ellip-address ellip-address',
- value: checksumAddress(address),
- }),
-
- h('div.account-modal-divider'),
-
- h('span.modal-body-title', this.context.t('showPrivateKeys')),
-
- h('div.private-key-password', {}, [
- this.renderPasswordLabel(privateKey),
-
- this.renderPasswordInput(privateKey),
-
- showWarning && warning ? h('span.private-key-password-error', warning) : null,
- ]),
-
- h('div.private-key-password-warning', this.context.t('privateKeyWarning')),
-
- this.renderButtons(privateKey, this.state.password, address, hideModal),
-
- ])
-}
diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js
deleted file mode 100644
index 43f3009a5..000000000
--- a/ui/app/components/modals/hide-token-confirmation-modal.js
+++ /dev/null
@@ -1,83 +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')
-import Identicon from '../identicon'
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- token: state.appState.modal.modalState.props.token,
- assetImages: state.metamask.assetImages,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => dispatch(actions.hideModal()),
- hideToken: address => {
- dispatch(actions.removeToken(address))
- .then(() => {
- dispatch(actions.hideModal())
- })
- },
- }
-}
-
-inherits(HideTokenConfirmationModal, Component)
-function HideTokenConfirmationModal () {
- Component.call(this)
-
- this.state = {}
-}
-
-HideTokenConfirmationModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmationModal)
-
-
-HideTokenConfirmationModal.prototype.render = function () {
- 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', {
- }, [
- h('div.hide-token-confirmation__title', {}, [
- this.context.t('hideTokenPrompt'),
- ]),
-
- h(Identicon, {
- className: 'hide-token-confirmation__identicon',
- diameter: 45,
- address,
- network,
- image,
- }),
-
- h('div.hide-token-confirmation__symbol', {}, symbol),
-
- h('div.hide-token-confirmation__copy', {}, [
- this.context.t('readdToken'),
- ]),
-
- h('div.hide-token-confirmation__buttons', {}, [
- h('button.btn-cancel.hide-token-confirmation__button.allcaps', {
- onClick: () => hideModal(),
- }, [
- this.context.t('cancel'),
- ]),
- h('button.btn-clear.hide-token-confirmation__button.allcaps', {
- onClick: () => hideToken(address),
- }, [
- this.context.t('hide'),
- ]),
- ]),
- ]),
- ])
-}
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
deleted file mode 100644
index 555da87ef..000000000
--- a/ui/app/components/modals/index.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import './cancel-transaction/index';
-
-@import './confirm-remove-account/index';
-
-@import './customize-gas/index';
-
-@import './qr-scanner/index';
-
-@import './transaction-confirmed/index';
-
-@import './metametrics-opt-in-modal/index';
diff --git a/ui/app/components/modals/loading-network-error/loading-network-error.container.js b/ui/app/components/modals/loading-network-error/loading-network-error.container.js
deleted file mode 100644
index 3fcba20aa..000000000
--- a/ui/app/components/modals/loading-network-error/loading-network-error.container.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import LoadingNetworkError from './loading-network-error.component'
-import withModalProps from '../../../higher-order-components/with-modal-props'
-
-export default withModalProps(LoadingNetworkError)
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
deleted file mode 100644
index de3d1d3a3..000000000
--- a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainerFooter from '../../page-container/page-container-footer'
-
-export default class MetaMetricsOptInModal extends Component {
- static propTypes = {
- setParticipateInMetaMetrics: PropTypes.func,
- hideModal: PropTypes.func,
- }
-
- static contextTypes = {
- metricsEvent: PropTypes.func,
- }
-
- render () {
- const { metricsEvent } = this.context
- const { setParticipateInMetaMetrics, hideModal } = this.props
-
- return (
- <div className="metametrics-opt-in metametrics-opt-in-modal">
- <div className="metametrics-opt-in__main">
- <div className="metametrics-opt-in__content">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <div className="metametrics-opt-in__body-graphic">
- <img src="images/metrics-chart.svg" />
- </div>
- <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
- <div className="metametrics-opt-in__body">
- <div className="metametrics-opt-in__description">
- MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
- will be used to continually improve the usability and user experience of our product and the etheruem ecosystem.
- </div>
- <div className="metametrics-opt-in__description">
- MetaMask will..
- </div>
-
- <div className="metametrics-opt-in__committments">
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Always allow you to opt-out via Settings
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Send anonymized click & pageview events
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Maintain a public aggregate dashboard to educate the community
- </div>
- </div>
- <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
- </div>
- </div>
- </div>
- </div>
- <div className="metametrics-opt-in__bottom-text">
- This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
- href="https://metamask.io/privacy.html"
- target="_blank"
- rel="noopener noreferrer"
- >
- Privacy Policy here
- </a>.
- </div>
- </div>
- <div className="metametrics-opt-in__footer">
- <PageContainerFooter
- onCancel={() => {
- setParticipateInMetaMetrics(false)
- .then(() => {
- metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Metrics Option',
- name: 'Metrics Opt Out',
- },
- isOptIn: true,
- }, {
- excludeMetaMetricsId: true,
- })
- hideModal()
- })
- }}
- cancelText={'No Thanks'}
- hideCancel={false}
- onSubmit={() => {
- setParticipateInMetaMetrics(true)
- .then(() => {
- metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Metrics Option',
- name: 'Metrics Opt In',
- },
- isOptIn: true,
- })
- hideModal()
- })
- }}
- submitText={'I agree'}
- submitButtonType={'confirm'}
- disabled={false}
- />
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
deleted file mode 100644
index 525806b75..000000000
--- a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import MetaMetricsOptInModal from './metametrics-opt-in-modal.component'
-import withModalProps from '../../../higher-order-components/with-modal-props'
-import { setParticipateInMetaMetrics } from '../../../actions'
-
-const mapStateToProps = (state, ownProps) => {
- const { unapprovedTxCount } = ownProps
-
- return {
- unapprovedTxCount,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
- }
-}
-
-export default compose(
- withModalProps,
- connect(mapStateToProps, mapDispatchToProps),
-)(MetaMetricsOptInModal)
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
deleted file mode 100644
index 8ab599a71..000000000
--- a/ui/app/components/modals/modal.js
+++ /dev/null
@@ -1,511 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const FadeModal = require('boron').FadeModal
-const actions = require('../../actions')
-const { resetCustomData: resetCustomGasData } = require('../../ducks/gas.duck')
-const isMobileView = require('../../../lib/is-mobile-view')
-const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
-
-// Modal Components
-const BuyOptions = require('./buy-options-modal')
-const DepositEtherModal = require('./deposit-ether-modal')
-const AccountDetailsModal = require('./account-details-modal')
-const EditAccountNameModal = require('./edit-account-name-modal')
-const ExportPrivateKeyModal = require('./export-private-key-modal')
-const NewAccountModal = require('./new-account-modal')
-const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
-const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
-const NotifcationModal = require('./notification-modal')
-const QRScanner = require('./qr-scanner')
-
-import ConfirmRemoveAccount from './confirm-remove-account'
-import ConfirmResetAccount from './confirm-reset-account'
-import TransactionConfirmed from './transaction-confirmed'
-import CancelTransaction from './cancel-transaction'
-
-import MetaMetricsOptInModal from './metametrics-opt-in-modal'
-import RejectTransactions from './reject-transactions'
-import ClearApprovedOrigins from './clear-approved-origins'
-import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
-
-const modalContainerBaseStyle = {
- transform: 'translate3d(-50%, 0, 0px)',
- border: '1px solid #CCCFD1',
- borderRadius: '8px',
- backgroundColor: '#FFFFFF',
- boxShadow: '0 2px 22px 0 rgba(0,0,0,0.2)',
-}
-
-const modalContainerLaptopStyle = {
- ...modalContainerBaseStyle,
- width: '344px',
- top: '15%',
-}
-
-const modalContainerMobileStyle = {
- ...modalContainerBaseStyle,
- width: '309px',
- top: '12.5%',
-}
-
-const accountModalStyle = {
- mobileModalStyle: {
- width: '95%',
- // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
- boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
- borderRadius: '4px',
- top: '10%',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: '360px',
- // top: 'calc(33% + 45px)',
- boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
- borderRadius: '4px',
- top: '10%',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- contentStyle: {
- borderRadius: '4px',
- },
-}
-
-const MODALS = {
- BUY: {
- contents: [
- h(BuyOptions, {}, []),
- ],
- mobileModalStyle: {
- width: '95%',
- // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
- top: '10%',
- },
- laptopModalStyle: {
- width: '66%',
- maxWidth: '550px',
- top: 'calc(10% + 10px)',
- left: '0',
- right: '0',
- margin: '0 auto',
- boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
- transform: 'none',
- },
- },
-
- DEPOSIT_ETHER: {
- contents: [
- h(DepositEtherModal, {}, []),
- ],
- onHide: (props) => props.hideWarning(),
- mobileModalStyle: {
- width: '100%',
- height: '100%',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
- top: '0',
- display: 'flex',
- },
- laptopModalStyle: {
- width: 'initial',
- maxWidth: '850px',
- top: 'calc(10% + 10px)',
- left: '0',
- right: '0',
- margin: '0 auto',
- boxShadow: '0 0 6px 0 rgba(0,0,0,0.3)',
- borderRadius: '7px',
- transform: 'none',
- height: 'calc(80% - 20px)',
- overflowY: 'hidden',
- },
- contentStyle: {
- borderRadius: '7px',
- height: '100%',
- },
- },
-
- EDIT_ACCOUNT_NAME: {
- contents: [
- h(EditAccountNameModal, {}, []),
- ],
- mobileModalStyle: {
- width: '95%',
- // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
- top: '10%',
- boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: '375px',
- // top: 'calc(30% + 10px)',
- top: '10%',
- boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- },
-
- ACCOUNT_DETAILS: {
- contents: [
- h(AccountDetailsModal, {}, []),
- ],
- ...accountModalStyle,
- },
-
- EXPORT_PRIVATE_KEY: {
- contents: [
- h(ExportPrivateKeyModal, {}, []),
- ],
- ...accountModalStyle,
- },
-
- SHAPESHIFT_DEPOSIT_TX: {
- contents: [
- h(ShapeshiftDepositTxModal),
- ],
- ...accountModalStyle,
- },
-
- HIDE_TOKEN_CONFIRMATION: {
- contents: [
- h(HideTokenConfirmationModal, {}, []),
- ],
- mobileModalStyle: {
- width: '95%',
- top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
- },
- laptopModalStyle: {
- width: '449px',
- top: 'calc(33% + 45px)',
- },
- },
-
- CLEAR_APPROVED_ORIGINS: {
- contents: h(ClearApprovedOrigins),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- METAMETRICS_OPT_IN_MODAL: {
- contents: h(MetaMetricsOptInModal),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- width: '100%',
- height: '100%',
- top: '0px',
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- top: '10%',
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- OLD_UI_NOTIFICATION_MODAL: {
- contents: [
- h(NotifcationModal, {
- header: 'oldUI',
- message: 'oldUIMessage',
- }),
- ],
- mobileModalStyle: {
- width: '95%',
- top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
- },
- laptopModalStyle: {
- width: '449px',
- top: 'calc(33% + 45px)',
- },
- },
-
- GAS_PRICE_INFO_MODAL: {
- contents: [
- h(NotifcationModal, {
- header: 'gasPriceNoDenom',
- message: 'gasPriceInfoModalContent',
- }),
- ],
- mobileModalStyle: {
- width: '95%',
- top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
- },
- laptopModalStyle: {
- width: '449px',
- top: 'calc(33% + 45px)',
- },
- },
-
- GAS_LIMIT_INFO_MODAL: {
- contents: [
- h(NotifcationModal, {
- header: 'gasLimit',
- message: 'gasLimitInfoModalContent',
- }),
- ],
- mobileModalStyle: {
- width: '95%',
- top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
- },
- laptopModalStyle: {
- width: '449px',
- top: 'calc(33% + 45px)',
- },
- },
-
- CONFIRM_RESET_ACCOUNT: {
- contents: h(ConfirmResetAccount),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- CONFIRM_REMOVE_ACCOUNT: {
- contents: h(ConfirmRemoveAccount),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- NEW_ACCOUNT: {
- contents: [
- h(NewAccountModal, {}, []),
- ],
- mobileModalStyle: {
- width: '95%',
- // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
- top: '10%',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: '449px',
- // top: 'calc(33% + 45px)',
- top: '10%',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- },
-
- CUSTOMIZE_GAS: {
- contents: [
- h(ConfirmCustomizeGasModal),
- ],
- mobileModalStyle: {
- width: '100vw',
- height: '100vh',
- top: '0',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: 'auto',
- height: '0px',
- top: '80px',
- left: '0px',
- transform: 'none',
- margin: '0 auto',
- position: 'relative',
- },
- contentStyle: {
- borderRadius: '8px',
- },
- customOnHideOpts: {
- action: resetCustomGasData,
- args: [],
- },
- },
-
- TRANSACTION_CONFIRMED: {
- disableBackdropClick: true,
- contents: h(TransactionConfirmed),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- QR_SCANNER: {
- contents: h(QRScanner),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- CANCEL_TRANSACTION: {
- contents: h(CancelTransaction),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- REJECT_TRANSACTIONS: {
- contents: h(RejectTransactions),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- DEFAULT: {
- contents: [],
- mobileModalStyle: {},
- laptopModalStyle: {},
- },
-}
-
-const BACKDROPSTYLE = {
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
-}
-
-function mapStateToProps (state) {
- return {
- active: state.appState.modal.open,
- modalState: state.appState.modal.modalState,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: (customOnHideOpts) => {
- dispatch(actions.hideModal())
- if (customOnHideOpts && customOnHideOpts.action) {
- dispatch(customOnHideOpts.action(...customOnHideOpts.args))
- }
- },
- hideWarning: () => {
- dispatch(actions.hideWarning())
- },
-
- }
-}
-
-// Global Modal Component
-inherits(Modal, Component)
-function Modal () {
- Component.call(this)
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal)
-
-Modal.prototype.render = function () {
- const modal = MODALS[this.props.modalState.name || 'DEFAULT']
-
- const { contents: children, disableBackdropClick = false } = modal
- const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle']
- const contentStyle = modal.contentStyle || {}
-
- return h(FadeModal,
- {
- className: 'modal',
- keyboard: false,
- onHide: () => {
- if (modal.onHide) {
- modal.onHide(this.props)
- }
- this.onHide(modal.customOnHideOpts)
- },
- ref: (ref) => {
- this.modalRef = ref
- },
- modalStyle,
- contentStyle,
- backdropStyle: BACKDROPSTYLE,
- closeOnClick: !disableBackdropClick,
- },
- children,
- )
-}
-
-Modal.prototype.componentWillReceiveProps = function (nextProps) {
- if (nextProps.active) {
- this.show()
- } else if (this.props.active) {
- this.hide()
- }
-}
-
-Modal.prototype.onHide = function (customOnHideOpts) {
- if (this.props.onHideCallback) {
- this.props.onHideCallback()
- }
- this.props.hideModal(customOnHideOpts)
-}
-
-Modal.prototype.hide = function () {
- this.modalRef.hide()
-}
-
-Modal.prototype.show = function () {
- this.modalRef.show()
-}
diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js
deleted file mode 100644
index a66a3ed4a..000000000
--- a/ui/app/components/modals/new-account-modal.js
+++ /dev/null
@@ -1,112 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('../../actions')
-
-class NewAccountModal extends Component {
- constructor (props) {
- super(props)
- const { numberOfExistingAccounts = 0 } = props
- const newAccountNumber = numberOfExistingAccounts + 1
-
- this.state = {
- newAccountName: `${this.context.t('account')} ${newAccountNumber}`,
- }
- }
-
- render () {
- const { newAccountName } = this.state
-
- return h('div', [
- h('div.new-account-modal-wrapper', {
- }, [
- h('div.new-account-modal-header', {}, [
- this.context.t('newAccount'),
- ]),
-
- h('div.modal-close-x', {
- onClick: this.props.hideModal,
- }),
-
- h('div.new-account-modal-content', {}, [
- this.context.t('accountName'),
- ]),
-
- h('div.new-account-input-wrapper', {}, [
- h('input.new-account-input', {
- value: this.state.newAccountName,
- placeholder: this.context.t('sampleAccountName'),
- onChange: event => this.setState({ newAccountName: event.target.value }),
- }, []),
- ]),
-
- h('div.new-account-modal-content.after-input', {}, [
- this.context.t('or'),
- ]),
-
- h('div.new-account-modal-content.after-input.pointer', {
- onClick: () => {
- this.props.hideModal()
- this.props.showImportPage()
- },
- }, this.context.t('importAnAccount')),
-
- h('div.new-account-modal-content.button.allcaps', {}, [
- h('button.btn-clear', {
- onClick: () => this.props.createAccount(newAccountName),
- }, [
- this.context.t('save'),
- ]),
- ]),
- ]),
- ])
- }
-}
-
-NewAccountModal.propTypes = {
- hideModal: PropTypes.func,
- showImportPage: PropTypes.func,
- createAccount: PropTypes.func,
- numberOfExistingAccounts: PropTypes.number,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const { metamask: { network, selectedAddress, identities = {} } } = state
- const numberOfExistingAccounts = Object.keys(identities).length
-
- return {
- network,
- address: selectedAddress,
- numberOfExistingAccounts,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- toCoinbase: (address) => {
- dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
- },
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- createAccount: (newAccountName) => {
- dispatch(actions.addNewAccount())
- .then((newAccountAddress) => {
- if (newAccountName) {
- dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
- }
- dispatch(actions.hideModal())
- })
- },
- showImportPage: () => dispatch(actions.showImportPage()),
- }
-}
-
-NewAccountModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal)
-
diff --git a/ui/app/components/modals/notification-modal.js b/ui/app/components/modals/notification-modal.js
deleted file mode 100644
index 46a4c8a21..000000000
--- a/ui/app/components/modals/notification-modal.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('../../actions')
-
-class NotificationModal extends Component {
- render () {
- const {
- header,
- message,
- showCancelButton = false,
- showConfirmButton = false,
- hideModal,
- onConfirm,
- } = this.props
-
- const showButtons = showCancelButton || showConfirmButton
-
- return h('div', [
- h('div.notification-modal__wrapper', {
- }, [
-
- h('div.notification-modal__header', {}, [
- this.context.t(header),
- ]),
-
- h('div.notification-modal__message-wrapper', {}, [
- h('div.notification-modal__message', {}, [
- this.context.t(message),
- ]),
- ]),
-
- h('div.modal-close-x', {
- onClick: hideModal,
- }),
-
- showButtons && h('div.notification-modal__buttons', [
-
- showCancelButton && h('div.btn-cancel.notification-modal__buttons__btn', {
- onClick: hideModal,
- }, 'Cancel'),
-
- showConfirmButton && h('div.btn-clear.notification-modal__buttons__btn', {
- onClick: () => {
- onConfirm()
- hideModal()
- },
- }, 'Confirm'),
-
- ]),
-
- ]),
- ])
- }
-}
-
-NotificationModal.propTypes = {
- hideModal: PropTypes.func,
- header: PropTypes.string,
- message: PropTypes.node,
- showCancelButton: PropTypes.bool,
- showConfirmButton: PropTypes.bool,
- onConfirm: PropTypes.func,
- t: PropTypes.func,
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- }
-}
-
-NotificationModal.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(null, mapDispatchToProps)(NotificationModal)
-
diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/modals/qr-scanner/qr-scanner.component.js
deleted file mode 100644
index cb8d07d83..000000000
--- a/ui/app/components/modals/qr-scanner/qr-scanner.component.js
+++ /dev/null
@@ -1,216 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import { BrowserQRCodeReader } from '@zxing/library'
-import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars
-import Spinner from '../../spinner'
-import WebcamUtils from '../../../../lib/webcam-utils'
-import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component'
-
-export default class QrScanner extends Component {
- static propTypes = {
- hideModal: PropTypes.func.isRequired,
- qrCodeDetected: PropTypes.func,
- scanQrCode: PropTypes.func,
- error: PropTypes.bool,
- errorType: PropTypes.string,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- constructor (props, context) {
- super(props)
-
- this.state = {
- ready: false,
- msg: context.t('accessingYourCamera'),
- }
- this.codeReader = null
- this.permissionChecker = null
- this.needsToReinit = false
-
- // Clear pre-existing qr code data before scanning
- this.props.qrCodeDetected(null)
- }
-
- componentDidMount () {
- this.initCamera()
- }
-
- async checkPermisisions () {
- const { permissions } = await WebcamUtils.checkStatus()
- if (permissions) {
- clearTimeout(this.permissionChecker)
- // Let the video stream load first...
- setTimeout(_ => {
- this.setState({
- ready: true,
- msg: this.context.t('scanInstructions'),
- })
- if (this.needsToReinit) {
- this.initCamera()
- this.needsToReinit = false
- }
- }, 2000)
- } else {
- // Keep checking for permissions
- this.permissionChecker = setTimeout(_ => {
- this.checkPermisisions()
- }, 1000)
- }
- }
-
- componentWillUnmount () {
- clearTimeout(this.permissionChecker)
- if (this.codeReader) {
- this.codeReader.reset()
- }
- }
-
- initCamera () {
- this.codeReader = new BrowserQRCodeReader()
- this.codeReader.getVideoInputDevices()
- .then(videoInputDevices => {
- clearTimeout(this.permissionChecker)
- this.checkPermisisions()
- this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
- .then(content => {
- const result = this.parseContent(content.text)
- if (result.type !== 'unknown') {
- this.props.qrCodeDetected(result)
- this.stopAndClose()
- } else {
- this.setState({msg: this.context.t('unknownQrCode')})
- }
- })
- .catch(err => {
- if (err && err.name === 'NotAllowedError') {
- this.setState({msg: this.context.t('youNeedToAllowCameraAccess')})
- clearTimeout(this.permissionChecker)
- this.needsToReinit = true
- this.checkPermisisions()
- }
- })
- }).catch(err => {
- console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err)
- })
- }
-
- parseContent (content) {
- let type = 'unknown'
- let values = {}
-
- // Here we could add more cases
- // To parse other type of links
- // For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
-
-
- // Ethereum address links - fox ex. ethereum:0x.....1111
- if (content.split('ethereum:').length > 1) {
-
- type = 'address'
- values = {'address': content.split('ethereum:')[1] }
-
- // Regular ethereum addresses - fox ex. 0x.....1111
- } else if (content.substring(0, 2).toLowerCase() === '0x') {
-
- type = 'address'
- values = {'address': content }
-
- }
- return {type, values}
- }
-
-
- stopAndClose = () => {
- if (this.codeReader) {
- this.codeReader.reset()
- }
- this.setState({ ready: false })
- this.props.hideModal()
- }
-
- tryAgain = () => {
- // close the modal
- this.stopAndClose()
- // wait for the animation and try again
- setTimeout(_ => {
- this.props.scanQrCode()
- }, 1000)
- }
-
- renderVideo () {
- return (
- <div className={'qr-scanner__content__video-wrapper'}>
- <video
- id="video"
- style={{
- display: this.state.ready ? 'block' : 'none',
- }}
- />
- { !this.state.ready ? <Spinner color={'#F7C06C'} /> : null}
- </div>
- )
- }
-
- renderErrorModal () {
- let title, msg
-
- if (this.props.error) {
- if (this.props.errorType === 'NO_WEBCAM_FOUND') {
- title = this.context.t('noWebcamFoundTitle')
- msg = this.context.t('noWebcamFound')
- } else {
- title = this.context.t('unknownCameraErrorTitle')
- msg = this.context.t('unknownCameraError')
- }
- }
-
- return (
- <div className="qr-scanner">
- <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
-
- <div className="qr-scanner__image">
- <img src={'images/webcam.svg'} width={70} height={70} />
- </div>
- <div className="qr-scanner__title">
- { title }
- </div>
- <div className={'qr-scanner__error'}>
- {msg}
- </div>
- <PageContainerFooter
- onCancel={this.stopAndClose}
- onSubmit={this.tryAgain}
- cancelText={this.context.t('cancel')}
- submitText={this.context.t('tryAgain')}
- submitButtonType="confirm"
- />
- </div>
- )
- }
-
- render () {
- const { t } = this.context
-
- if (this.props.error) {
- return this.renderErrorModal()
- }
-
- return (
- <div className="qr-scanner">
- <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
- <div className="qr-scanner__title">
- { `${t('scanQrCode')}` }
- </div>
- <div className="qr-scanner__content">
- { this.renderVideo() }
- </div>
- <div className={'qr-scanner__status'}>
- {this.state.msg}
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/modals/qr-scanner/qr-scanner.container.js
deleted file mode 100644
index d0a35e03b..000000000
--- a/ui/app/components/modals/qr-scanner/qr-scanner.container.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { connect } from 'react-redux'
-import QrScanner from './qr-scanner.component'
-
-const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions')
-import {
- SEND_ROUTE,
-} from '../../../routes'
-
-const mapStateToProps = state => {
- return {
- error: state.appState.modal.modalState.props.error,
- errorType: state.appState.modal.modalState.props.errorType,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- hideModal: () => dispatch(hideModal()),
- qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
- scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(QrScanner)
diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/modals/reject-transactions/reject-transactions.container.js
deleted file mode 100644
index 81e98d3ff..000000000
--- a/ui/app/components/modals/reject-transactions/reject-transactions.container.js
+++ /dev/null
@@ -1,17 +0,0 @@
-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/shapeshift-deposit-tx-modal.js b/ui/app/components/modals/shapeshift-deposit-tx-modal.js
deleted file mode 100644
index 242c7b89d..000000000
--- a/ui/app/components/modals/shapeshift-deposit-tx-modal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const actions = require('../../actions')
-const QrView = require('../qr-code')
-const AccountModalContainer = require('./account-modal-container')
-
-function mapStateToProps (state) {
- return {
- Qr: state.appState.modal.modalState.props.Qr,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- hideModal: () => {
- dispatch(actions.hideModal())
- },
- }
-}
-
-inherits(ShapeshiftDepositTxModal, Component)
-function ShapeshiftDepositTxModal () {
- Component.call(this)
-
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftDepositTxModal)
-
-ShapeshiftDepositTxModal.prototype.render = function () {
- const { Qr } = this.props
-
- return h(AccountModalContainer, {
- }, [
- h('div', {}, [
- h(QrView, {key: 'qr', Qr}),
- ]),
- ])
-}
diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js
deleted file mode 100644
index d4e39681a..000000000
--- a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js
+++ /dev/null
@@ -1,4 +0,0 @@
-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/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js
deleted file mode 100644
index 22d617099..000000000
--- a/ui/app/components/network-display/network-display.component.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import {
- MAINNET_CODE,
- ROPSTEN_CODE,
- RINKEYBY_CODE,
- KOVAN_CODE,
-} from '../../../../app/scripts/controllers/network/enums'
-
-const networkToClassHash = {
- [MAINNET_CODE]: 'mainnet',
- [ROPSTEN_CODE]: 'ropsten',
- [RINKEYBY_CODE]: 'rinkeby',
- [KOVAN_CODE]: 'kovan',
-}
-
-export default class NetworkDisplay extends Component {
- static defaultProps = {
- colored: true,
- }
-
- static propTypes = {
- colored: PropTypes.bool,
- network: PropTypes.string,
- provider: PropTypes.object,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- renderNetworkIcon () {
- const { network } = this.props
- const networkClass = networkToClassHash[network]
-
- return networkClass
- ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
- : <div
- className="i fa fa-question-circle fa-med"
- style={{
- margin: '0 4px',
- color: 'rgb(125, 128, 130)',
- }}
- />
- }
-
- render () {
- const { colored, network, provider: { type, nickname } } = this.props
- const networkClass = networkToClassHash[network]
-
- return (
- <div
- className={classnames('network-display__container', {
- 'network-display__container--colored': colored,
- ['network-display__container--' + networkClass]: colored && networkClass,
- })}
- >
- {
- networkClass
- ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
- : <div
- className="i fa fa-question-circle fa-med"
- style={{
- margin: '0 4px',
- color: 'rgb(125, 128, 130)',
- }}
- />
- }
- <div className="network-display__name">
- { type === 'rpc' && nickname ? nickname : this.context.t(type) }
- </div>
- </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
deleted file mode 100644
index 198889cf2..000000000
--- a/ui/app/components/pages/add-token/add-token.component.js
+++ /dev/null
@@ -1,335 +0,0 @@
-import React, { Component } from 'react'
-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 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'
-const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
-
-class AddToken extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- setPendingTokens: PropTypes.func,
- pendingTokens: PropTypes.object,
- clearPendingTokens: PropTypes.func,
- tokens: PropTypes.array,
- identities: PropTypes.object,
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- customAddress: '',
- customSymbol: '',
- customDecimals: 0,
- searchResults: [],
- selectedTokens: {},
- tokenSelectorError: null,
- customAddressError: null,
- customSymbolError: null,
- customDecimalsError: null,
- autoFilled: false,
- displayedTab: SEARCH_TAB,
- forceEditSymbol: false,
- }
- }
-
- componentDidMount () {
- this.tokenInfoGetter = tokenInfoGetter()
- const { pendingTokens = {} } = this.props
- const pendingTokenKeys = Object.keys(pendingTokens)
-
- if (pendingTokenKeys.length > 0) {
- let selectedTokens = {}
- let customToken = {}
-
- pendingTokenKeys.forEach(tokenAddress => {
- const token = pendingTokens[tokenAddress]
- const { isCustom } = token
-
- if (isCustom) {
- customToken = { ...token }
- } else {
- selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
- }
- })
-
- const {
- address: customAddress = '',
- symbol: customSymbol = '',
- decimals: customDecimals = 0,
- } = customToken
-
- const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
- this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
- }
- }
-
- handleToggleToken (token) {
- const { address } = token
- const { selectedTokens = {} } = this.state
- const selectedTokensCopy = { ...selectedTokens }
-
- if (address in selectedTokensCopy) {
- delete selectedTokensCopy[address]
- } else {
- selectedTokensCopy[address] = token
- }
-
- this.setState({
- selectedTokens: selectedTokensCopy,
- tokenSelectorError: null,
- })
- }
-
- hasError () {
- const {
- tokenSelectorError,
- customAddressError,
- customSymbolError,
- customDecimalsError,
- } = this.state
-
- return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
- }
-
- hasSelected () {
- const { customAddress = '', selectedTokens = {} } = this.state
- return customAddress || Object.keys(selectedTokens).length > 0
- }
-
- handleNext () {
- if (this.hasError()) {
- return
- }
-
- if (!this.hasSelected()) {
- this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
- return
- }
-
- const { setPendingTokens, history } = this.props
- const {
- customAddress: address,
- customSymbol: symbol,
- customDecimals: decimals,
- selectedTokens,
- } = this.state
-
- const customToken = {
- address,
- symbol,
- decimals,
- }
-
- setPendingTokens({ customToken, selectedTokens })
- history.push(CONFIRM_ADD_TOKEN_ROUTE)
- }
-
- async attemptToAutoFillTokenParams (address) {
- const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address)
-
- const autoFilled = Boolean(symbol && decimals)
- this.setState({ autoFilled })
- this.handleCustomSymbolChange(symbol || '')
- this.handleCustomDecimalsChange(decimals)
- }
-
- handleCustomAddressChange (value) {
- const customAddress = value.trim()
- this.setState({
- customAddress,
- customAddressError: null,
- tokenSelectorError: null,
- autoFilled: false,
- })
-
- const isValidAddress = ethUtil.isValidAddress(customAddress)
- const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
-
- switch (true) {
- case !isValidAddress:
- this.setState({
- customAddressError: this.context.t('invalidAddress'),
- customSymbol: '',
- customDecimals: 0,
- customSymbolError: null,
- customDecimalsError: null,
- })
-
- break
- case Boolean(this.props.identities[standardAddress]):
- this.setState({
- customAddressError: this.context.t('personalAddressDetected'),
- })
-
- break
- case checkExistingAddresses(customAddress, this.props.tokens):
- this.setState({
- customAddressError: this.context.t('tokenAlreadyAdded'),
- })
-
- break
- default:
- if (customAddress !== emptyAddr) {
- this.attemptToAutoFillTokenParams(customAddress)
- }
- }
- }
-
- handleCustomSymbolChange (value) {
- const customSymbol = value.trim()
- const symbolLength = customSymbol.length
- let customSymbolError = null
-
- if (symbolLength <= 0 || symbolLength >= 12) {
- customSymbolError = this.context.t('symbolBetweenZeroTwelve')
- }
-
- this.setState({ customSymbol, customSymbolError })
- }
-
- handleCustomDecimalsChange (value) {
- const customDecimals = value.trim()
- const validDecimals = customDecimals !== null &&
- customDecimals !== '' &&
- customDecimals >= 0 &&
- customDecimals <= 36
- let customDecimalsError = null
-
- if (!validDecimals) {
- customDecimalsError = this.context.t('decimalsMustZerotoTen')
- }
-
- this.setState({ customDecimals, customDecimalsError })
- }
-
- renderCustomTokenForm () {
- const {
- customAddress,
- customSymbol,
- customDecimals,
- customAddressError,
- customSymbolError,
- customDecimalsError,
- autoFilled,
- forceEditSymbol,
- } = this.state
-
- return (
- <div className="add-token__custom-token-form">
- <TextField
- id="custom-address"
- label={this.context.t('tokenContractAddress')}
- type="text"
- value={customAddress}
- onChange={e => this.handleCustomAddressChange(e.target.value)}
- error={customAddressError}
- fullWidth
- margin="normal"
- />
- <TextField
- id="custom-symbol"
- label={(
- <div className="add-token__custom-symbol__label-wrapper">
- <span className="add-token__custom-symbol__label">
- {this.context.t('tokenSymbol')}
- </span>
- {(autoFilled && !forceEditSymbol) && (
- <div
- className="add-token__custom-symbol__edit"
- onClick={() => this.setState({ forceEditSymbol: true })}
- >
- {this.context.t('edit')}
- </div>
- )}
- </div>
- )}
- type="text"
- value={customSymbol}
- onChange={e => this.handleCustomSymbolChange(e.target.value)}
- error={customSymbolError}
- fullWidth
- margin="normal"
- disabled={autoFilled && !forceEditSymbol}
- />
- <TextField
- id="custom-decimals"
- label={this.context.t('decimal')}
- type="number"
- value={customDecimals}
- onChange={e => this.handleCustomDecimalsChange(e.target.value)}
- error={customDecimalsError}
- fullWidth
- margin="normal"
- disabled={autoFilled}
- />
- </div>
- )
- }
-
- renderSearchToken () {
- const { tokenSelectorError, selectedTokens, searchResults } = this.state
-
- return (
- <div className="add-token__search-token">
- <TokenSearch
- onSearch={({ results = [] }) => this.setState({ searchResults: results })}
- error={tokenSelectorError}
- />
- <div className="add-token__token-list">
- <TokenList
- results={searchResults}
- selectedTokens={selectedTokens}
- onToggleToken={token => this.handleToggleToken(token)}
- />
- </div>
- </div>
- )
- }
-
- renderTabs () {
- return (
- <Tabs>
- <Tab name={this.context.t('search')}>
- { this.renderSearchToken() }
- </Tab>
- <Tab name={this.context.t('customToken')}>
- { this.renderCustomTokenForm() }
- </Tab>
- </Tabs>
- )
- }
-
- render () {
- const { history, clearPendingTokens } = this.props
-
- return (
- <PageContainer
- title={this.context.t('addTokens')}
- tabsComponent={this.renderTabs()}
- onSubmit={() => this.handleNext()}
- disabled={this.hasError() || !this.hasSelected()}
- onCancel={() => {
- clearPendingTokens()
- history.push(DEFAULT_ROUTE)
- }}
- />
- )
- }
-}
-
-export default AddToken
diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js
deleted file mode 100644
index 87671b156..000000000
--- a/ui/app/components/pages/add-token/add-token.container.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { connect } from 'react-redux'
-import AddToken from './add-token.component'
-
-const { setPendingTokens, clearPendingTokens } = require('../../../actions')
-
-const mapStateToProps = ({ metamask }) => {
- const { identities, tokens, pendingTokens } = metamask
- return {
- identities,
- tokens,
- pendingTokens,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
- clearPendingTokens: () => dispatch(clearPendingTokens()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss
deleted file mode 100644
index 1690c7654..000000000
--- a/ui/app/components/pages/add-token/index.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-@import './token-list/index';
-
-.add-token {
- &__custom-token-form {
- padding: 8px 16px 16px;
-
- input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- display: none;
- }
-
- input[type="number"]:hover::-webkit-inner-spin-button {
- -webkit-appearance: none;
- display: none;
- }
- }
-
- &__search-token {
- padding: 16px;
- }
-
- &__token-list {
- margin-top: 16px;
- }
-
- &__custom-symbol {
-
- &__label-wrapper {
- display: flex;
- flex-flow: row nowrap;
- }
-
- &__label {
- flex: 0 0 auto;
- }
-
- &__edit {
- flex: 1 1 auto;
- text-align: right;
- color: $curious-blue;
- padding-right: 4px;
- cursor: pointer;
- }
- }
-}
diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss
deleted file mode 100644
index e32739d59..000000000
--- a/ui/app/components/pages/add-token/token-list/index.scss
+++ /dev/null
@@ -1,65 +0,0 @@
-@import './token-list-placeholder/index';
-
-.token-list {
- &__title {
- font-size: .75rem;
- }
-
- &__tokens-container {
- display: flex;
- flex-direction: column;
- }
-
- &__token {
- transition: 200ms ease-in-out;
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- padding: 8px;
- margin-top: 8px;
- box-sizing: border-box;
- border-radius: 10px;
- cursor: pointer;
- border: 2px solid transparent;
- position: relative;
-
- &:hover {
- border: 2px solid rgba($malibu-blue, .5);
- }
-
- &--selected {
- border: 2px solid $malibu-blue !important;
- }
-
- &--disabled {
- opacity: .4;
- pointer-events: none;
- }
- }
-
- &__token-icon {
- width: 48px;
- height: 48px;
- background-repeat: no-repeat;
- background-size: contain;
- background-position: center;
- border-radius: 50%;
- background-color: $white;
- box-shadow: 0 2px 4px 0 rgba($black, .24);
- margin-right: 12px;
- flex: 0 0 auto;
- }
-
- &__token-data {
- display: flex;
- flex-direction: row;
- align-items: center;
- min-width: 0;
- }
-
- &__token-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-}
diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js
deleted file mode 100644
index 036b2db1e..000000000
--- a/ui/app/components/pages/add-token/token-search/token-search.component.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import contractMap from 'eth-contract-metadata'
-import Fuse from 'fuse.js'
-import InputAdornment from '@material-ui/core/InputAdornment'
-import TextField from '../../../text-field'
-
-const contractList = Object.entries(contractMap)
- .map(([ _, tokenData]) => tokenData)
- .filter(tokenData => Boolean(tokenData.erc20))
-
-const fuse = new Fuse(contractList, {
- shouldSort: true,
- threshold: 0.45,
- location: 0,
- distance: 100,
- maxPatternLength: 32,
- minMatchCharLength: 1,
- keys: [
- { name: 'name', weight: 0.5 },
- { name: 'symbol', weight: 0.5 },
- ],
-})
-
-export default class TokenSearch extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static defaultProps = {
- error: null,
- }
-
- static propTypes = {
- onSearch: PropTypes.func,
- error: PropTypes.string,
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- searchQuery: '',
- }
- }
-
- handleSearch (searchQuery) {
- this.setState({ searchQuery })
- const fuseSearchResult = fuse.search(searchQuery)
- const addressSearchResult = contractList.filter(token => {
- return token.address.toLowerCase() === searchQuery.toLowerCase()
- })
- const results = [...addressSearchResult, ...fuseSearchResult]
- this.props.onSearch({ searchQuery, results })
- }
-
- renderAdornment () {
- return (
- <InputAdornment
- position="start"
- style={{ marginRight: '12px' }}
- >
- <img src="images/search.svg" />
- </InputAdornment>
- )
- }
-
- render () {
- const { error } = this.props
- const { searchQuery } = this.state
-
- return (
- <TextField
- id="search-tokens"
- placeholder={this.context.t('searchTokens')}
- type="text"
- value={searchQuery}
- onChange={e => this.handleSearch(e.target.value)}
- error={error}
- fullWidth
- startAdornment={this.renderAdornment()}
- />
- )
- }
-}
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
deleted file mode 100644
index ee5d6fa64..000000000
--- a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
+++ /dev/null
@@ -1,122 +0,0 @@
-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
deleted file mode 100644
index 1f2737e52..000000000
--- a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
+++ /dev/null
@@ -1,29 +0,0 @@
-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-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
deleted file mode 100644
index d3fec79d7..000000000
--- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
+++ /dev/null
@@ -1,117 +0,0 @@
-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 '../../identicon'
-import TokenBalance from '../../token-balance'
-
-export default class ConfirmAddToken extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- clearPendingTokens: PropTypes.func,
- addTokens: PropTypes.func,
- pendingTokens: PropTypes.object,
- }
-
- 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 { history, addTokens, clearPendingTokens, pendingTokens } = this.props
-
- return (
- <div className="page-container">
- <div className="page-container__header">
- <div className="page-container__title">
- { this.context.t('addTokens') }
- </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 } = 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}
- />
- <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={() => 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/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
deleted file mode 100644
index 0190024d9..000000000
--- a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { connect } from 'react-redux'
-import ConfirmAddToken from './confirm-add-token.component'
-
-const { addTokens, clearPendingTokens } = require('../../../actions')
-
-const mapStateToProps = ({ metamask }) => {
- const { pendingTokens } = metamask
- return {
- pendingTokens,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- addTokens: tokens => dispatch(addTokens(tokens)),
- clearPendingTokens: () => dispatch(clearPendingTokens()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
deleted file mode 100644
index 4ef9f4ced..000000000
--- a/ui/app/components/pages/confirm-approve/confirm-approve.container.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { connect } from 'react-redux'
-import ConfirmApprove from './confirm-approve.component'
-import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
-
-const mapStateToProps = state => {
- const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
- const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
-
- return {
- tokenAmount,
- tokenSymbol,
- }
-}
-
-export default connect(mapStateToProps)(ConfirmApprove)
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
deleted file mode 100644
index 442a478b8..000000000
--- a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ConfirmTransactionBase from '../confirm-transaction-base'
-import { SEND_ROUTE } from '../../../routes'
-
-export default class ConfirmSendEther extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- editTransaction: PropTypes.func,
- history: PropTypes.object,
- txParams: PropTypes.object,
- }
-
- handleEdit ({ txData }) {
- const { editTransaction, history } = this.props
- editTransaction(txData)
- history.push(SEND_ROUTE)
- }
-
- shouldHideData () {
- const { txParams = {} } = this.props
- return !txParams.data
- }
-
- render () {
- const hideData = this.shouldHideData()
-
- return (
- <ConfirmTransactionBase
- action={this.context.t('confirm')}
- hideData={hideData}
- onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
- />
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
deleted file mode 100644
index e48ef54a8..000000000
--- a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router-dom'
-import { updateSend } from '../../../actions'
-import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
-import ConfirmSendEther from './confirm-send-ether.component'
-
-const mapStateToProps = state => {
- const { confirmTransaction: { txData: { txParams } = {} } } = state
-
- return {
- txParams,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- editTransaction: txData => {
- const { id, txParams } = txData
- const {
- gas: gasLimit,
- gasPrice,
- to,
- value: amount,
- } = txParams
-
- dispatch(updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount,
- errors: { to: null, amount: null },
- editingTransactionId: id && id.toString(),
- }))
-
- dispatch(clearConfirmTransaction())
- },
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendEther)
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
deleted file mode 100644
index cb39e3d7b..000000000
--- a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
-import { SEND_ROUTE } from '../../../routes'
-
-export default class ConfirmSendToken extends Component {
- static propTypes = {
- history: PropTypes.object,
- editTransaction: PropTypes.func,
- tokenAmount: PropTypes.number,
- }
-
- handleEdit (confirmTransactionData) {
- const { editTransaction, history } = this.props
- editTransaction(confirmTransactionData)
- history.push(SEND_ROUTE)
- }
-
- render () {
- const { tokenAmount } = this.props
-
- return (
- <ConfirmTokenTransactionBase
- onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
- tokenAmount={tokenAmount}
- />
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
deleted file mode 100644
index d60911e59..000000000
--- a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router-dom'
-import ConfirmSendToken from './confirm-send-token.component'
-import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
-import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
-import { conversionUtil } from '../../../conversion-util'
-import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
-
-const mapStateToProps = state => {
- const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
-
- return {
- tokenAmount,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- editTransaction: ({ txData, tokenData, tokenProps }) => {
- const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
- const { params = [] } = tokenData
- const { value: to } = params[0] || {}
- const { value: tokenAmountInDec } = params[1] || {}
- const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
- dispatch(setSelectedToken(tokenAddress))
- dispatch(updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount: tokenAmountInHex,
- errors: { to: null, amount: null },
- editingTransactionId: id && id.toString(),
- token: {
- ...tokenProps,
- address: tokenAddress,
- },
- }))
- dispatch(clearConfirmTransaction())
- dispatch(showSendTokenPage())
- },
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendToken)
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
deleted file mode 100644
index 7f1fb4e49..000000000
--- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ConfirmTransactionBase from '../confirm-transaction-base'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
-import {
- formatCurrency,
- convertTokenToFiat,
- addFiat,
- roundExponential,
-} from '../../../helpers/confirm-transaction/util'
-import { getWeiHexFromDecimalValue } from '../../../helpers/conversions.util'
-import { ETH, PRIMARY } from '../../../constants/common'
-
-export default class ConfirmTokenTransactionBase extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- tokenAddress: PropTypes.string,
- toAddress: PropTypes.string,
- tokenAmount: PropTypes.number,
- tokenSymbol: PropTypes.string,
- fiatTransactionTotal: PropTypes.string,
- ethTransactionTotal: PropTypes.string,
- contractExchangeRate: PropTypes.number,
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- }
-
- getFiatTransactionAmount () {
- const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
-
- return convertTokenToFiat({
- value: tokenAmount,
- toCurrency: currentCurrency,
- conversionRate,
- contractExchangeRate,
- })
- }
-
- renderSubtitleComponent () {
- const { contractExchangeRate, tokenAmount } = this.props
-
- const decimalEthValue = (tokenAmount * contractExchangeRate) || 0
- const hexWeiValue = getWeiHexFromDecimalValue({
- value: decimalEthValue,
- fromCurrency: ETH,
- fromDenomination: ETH,
- })
-
- return typeof contractExchangeRate === 'undefined'
- ? (
- <span>
- { this.context.t('noConversionRateAvailable') }
- </span>
- ) : (
- <UserPreferencedCurrencyDisplay
- value={hexWeiValue}
- type={PRIMARY}
- showEthLogo
- hideLabel
- />
- )
- }
-
- renderPrimaryTotalTextOverride () {
- const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props
- const tokensText = `${tokenAmount} ${tokenSymbol}`
-
- return (
- <div>
- <span>{ `${tokensText} + ` }</span>
- <img
- src="/images/eth.svg"
- height="18"
- />
- <span>{ ethTransactionTotal }</span>
- </div>
- )
- }
-
- getSecondaryTotalTextOverride () {
- const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
-
- if (typeof contractExchangeRate === 'undefined') {
- return formatCurrency(fiatTransactionTotal, currentCurrency)
- } else {
- const fiatTransactionAmount = this.getFiatTransactionAmount()
- const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
- const roundedFiatTotal = roundExponential(fiatTotal)
- return formatCurrency(roundedFiatTotal, currentCurrency)
- }
- }
-
- render () {
- const {
- toAddress,
- tokenAddress,
- tokenSymbol,
- tokenAmount,
- ...restProps
- } = this.props
-
- const tokensText = `${tokenAmount} ${tokenSymbol}`
-
- return (
- <ConfirmTransactionBase
- toAddress={toAddress}
- identiconAddress={tokenAddress}
- title={tokensText}
- subtitleComponent={this.renderSubtitleComponent()}
- primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()}
- secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()}
- {...restProps}
- />
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
deleted file mode 100644
index be38acdb0..000000000
--- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { connect } from 'react-redux'
-import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
-import {
- tokenAmountAndToAddressSelector,
- contractExchangeRateSelector,
-} from '../../../selectors/confirm-transaction'
-
-const mapStateToProps = (state, ownProps) => {
- const { tokenAmount: ownTokenAmount } = ownProps
- const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
- const {
- txData: { txParams: { to: tokenAddress } = {} } = {},
- tokenProps: { tokenSymbol } = {},
- fiatTransactionTotal,
- ethTransactionTotal,
- } = confirmTransaction
-
- const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
- const contractExchangeRate = contractExchangeRateSelector(state)
-
- return {
- toAddress,
- tokenAddress,
- tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
- tokenSymbol,
- currentCurrency,
- conversionRate,
- contractExchangeRate,
- fiatTransactionTotal,
- ethTransactionTotal,
- }
-}
-
-export default connect(mapStateToProps)(ConfirmTokenTransactionBase)
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
deleted file mode 100644
index 8b101b1ba..000000000
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ /dev/null
@@ -1,574 +0,0 @@
-import ethUtil from 'ethereumjs-util'
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
-import { isBalanceSufficient } from '../../send/send.utils'
-import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../../routes'
-import {
- INSUFFICIENT_FUNDS_ERROR_KEY,
- TRANSACTION_ERROR_KEY,
-} from '../../../constants/error-keys'
-import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../../constants/common'
-import AdvancedGasInputs from '../../gas-customization/advanced-gas-inputs'
-
-export default class ConfirmTransactionBase extends Component {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- // react-router props
- match: PropTypes.object,
- history: PropTypes.object,
- // Redux props
- balance: PropTypes.string,
- cancelTransaction: PropTypes.func,
- cancelAllTransactions: PropTypes.func,
- clearConfirmTransaction: PropTypes.func,
- clearSend: PropTypes.func,
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- editTransaction: PropTypes.func,
- ethTransactionAmount: PropTypes.string,
- ethTransactionFee: PropTypes.string,
- ethTransactionTotal: PropTypes.string,
- fiatTransactionAmount: PropTypes.string,
- fiatTransactionFee: PropTypes.string,
- fiatTransactionTotal: PropTypes.string,
- fromAddress: PropTypes.string,
- fromName: PropTypes.string,
- hexTransactionAmount: PropTypes.string,
- hexTransactionFee: PropTypes.string,
- hexTransactionTotal: PropTypes.string,
- 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,
- currentNetworkUnapprovedTxs: PropTypes.object,
- updateGasAndCalculate: PropTypes.func,
- customGas: PropTypes.object,
- // Component props
- action: PropTypes.string,
- contentComponent: PropTypes.node,
- dataComponent: PropTypes.node,
- detailsComponent: PropTypes.node,
- errorKey: PropTypes.string,
- errorMessage: PropTypes.string,
- primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- secondaryTotalTextOverride: PropTypes.string,
- hideData: PropTypes.bool,
- hideDetails: PropTypes.bool,
- hideSubtitle: PropTypes.bool,
- identiconAddress: PropTypes.string,
- onCancel: PropTypes.func,
- onEdit: PropTypes.func,
- onEditGas: PropTypes.func,
- onSubmit: PropTypes.func,
- setMetaMetricsSendCount: PropTypes.func,
- metaMetricsSendCount: PropTypes.number,
- subtitle: PropTypes.string,
- subtitleComponent: PropTypes.node,
- summaryComponent: PropTypes.node,
- title: PropTypes.string,
- titleComponent: PropTypes.node,
- valid: PropTypes.bool,
- warning: PropTypes.string,
- advancedInlineGasShown: PropTypes.bool,
- insufficientBalance: PropTypes.bool,
- hideFiatConversion: PropTypes.bool,
- }
-
- state = {
- submitting: false,
- submitError: null,
- }
-
- componentDidUpdate () {
- const {
- transactionStatus,
- showTransactionConfirmedModal,
- history,
- clearConfirmTransaction,
- } = this.props
-
- if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
- showTransactionConfirmedModal({
- onSubmit: () => {
- clearConfirmTransaction()
- history.push(DEFAULT_ROUTE)
- },
- })
-
- return
- }
- }
-
- getErrorKey () {
- const {
- balance,
- conversionRate,
- hexTransactionFee,
- txData: {
- simulationFails,
- txParams: {
- value: amount,
- } = {},
- } = {},
- } = this.props
-
- const insufficientBalance = balance && !isBalanceSufficient({
- amount,
- gasTotal: hexTransactionFee || '0x0',
- balance,
- conversionRate,
- })
-
- if (insufficientBalance) {
- return {
- valid: false,
- errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
- }
- }
-
- if (simulationFails) {
- return {
- valid: true,
- errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY,
- }
- }
-
- return {
- valid: true,
- }
- }
-
- handleEditGas () {
- const { onEditGas, showCustomizeGasModal, action, txData: { origin }, methodData = {} } = this.props
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Confirm Screen',
- name: 'User clicks "Edit" on gas',
- },
- customVariables: {
- recipientKnown: null,
- functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
- origin,
- },
- })
-
- if (onEditGas) {
- onEditGas()
- } else {
- showCustomizeGasModal()
- }
- }
-
- renderDetails () {
- const {
- detailsComponent,
- primaryTotalTextOverride,
- secondaryTotalTextOverride,
- hexTransactionFee,
- hexTransactionTotal,
- hideDetails,
- advancedInlineGasShown,
- customGas,
- insufficientBalance,
- updateGasAndCalculate,
- hideFiatConversion,
- } = this.props
-
- if (hideDetails) {
- return null
- }
-
- return (
- detailsComponent || (
- <div className="confirm-page-container-content__details">
- <div className="confirm-page-container-content__gas-fee">
- <ConfirmDetailRow
- label="Gas Fee"
- value={hexTransactionFee}
- headerText="Edit"
- headerTextClassName="confirm-detail-row__header-text--edit"
- onHeaderClick={() => this.handleEditGas()}
- secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : ''}
- />
- {advancedInlineGasShown
- ? <AdvancedGasInputs
- updateCustomGasPrice={newGasPrice => updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })}
- updateCustomGasLimit={newGasLimit => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit })}
- customGasPrice={customGas.gasPrice}
- customGasLimit={customGas.gasLimit}
- insufficientBalance={insufficientBalance}
- customPriceIsSafe={true}
- isSpeedUp={false}
- />
- : null
- }
- </div>
- <div>
- <ConfirmDetailRow
- label="Total"
- value={hexTransactionTotal}
- primaryText={primaryTotalTextOverride}
- secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : secondaryTotalTextOverride}
- headerText="Amount + Gas Fee"
- headerTextClassName="confirm-detail-row__header-text--total"
- primaryValueTextColor="#2f9ae0"
- />
- </div>
- </div>
- )
- )
- }
-
- renderData () {
- const { t } = this.context
- const {
- txData: {
- txParams: {
- data,
- } = {},
- } = {},
- methodData: {
- name,
- params,
- } = {},
- hideData,
- dataComponent,
- } = this.props
-
- if (hideData) {
- return null
- }
-
- return dataComponent || (
- <div className="confirm-page-container-content__data">
- <div className="confirm-page-container-content__data-box-label">
- {`${t('functionType')}:`}
- <span className="confirm-page-container-content__function-type">
- { name || t('notFound') }
- </span>
- </div>
- {
- params && (
- <div className="confirm-page-container-content__data-box">
- <div className="confirm-page-container-content__data-field-label">
- { `${t('parameters')}:` }
- </div>
- <div>
- <pre>{ JSON.stringify(params, null, 2) }</pre>
- </div>
- </div>
- )
- }
- <div className="confirm-page-container-content__data-box-label">
- {`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`}
- </div>
- <div className="confirm-page-container-content__data-box">
- { data }
- </div>
- </div>
- )
- }
-
- handleEdit () {
- const { txData, tokenData, tokenProps, onEdit, action, txData: { origin }, methodData = {} } = this.props
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Confirm Screen',
- name: 'Edit Transaction',
- },
- customVariables: {
- recipientKnown: null,
- functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
- origin,
- },
- })
-
- 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 { metricsEvent } = this.context
- const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, action, txData: { origin }, methodData = {} } = this.props
-
- if (onCancel) {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Confirm Screen',
- name: 'Cancel',
- },
- customVariables: {
- recipientKnown: null,
- functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
- origin,
- },
- })
- onCancel(txData)
- } else {
- cancelTransaction(txData)
- .then(() => {
- clearConfirmTransaction()
- history.push(DEFAULT_ROUTE)
- })
- }
- }
-
- handleSubmit () {
- const { metricsEvent } = this.context
- const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, action, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props
- const { submitting } = this.state
-
- if (submitting) {
- return
- }
-
- this.setState({
- submitting: true,
- submitError: null,
- }, () => {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Confirm Screen',
- name: 'Transaction Completed',
- },
- customVariables: {
- recipientKnown: null,
- functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
- origin,
- },
- })
-
- setMetaMetricsSendCount(metaMetricsSendCount + 1)
- .then(() => {
- if (onSubmit) {
- Promise.resolve(onSubmit(txData))
- .then(() => {
- this.setState({
- submitting: false,
- })
- })
- } else {
- sendTransaction(txData)
- .then(() => {
- clearConfirmTransaction()
- this.setState({
- submitting: false,
- }, () => {
- history.push(DEFAULT_ROUTE)
- })
- })
- .catch(error => {
- this.setState({
- submitting: false,
- submitError: error.message,
- })
- })
- }
- })
- })
- }
-
- renderTitleComponent () {
- const { title, titleComponent, hexTransactionAmount } = this.props
-
- // Title string passed in by props takes priority
- if (title) {
- return null
- }
-
- return titleComponent || (
- <UserPreferencedCurrencyDisplay
- value={hexTransactionAmount}
- type={PRIMARY}
- showEthLogo
- ethLogoHeight="26"
- hideLabel
- />
- )
- }
-
- renderSubtitleComponent () {
- const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
-
- // Subtitle string passed in by props takes priority
- if (subtitle) {
- return null
- }
-
- return subtitleComponent || (
- <UserPreferencedCurrencyDisplay
- value={hexTransactionAmount}
- type={SECONDARY}
- showEthLogo
- hideLabel
- />
- )
- }
-
- handleNextTx (txId) {
- const { history, clearConfirmTransaction } = this.props
- if (txId) {
- clearConfirmTransaction()
- history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`)
- }
- }
-
- getNavigateTxData () {
- const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props
- const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs).reverse()
- const currentPosition = enumUnapprovedTxs.indexOf(id.toString())
-
- return {
- totalTx: enumUnapprovedTxs.length,
- positionOfCurrentTx: currentPosition + 1,
- nextTxId: enumUnapprovedTxs[currentPosition + 1],
- prevTxId: enumUnapprovedTxs[currentPosition - 1],
- showNavigation: enumUnapprovedTxs.length > 1,
- firstTx: enumUnapprovedTxs[0],
- lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1],
- ofText: this.context.t('ofTextNofM'),
- requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'),
- }
- }
-
- componentDidMount () {
- const { txData: { origin } = {} } = this.props
- const { metricsEvent } = this.context
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Confirm Screen',
- name: 'Confirm: Started',
- },
- customVariables: {
- origin,
- },
- })
- }
-
- render () {
- const {
- isTxReprice,
- fromName,
- fromAddress,
- toName,
- toAddress,
- methodData,
- valid: propsValid = true,
- errorMessage,
- errorKey: propsErrorKey,
- action,
- title,
- subtitle,
- hideSubtitle,
- identiconAddress,
- summaryComponent,
- contentComponent,
- onEdit,
- nonce,
- assetImage,
- warning,
- unapprovedTxCount,
- } = this.props
- const { submitting, submitError } = this.state
-
- const { name } = methodData
- const { valid, errorKey } = this.getErrorKey()
- const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
-
- return (
- <ConfirmPageContainer
- fromName={fromName}
- fromAddress={fromAddress}
- toName={toName}
- toAddress={toAddress}
- showEdit={onEdit && !isTxReprice}
- action={action || getMethodName(name) || this.context.t('contractInteraction')}
- title={title}
- titleComponent={this.renderTitleComponent()}
- subtitle={subtitle}
- subtitleComponent={this.renderSubtitleComponent()}
- hideSubtitle={hideSubtitle}
- summaryComponent={summaryComponent}
- detailsComponent={this.renderDetails()}
- dataComponent={this.renderData()}
- contentComponent={contentComponent}
- nonce={nonce}
- unapprovedTxCount={unapprovedTxCount}
- assetImage={assetImage}
- identiconAddress={identiconAddress}
- errorMessage={errorMessage || submitError}
- errorKey={propsErrorKey || errorKey}
- warning={warning}
- totalTx={totalTx}
- positionOfCurrentTx={positionOfCurrentTx}
- nextTxId={nextTxId}
- prevTxId={prevTxId}
- showNavigation={showNavigation}
- onNextTx={(txId) => this.handleNextTx(txId)}
- firstTx={firstTx}
- lastTx={lastTx}
- ofText={ofText}
- requestsWaitingText={requestsWaitingText}
- disabled={!propsValid || !valid || submitting}
- onEdit={() => this.handleEdit()}
- onCancelAll={() => this.handleCancelAll()}
- onCancel={() => this.handleCancel()}
- onSubmit={() => this.handleSubmit()}
- />
- )
- }
-}
-
-export function getMethodName (camelCase) {
- if (!camelCase || typeof camelCase !== 'string') {
- return ''
- }
-
- return camelCase
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .replace(/([A-Z])([a-z])/g, ' $1$2')
- .replace(/ +/g, ' ')
-}
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
deleted file mode 100644
index 22f509905..000000000
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router-dom'
-import R from 'ramda'
-import contractMap from 'eth-contract-metadata'
-import ConfirmTransactionBase from './confirm-transaction-base.component'
-import {
- clearConfirmTransaction,
- updateGasAndCalculate,
-} from '../../../ducks/confirm-transaction.duck'
-import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../../actions'
-import {
- INSUFFICIENT_FUNDS_ERROR_KEY,
- GAS_LIMIT_TOO_LOW_ERROR_KEY,
-} from '../../../constants/error-keys'
-import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
-import { isBalanceSufficient, calcGasTotal } from '../../send/send.utils'
-import { conversionGreaterThan } from '../../../conversion-util'
-import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
-import { checksumAddress, addressSlicer, valuesFor } from '../../../util'
-import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../../selectors'
-
-const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
- return {
- ...acc,
- [base.toLowerCase()]: contractMap[base],
- }
-}, {})
-
-const mapStateToProps = (state, props) => {
- const { toAddress: propsToAddress } = props
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
- const { confirmTransaction, metamask, gas } = state
- const {
- ethTransactionAmount,
- ethTransactionFee,
- ethTransactionTotal,
- fiatTransactionAmount,
- fiatTransactionFee,
- fiatTransactionTotal,
- hexTransactionAmount,
- hexTransactionFee,
- hexTransactionTotal,
- tokenData,
- methodData,
- txData,
- tokenProps,
- nonce,
- } = confirmTransaction
- const { txParams = {}, lastGasPrice, id: transactionId } = txData
- const {
- from: fromAddress,
- to: txParamsToAddress,
- gasPrice,
- gas: gasLimit,
- value: amount,
- } = txParams
- const accounts = getMetaMaskAccounts(state)
- const {
- conversionRate,
- identities,
- currentCurrency,
- selectedAddress,
- selectedAddressTxList,
- assetImages,
- network,
- unapprovedTxs,
- metaMetricsSendCount,
- } = metamask
- const assetImage = assetImages[txParamsToAddress]
-
- const {
- customGasLimit,
- customGasPrice,
- } = gas
-
- const { balance } = accounts[selectedAddress]
- const { name: fromName } = identities[selectedAddress]
- const toAddress = propsToAddress || txParamsToAddress
- const toName = identities[toAddress]
- ? identities[toAddress].name
- : (
- casedContractMap[toAddress]
- ? casedContractMap[toAddress].name
- : addressSlicer(checksumAddress(toAddress))
- )
-
- const isTxReprice = Boolean(lastGasPrice)
-
- const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
- const transactionStatus = transaction ? transaction.status : ''
-
- const currentNetworkUnapprovedTxs = R.filter(
- ({ metamaskNetworkId }) => metamaskNetworkId === network,
- unapprovedTxs,
- )
- const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
-
- const insufficientBalance = !isBalanceSufficient({
- amount,
- gasTotal: calcGasTotal(gasLimit, gasPrice),
- balance,
- conversionRate,
- })
-
- return {
- balance,
- fromAddress,
- fromName,
- toAddress,
- toName,
- ethTransactionAmount,
- ethTransactionFee,
- ethTransactionTotal,
- fiatTransactionAmount,
- fiatTransactionFee,
- fiatTransactionTotal,
- hexTransactionAmount,
- hexTransactionFee,
- hexTransactionTotal,
- txData,
- tokenData,
- methodData,
- tokenProps,
- isTxReprice,
- currentCurrency,
- conversionRate,
- transactionStatus,
- nonce,
- assetImage,
- unapprovedTxs,
- unapprovedTxCount,
- currentNetworkUnapprovedTxs,
- customGas: {
- gasLimit: customGasLimit || gasLimit,
- gasPrice: customGasPrice || gasPrice,
- },
- advancedInlineGasShown: getAdvancedInlineGasShown(state),
- insufficientBalance,
- hideSubtitle: (!isMainnet && !showFiatInTestnets),
- hideFiatConversion: (!isMainnet && !showFiatInTestnets),
- metaMetricsSendCount,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
- clearSend: () => dispatch(clearSend()),
- showTransactionConfirmedModal: ({ onSubmit }) => {
- return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
- },
- showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
- return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }))
- },
- 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)),
- setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
- }
-}
-
-const getValidateEditGas = ({ balance, conversionRate, txData }) => {
- const { txParams: { value: amount } = {} } = txData
-
- return ({ gasLimit, gasPrice }) => {
- const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
- const hasSufficientBalance = isBalanceSufficient({
- amount,
- gasTotal,
- balance,
- conversionRate,
- })
-
- if (!hasSufficientBalance) {
- return {
- valid: false,
- errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
- }
- }
-
- const gasLimitTooLow = gasLimit && conversionGreaterThan(
- {
- value: MIN_GAS_LIMIT_DEC,
- fromNumericBase: 'dec',
- conversionRate,
- },
- {
- value: gasLimit,
- fromNumericBase: 'hex',
- },
- )
-
- if (gasLimitTooLow) {
- return {
- valid: false,
- errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
- }
- }
-
- return {
- valid: true,
- }
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { balance, conversionRate, txData, unapprovedTxs } = stateProps
- const {
- cancelAllTransactions: dispatchCancelAllTransactions,
- showCustomizeGasModal: dispatchShowCustomizeGasModal,
- updateGasAndCalculate: dispatchUpdateGasAndCalculate,
- ...otherDispatchProps
- } = dispatchProps
-
- const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
-
- return {
- ...stateProps,
- ...otherDispatchProps,
- ...ownProps,
- showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
- txData,
- onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas),
- validate: validateEditGas,
- }),
- cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
- updateGasAndCalculate: dispatchUpdateGasAndCalculate,
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps, mergeProps)
-)(ConfirmTransactionBase)
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
deleted file mode 100644
index cf79b94bc..000000000
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import { Redirect } from 'react-router-dom'
-import Loading from '../../loading-screen'
-import {
- CONFIRM_TRANSACTION_ROUTE,
- CONFIRM_DEPLOY_CONTRACT_PATH,
- CONFIRM_SEND_ETHER_PATH,
- CONFIRM_SEND_TOKEN_PATH,
- CONFIRM_APPROVE_PATH,
- CONFIRM_TRANSFER_FROM_PATH,
- CONFIRM_TOKEN_METHOD_PATH,
- SIGNATURE_REQUEST_PATH,
-} from '../../../routes'
-import { isConfirmDeployContract } from '../../../helpers/transactions.util'
-import {
- TOKEN_METHOD_TRANSFER,
- TOKEN_METHOD_APPROVE,
- TOKEN_METHOD_TRANSFER_FROM,
-} from '../../../constants/transactions'
-
-export default class ConfirmTransactionSwitch extends Component {
- static propTypes = {
- txData: PropTypes.object,
- methodData: PropTypes.object,
- fetchingData: PropTypes.bool,
- isEtherTransaction: PropTypes.bool,
- }
-
- redirectToTransaction () {
- const {
- txData,
- methodData: { name },
- fetchingData,
- isEtherTransaction,
- } = this.props
- const { id, txParams: { data } = {} } = txData
-
- if (fetchingData) {
- return <Loading />
- }
-
- if (isConfirmDeployContract(txData)) {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- if (isEtherTransaction) {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- if (data) {
- const methodName = name && name.toLowerCase()
-
- switch (methodName) {
- case TOKEN_METHOD_TRANSFER: {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
- return <Redirect to={{ pathname }} />
- }
- case TOKEN_METHOD_APPROVE: {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
- return <Redirect to={{ pathname }} />
- }
- case TOKEN_METHOD_TRANSFER_FROM: {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
- return <Redirect to={{ pathname }} />
- }
- default: {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
- return <Redirect to={{ pathname }} />
- }
- }
- }
-
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- render () {
- const { txData } = this.props
-
- if (txData.txParams) {
- return this.redirectToTransaction()
- } else if (txData.msgParams) {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- return <Loading />
- }
-}
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
deleted file mode 100644
index 2e460f377..000000000
--- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import { Switch, Route } from 'react-router-dom'
-import Loading from '../../loading-screen'
-import ConfirmTransactionSwitch from '../confirm-transaction-switch'
-import ConfirmTransactionBase from '../confirm-transaction-base'
-import ConfirmSendEther from '../confirm-send-ether'
-import ConfirmSendToken from '../confirm-send-token'
-import ConfirmDeployContract from '../confirm-deploy-contract'
-import ConfirmApprove from '../confirm-approve'
-import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
-import ConfTx from '../../../conf-tx'
-import {
- DEFAULT_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- CONFIRM_DEPLOY_CONTRACT_PATH,
- CONFIRM_SEND_ETHER_PATH,
- CONFIRM_SEND_TOKEN_PATH,
- CONFIRM_APPROVE_PATH,
- CONFIRM_TRANSFER_FROM_PATH,
- CONFIRM_TOKEN_METHOD_PATH,
- SIGNATURE_REQUEST_PATH,
-} from '../../../routes'
-
-export default class ConfirmTransaction extends Component {
- static propTypes = {
- history: PropTypes.object.isRequired,
- totalUnapprovedCount: PropTypes.number.isRequired,
- match: PropTypes.object,
- send: PropTypes.object,
- unconfirmedTransactions: PropTypes.array,
- setTransactionToConfirm: PropTypes.func,
- confirmTransaction: PropTypes.object,
- clearConfirmTransaction: PropTypes.func,
- fetchBasicGasAndTimeEstimates: PropTypes.func,
- }
-
- getParamsTransactionId () {
- const { match: { params: { id } = {} } } = this.props
- return id || null
- }
-
- componentDidMount () {
- const {
- totalUnapprovedCount = 0,
- send = {},
- history,
- confirmTransaction: { txData: { id: transactionId } = {} },
- fetchBasicGasAndTimeEstimates,
- } = this.props
-
- if (!totalUnapprovedCount && !send.to) {
- history.replace(DEFAULT_ROUTE)
- return
- }
-
- if (!transactionId) {
- fetchBasicGasAndTimeEstimates()
- this.setTransactionToConfirm()
- }
- }
-
- componentDidUpdate () {
- const {
- setTransactionToConfirm,
- confirmTransaction: { txData: { id: transactionId } = {} },
- clearConfirmTransaction,
- } = this.props
- const paramsTransactionId = this.getParamsTransactionId()
-
- if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
- clearConfirmTransaction()
- setTransactionToConfirm(paramsTransactionId)
- return
- }
-
- if (!transactionId) {
- this.setTransactionToConfirm()
- }
- }
-
- setTransactionToConfirm () {
- const {
- history,
- unconfirmedTransactions,
- setTransactionToConfirm,
- } = this.props
- const paramsTransactionId = this.getParamsTransactionId()
-
- if (paramsTransactionId) {
- // Check to make sure params ID is valid
- const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
-
- if (!tx) {
- history.replace(DEFAULT_ROUTE)
- } else {
- setTransactionToConfirm(paramsTransactionId)
- }
- } else if (unconfirmedTransactions.length) {
- const totalUnconfirmed = unconfirmedTransactions.length
- const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
- const { id: transactionId, loadingDefaults } = transaction
-
- if (!loadingDefaults) {
- setTransactionToConfirm(transactionId)
- }
- }
- }
-
- render () {
- const { confirmTransaction: { txData: { id } } = {} } = this.props
- const paramsTransactionId = this.getParamsTransactionId()
-
- // Show routes when state.confirmTransaction has been set and when either the ID in the params
- // isn't specified or is specified and matches the ID in state.confirmTransaction in order to
- // support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
- return id && (!paramsTransactionId || paramsTransactionId === id + '')
- ? (
- <Switch>
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
- component={ConfirmDeployContract}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
- component={ConfirmTransactionBase}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
- component={ConfirmSendEther}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
- component={ConfirmSendToken}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
- component={ConfirmApprove}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
- component={ConfirmTokenTransactionBase}
- />
- <Route
- exact
- path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
- component={ConfTx}
- />
- <Route path="*" component={ConfirmTransactionSwitch} />
- </Switch>
- )
- : <Loading />
- }
-}
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
deleted file mode 100644
index 46342dc76..000000000
--- a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router-dom'
-import {
- setTransactionToConfirm,
- clearConfirmTransaction,
-} from '../../../ducks/confirm-transaction.duck'
-import {
- fetchBasicGasAndTimeEstimates,
-} from '../../../ducks/gas.duck'
-import ConfirmTransaction from './confirm-transaction.component'
-import { getTotalUnapprovedCount } from '../../../selectors'
-import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
-
-const mapStateToProps = state => {
- const { metamask: { send }, confirmTransaction } = state
-
- return {
- totalUnapprovedCount: getTotalUnapprovedCount(state),
- send,
- confirmTransaction,
- unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
- clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
- fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps),
-)(ConfirmTransaction)
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
deleted file mode 100644
index c63de234a..000000000
--- a/ui/app/components/pages/create-account/connect-hardware/account-list.js
+++ /dev/null
@@ -1,205 +0,0 @@
-const { Component } = require('react')
-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) {
- super(props)
- }
-
- getHdPaths () {
- return [
- {
- label: `Ledger Live`,
- value: `m/44'/60'/0'/0/0`,
- },
- {
- label: `Legacy (MEW / MyCrypto)`,
- value: `m/44'/60'/0'`,
- },
- ]
- }
-
- goToNextPage = () => {
- // If we have < 5 accounts, it's restricted by BIP-44
- if (this.props.accounts.length === 5) {
- this.props.getPage(this.props.device, 1, this.props.selectedPath)
- } else {
- this.props.onAccountRestriction()
- }
- }
-
- goToPreviousPage = () => {
- this.props.getPage(this.props.device, -1, this.props.selectedPath)
- }
-
- renderHdPathSelector () {
- const { onPathChange, selectedPath } = this.props
-
- const options = this.getHdPaths()
- return h('div', [
- h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')),
- h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
- h('div.hw-connect__hdPath', [
- h(Select, {
- className: 'hw-connect__hdPath__select',
- name: 'hd-path-select',
- clearable: false,
- value: selectedPath,
- options,
- onChange: (opt) => {
- onPathChange(opt.value)
- },
- }),
- ]),
- ])
- }
-
- capitalizeDevice (device) {
- return device.slice(0, 1).toUpperCase() + device.slice(1)
- }
-
- renderHeader () {
- const { device } = this.props
- return (
- h('div.hw-connect', [
-
- h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`),
-
- device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
-
- h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')),
- h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
- ])
- )
- }
-
- renderAccounts () {
- return h('div.hw-account-list', [
- this.props.accounts.map((a, i) => {
-
- return h('div.hw-account-list__item', { key: a.address }, [
- h('div.hw-account-list__item__radio', [
- h('input', {
- type: 'radio',
- name: 'selectedAccount',
- id: `address-${i}`,
- value: a.index,
- onChange: (e) => this.props.onAccountChange(e.target.value),
- checked: this.props.selectedAccount === a.index.toString(),
- }),
- h(
- 'label.hw-account-list__item__label',
- {
- htmlFor: `address-${i}`,
- },
- [
- h('span.hw-account-list__item__index', a.index + 1),
- `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
- h('span.hw-account-list__item__balance', `${a.balance}`),
- ]),
- ]),
- h(
- 'a.hw-account-list__item__link',
- {
- href: genAccountLink(a.address, this.props.network),
- target: '_blank',
- title: this.context.t('etherscanView'),
- },
- h('img', { src: 'images/popout.svg' })
- ),
- ])
- }),
- ])
- }
-
- renderPagination () {
- return h('div.hw-list-pagination', [
- h(
- 'button.hw-list-pagination__button',
- {
- onClick: this.goToPreviousPage,
- },
- `< ${this.context.t('prev')}`
- ),
-
- h(
- 'button.hw-list-pagination__button',
- {
- onClick: this.goToNextPage,
- },
- `${this.context.t('next')} >`
- ),
- ])
- }
-
- renderButtons () {
- const disabled = this.props.selectedAccount === null
- const buttonProps = {}
- if (disabled) {
- buttonProps.disabled = true
- }
-
- return h('div.new-account-connect-form__buttons', {}, [
- 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: 'confirm',
- large: true,
- className: 'new-account-connect-form__button unlock',
- disabled,
- onClick: this.props.onUnlockAccount.bind(this, this.props.device),
- }, [this.context.t('unlock')]),
- ])
- }
-
- renderForgetDevice () {
- return h('div.hw-forget-device-container', {}, [
- h('a', {
- onClick: this.props.onForgetDevice.bind(this, this.props.device),
- }, this.context.t('forgetDevice')),
- ])
- }
-
- render () {
- return h('div.new-account-connect-form.account-list', {}, [
- this.renderHeader(),
- this.renderAccounts(),
- this.renderPagination(),
- this.renderButtons(),
- this.renderForgetDevice(),
- ])
- }
-
-}
-
-
-AccountList.propTypes = {
- onPathChange: PropTypes.func.isRequired,
- selectedPath: PropTypes.string.isRequired,
- device: PropTypes.string.isRequired,
- accounts: PropTypes.array.isRequired,
- onAccountChange: PropTypes.func.isRequired,
- onForgetDevice: PropTypes.func.isRequired,
- getPage: PropTypes.func.isRequired,
- network: PropTypes.string,
- selectedAccount: PropTypes.string,
- history: PropTypes.object,
- onUnlockAccount: PropTypes.func,
- onCancel: PropTypes.func,
- onAccountRestriction: PropTypes.func,
-}
-
-AccountList.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = AccountList
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
deleted file mode 100644
index 49a5610c1..000000000
--- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
+++ /dev/null
@@ -1,197 +0,0 @@
-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) {
- super(props)
- this.state = {
- selectedDevice: null,
- }
- }
-
- connect = () => {
- if (this.state.selectedDevice) {
- this.props.connectToHardwareWallet(this.state.selectedDevice)
- }
- return null
- }
-
- renderConnectToTrezorButton () {
- return h(
- `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`,
- { onClick: _ => this.setState({selectedDevice: 'trezor'}) },
- h('img.hw-connect__btn__img', {
- src: 'images/trezor-logo.svg',
- })
- )
- }
-
- renderConnectToLedgerButton () {
- return h(
- `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`,
- { onClick: _ => this.setState({selectedDevice: 'ledger'}) },
- h('img.hw-connect__btn__img', {
- src: 'images/ledger-logo.svg',
- })
- )
- }
-
- renderButtons () {
- return (
- h('div', {}, [
- h('div.hw-connect__btn-wrapper', {}, [
- this.renderConnectToLedgerButton(),
- this.renderConnectToTrezorButton(),
- ]),
- h(Button, {
- type: 'confirm',
- large: true,
- className: 'hw-connect__connect-btn',
- onClick: this.connect,
- disabled: !this.state.selectedDevice,
- }, this.context.t('connect')),
- ])
- )
- }
-
- renderUnsupportedBrowser () {
- return (
- h('div.new-account-connect-form.unsupported-browser', {}, [
- h('div.hw-connect', [
- h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
- h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
- ]),
- h(Button, {
- type: 'primary',
- large: true,
- onClick: () => global.platform.openWindow({
- url: 'https://google.com/chrome',
- }),
- }, this.context.t('downloadGoogleChrome')),
- ])
- )
- }
-
- renderHeader () {
- return (
- h('div.hw-connect__header', {}, [
- h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
- h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
- ])
- )
- }
-
- getAffiliateLinks () {
- const links = {
- trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
- ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
- }
-
- const text = this.context.t('orderOneHere')
- const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger)
-
- return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }})
- }
-
- renderTrezorAffiliateLink () {
- return h('div.hw-connect__get-hw', {}, [
- h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)),
- this.getAffiliateLinks(),
- ])
- }
-
-
- scrollToTutorial = (e) => {
- if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
- }
-
- renderLearnMore () {
- return (
- h('p.hw-connect__learn-more', {
- onClick: this.scrollToTutorial,
- }, [
- this.context.t('learnMore'),
- h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
- ])
- )
- }
-
- renderTutorialSteps () {
- const steps = [
- {
- asset: 'hardware-wallet-step-1',
- dimensions: {width: '225px', height: '75px'},
- },
- {
- asset: 'hardware-wallet-step-2',
- dimensions: {width: '300px', height: '100px'},
- },
- {
- asset: 'hardware-wallet-step-3',
- dimensions: {width: '120px', height: '90px'},
- },
- ]
-
- return h('.hw-tutorial', {
- ref: node => { this.referenceNode = node },
- },
- steps.map((step, i) => (
- h('div.hw-connect', {}, [
- h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
- h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
- h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
- ])
- ))
- )
- }
-
- renderFooter () {
- return (
- h('div.hw-connect__footer', {}, [
- h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
- this.renderButtons(),
- h('p.hw-connect__footer__msg', {}, [
- this.context.t(`havingTroubleConnecting`),
- h('a.hw-connect__footer__link', {
- href: 'https://support.metamask.io/',
- target: '_blank',
- }, this.context.t('getHelp')),
- ]),
- ])
- )
- }
-
- renderConnectScreen () {
- return (
- h('div.new-account-connect-form', {}, [
- this.renderHeader(),
- this.renderButtons(),
- this.renderTrezorAffiliateLink(),
- this.renderLearnMore(),
- this.renderTutorialSteps(),
- this.renderFooter(),
- ])
- )
- }
-
- render () {
- if (this.props.browserSupported) {
- return this.renderConnectScreen()
- }
- return this.renderUnsupportedBrowser()
- }
-}
-
-ConnectScreen.propTypes = {
- connectToHardwareWallet: PropTypes.func.isRequired,
- browserSupported: PropTypes.bool.isRequired,
-}
-
-ConnectScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = ConnectScreen
-
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
deleted file mode 100644
index 712cc5cbb..000000000
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ /dev/null
@@ -1,293 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('../../../../actions')
-const { getMetaMaskAccounts } = require('../../../../selectors')
-const ConnectScreen = require('./connect-screen')
-const AccountList = require('./account-list')
-const { DEFAULT_ROUTE } = require('../../../../routes')
-const { formatBalance } = require('../../../../util')
-const { getPlatform } = require('../../../../../../app/scripts/lib/util')
-const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums')
-
-class ConnectHardwareForm extends Component {
- constructor (props, context) {
- super(props)
- this.state = {
- error: null,
- selectedAccount: null,
- accounts: [],
- browserSupported: true,
- unlocked: false,
- device: null,
- }
- }
-
- componentWillReceiveProps (nextProps) {
- const { accounts } = nextProps
- const newAccounts = this.state.accounts.map(a => {
- const normalizedAddress = a.address.toLowerCase()
- const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
- a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
- return a
- })
- this.setState({accounts: newAccounts})
- }
-
-
- componentDidMount () {
- this.checkIfUnlocked()
- }
-
- async checkIfUnlocked () {
- ['trezor', 'ledger'].forEach(async device => {
- const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
- if (unlocked) {
- this.setState({unlocked: true})
- this.getPage(device, 0, this.props.defaultHdPaths[device])
- }
- })
- }
-
- connectToHardwareWallet = (device) => {
- // Ledger hardware wallets are not supported on firefox
- if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') {
- this.setState({ browserSupported: false, error: null})
- return null
- }
-
- if (this.state.accounts.length) {
- return null
- }
-
- // Default values
- this.getPage(device, 0, this.props.defaultHdPaths[device])
- }
-
- onPathChange = (path) => {
- this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
- this.getPage(this.state.device, 0, path)
- }
-
- onAccountChange = (account) => {
- this.setState({selectedAccount: account.toString(), error: null})
- }
-
- onAccountRestriction = () => {
- this.setState({error: this.context.t('ledgerAccountRestriction') })
- }
-
- showTemporaryAlert () {
- this.props.showAlert(this.context.t('hardwareWalletConnected'))
- // Autohide the alert after 5 seconds
- setTimeout(_ => {
- this.props.hideAlert()
- }, 5000)
- }
-
- getPage = (device, page, hdPath) => {
- this.props
- .connectHardware(device, page, hdPath)
- .then(accounts => {
- if (accounts.length) {
-
- // If we just loaded the accounts for the first time
- // (device previously locked) show the global alert
- if (this.state.accounts.length === 0 && !this.state.unlocked) {
- this.showTemporaryAlert()
- }
-
- const newState = { unlocked: true, device, error: null }
- // Default to the first account
- if (this.state.selectedAccount === null) {
- accounts.forEach((a, i) => {
- if (a.address.toLowerCase() === this.props.address) {
- newState.selectedAccount = a.index.toString()
- }
- })
- // If the page doesn't contain the selected account, let's deselect it
- } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
- newState.selectedAccount = null
- }
-
-
- // Map accounts with balances
- newState.accounts = accounts.map(account => {
- const normalizedAddress = account.address.toLowerCase()
- const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
- account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
- return account
- })
-
- this.setState(newState)
- }
- })
- .catch(e => {
- if (e === 'Window blocked') {
- this.setState({ browserSupported: false, error: null})
- } else if (e !== 'Window closed' && e !== 'Popup closed') {
- this.setState({ error: e.toString() })
- }
- })
- }
-
- onForgetDevice = (device) => {
- this.props.forgetDevice(device)
- .then(_ => {
- this.setState({
- error: null,
- selectedAccount: null,
- accounts: [],
- unlocked: false,
- })
- }).catch(e => {
- this.setState({ error: e.toString() })
- })
- }
-
- onUnlockAccount = (device) => {
-
- if (this.state.selectedAccount === null) {
- this.setState({ error: this.context.t('accountSelectionRequired') })
- }
-
- this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
- .then(_ => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Connected Hardware Wallet',
- name: 'Connected Account with: ' + device,
- },
- })
- this.props.history.push(DEFAULT_ROUTE)
- }).catch(e => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Connected Hardware Wallet',
- name: 'Error connecting hardware wallet',
- },
- customVariables: {
- error: e.toString(),
- },
- })
- this.setState({ error: e.toString() })
- })
- }
-
- onCancel = () => {
- this.props.history.push(DEFAULT_ROUTE)
- }
-
- renderError () {
- return this.state.error
- ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
- : null
- }
-
- renderContent () {
- if (!this.state.accounts.length) {
- return h(ConnectScreen, {
- connectToHardwareWallet: this.connectToHardwareWallet,
- browserSupported: this.state.browserSupported,
- })
- }
-
- return h(AccountList, {
- onPathChange: this.onPathChange,
- selectedPath: this.props.defaultHdPaths[this.state.device],
- device: this.state.device,
- accounts: this.state.accounts,
- selectedAccount: this.state.selectedAccount,
- onAccountChange: this.onAccountChange,
- network: this.props.network,
- getPage: this.getPage,
- history: this.props.history,
- onUnlockAccount: this.onUnlockAccount,
- onForgetDevice: this.onForgetDevice,
- onCancel: this.onCancel,
- onAccountRestriction: this.onAccountRestriction,
- })
- }
-
- render () {
- return h('div', [
- this.renderError(),
- this.renderContent(),
- ])
- }
-}
-
-ConnectHardwareForm.propTypes = {
- hideModal: PropTypes.func,
- showImportPage: PropTypes.func,
- showConnectPage: PropTypes.func,
- connectHardware: PropTypes.func,
- checkHardwareStatus: PropTypes.func,
- forgetDevice: PropTypes.func,
- showAlert: PropTypes.func,
- hideAlert: PropTypes.func,
- unlockHardwareWalletAccount: PropTypes.func,
- setHardwareWalletDefaultHdPath: PropTypes.func,
- numberOfExistingAccounts: PropTypes.number,
- history: PropTypes.object,
- t: PropTypes.func,
- network: PropTypes.string,
- accounts: PropTypes.object,
- address: PropTypes.string,
- defaultHdPaths: PropTypes.object,
-}
-
-const mapStateToProps = state => {
- const {
- metamask: { network, selectedAddress, identities = {} },
- } = state
- const accounts = getMetaMaskAccounts(state)
- const numberOfExistingAccounts = Object.keys(identities).length
- const {
- appState: { defaultHdPaths },
- } = state
-
- return {
- network,
- accounts,
- address: selectedAddress,
- numberOfExistingAccounts,
- defaultHdPaths,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setHardwareWalletDefaultHdPath: ({device, path}) => {
- return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
- },
- connectHardware: (deviceName, page, hdPath) => {
- return dispatch(actions.connectHardware(deviceName, page, hdPath))
- },
- checkHardwareStatus: (deviceName, hdPath) => {
- return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
- },
- forgetDevice: (deviceName) => {
- return dispatch(actions.forgetDevice(deviceName))
- },
- unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
- return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
- },
- showImportPage: () => dispatch(actions.showImportPage()),
- showConnectPage: () => dispatch(actions.showConnectPage()),
- showAlert: (msg) => dispatch(actions.showAlert(msg)),
- hideAlert: () => dispatch(actions.hideAlert()),
- }
-}
-
-ConnectHardwareForm.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(
- ConnectHardwareForm
-)
diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
deleted file mode 100644
index 9aeea5579..000000000
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ /dev/null
@@ -1,170 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const connect = require('react-redux').connect
-const actions = require('../../../../actions')
-const FileInput = require('react-simple-file-input').default
-const { DEFAULT_ROUTE } = require('../../../../routes')
-const { getMetaMaskAccounts } = require('../../../../selectors')
-const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
-import Button from '../../../button'
-
-class JsonImportSubview extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- file: null,
- fileContents: '',
- }
- }
-
- render () {
- const { error } = this.props
-
- return (
- h('div.new-account-import-form__json', [
-
- h('p', this.context.t('usedByClients')),
- h('a.warning', {
- href: HELP_LINK,
- target: '_blank',
- }, this.context.t('fileImportFail')),
-
- h(FileInput, {
- readAs: 'text',
- onLoad: this.onLoad.bind(this),
- style: {
- margin: '20px 0px 12px 34%',
- fontSize: '15px',
- display: 'flex',
- justifyContent: 'center',
- },
- }),
-
- h('input.new-account-import-form__input-password', {
- type: 'password',
- placeholder: this.context.t('enterPassword'),
- id: 'json-password-box',
- onKeyPress: this.createKeyringOnEnter.bind(this),
- }),
-
- h('div.new-account-create-form__buttons', {}, [
-
- h(Button, {
- type: 'default',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, [this.context.t('cancel')]),
-
- h(Button, {
- type: 'primary',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => this.createNewKeychain(),
- }, [this.context.t('import')]),
-
- ]),
-
- error ? h('span.error', error) : null,
- ])
- )
- }
-
- onLoad (event, file) {
- this.setState({file: file, fileContents: event.target.result})
- }
-
- createKeyringOnEnter (event) {
- if (event.key === 'Enter') {
- event.preventDefault()
- this.createNewKeychain()
- }
- }
-
- createNewKeychain () {
- const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress, history } = this.props
- const state = this.state
-
- if (!state) {
- const message = this.context.t('validFileImport')
- return displayWarning(message)
- }
-
- const { fileContents } = state
-
- if (!fileContents) {
- const message = this.context.t('needImportFile')
- return displayWarning(message)
- }
-
- const passwordInput = document.getElementById('json-password-box')
- const password = passwordInput.value
-
- importNewJsonAccount([ fileContents, password ])
- .then(({ selectedAddress }) => {
- if (selectedAddress) {
- history.push(DEFAULT_ROUTE)
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Import Account',
- name: 'Imported Account with JSON',
- },
- })
- displayWarning(null)
- } else {
- displayWarning('Error importing account.')
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Import Account',
- name: 'Error importing JSON',
- },
- })
- setSelectedAddress(firstAddress)
- }
- })
- .catch(err => err && displayWarning(err.message || err))
- }
-}
-
-JsonImportSubview.propTypes = {
- error: PropTypes.string,
- goHome: PropTypes.func,
- displayWarning: PropTypes.func,
- firstAddress: PropTypes.string,
- importNewJsonAccount: PropTypes.func,
- history: PropTypes.object,
- setSelectedAddress: PropTypes.func,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- return {
- error: state.appState.warning,
- firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- goHome: () => dispatch(actions.goHome()),
- displayWarning: warning => dispatch(actions.displayWarning(warning)),
- importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)),
- setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
- }
-}
-
-JsonImportSubview.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(JsonImportSubview)
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
deleted file mode 100644
index 4ba31806f..000000000
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ /dev/null
@@ -1,128 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const actions = require('../../../../actions')
-const { DEFAULT_ROUTE } = require('../../../../routes')
-const { getMetaMaskAccounts } = require('../../../../selectors')
-import Button from '../../../button'
-
-PrivateKeyImportView.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(PrivateKeyImportView)
-
-
-function mapStateToProps (state) {
- return {
- error: state.appState.warning,
- firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- importNewAccount: (strategy, [ privateKey ]) => {
- return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
- },
- displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
- setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
- }
-}
-
-inherits(PrivateKeyImportView, Component)
-function PrivateKeyImportView () {
- this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
- Component.call(this)
-}
-
-PrivateKeyImportView.prototype.render = function () {
- const { error, displayWarning } = this.props
-
- return (
- h('div.new-account-import-form__private-key', [
-
- h('span.new-account-create-form__instruction', this.context.t('pastePrivateKey')),
-
- h('div.new-account-import-form__private-key-password-container', [
-
- h('input.new-account-import-form__input-password', {
- type: 'password',
- id: 'private-key-box',
- onKeyPress: e => this.createKeyringOnEnter(e),
- }),
-
- ]),
-
- h('div.new-account-import-form__buttons', {}, [
-
- 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')]),
-
- h(Button, {
- type: 'primary',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => this.createNewKeychain(),
- }, [this.context.t('import')]),
-
- ]),
-
- error ? h('span.error', error) : null,
- ])
- )
-}
-
-PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
- if (event.key === 'Enter') {
- event.preventDefault()
- this.createNewKeychain()
- }
-}
-
-PrivateKeyImportView.prototype.createNewKeychain = function () {
- const input = document.getElementById('private-key-box')
- const privateKey = input.value
- const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props
-
- importNewAccount('Private Key', [ privateKey ])
- .then(({ selectedAddress }) => {
- if (selectedAddress) {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Import Account',
- name: 'Imported Account with Private Key',
- },
- })
- history.push(DEFAULT_ROUTE)
- displayWarning(null)
- } else {
- displayWarning('Error importing account.')
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Import Account',
- name: 'Error importing with Private Key',
- },
- })
- setSelectedAddress(firstAddress)
- }
- })
- .catch(err => err && displayWarning(err.message || err))
-}
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
deleted file mode 100644
index d3de1ea01..000000000
--- a/ui/app/components/pages/create-account/index.js
+++ /dev/null
@@ -1,113 +0,0 @@
-const Component = require('react').Component
-const { Switch, Route, matchPath } = require('react-router-dom')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('../../../actions')
-const { getCurrentViewContext } = require('../../../selectors')
-const classnames = require('classnames')
-const NewAccountCreateForm = require('./new-account')
-const NewAccountImportForm = require('./import-account')
-const ConnectHardwareForm = require('./connect-hardware')
-const {
- NEW_ACCOUNT_ROUTE,
- IMPORT_ACCOUNT_ROUTE,
- CONNECT_HARDWARE_ROUTE,
-} = require('../../../routes')
-
-class CreateAccountPage extends Component {
- renderTabs () {
- const { history, location } = this.props
-
- return h('div.new-account__tabs', [
- h('div.new-account__tabs__tab', {
- className: classnames('new-account__tabs__tab', {
- 'new-account__tabs__selected': matchPath(location.pathname, {
- path: NEW_ACCOUNT_ROUTE, exact: true,
- }),
- }),
- onClick: () => history.push(NEW_ACCOUNT_ROUTE),
- }, [
- this.context.t('create'),
- ]),
-
- h('div.new-account__tabs__tab', {
- className: classnames('new-account__tabs__tab', {
- 'new-account__tabs__selected': matchPath(location.pathname, {
- path: IMPORT_ACCOUNT_ROUTE, exact: true,
- }),
- }),
- onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
- }, [
- this.context.t('import'),
- ]),
- h(
- 'div.new-account__tabs__tab',
- {
- className: classnames('new-account__tabs__tab', {
- 'new-account__tabs__selected': matchPath(location.pathname, {
- path: CONNECT_HARDWARE_ROUTE,
- exact: true,
- }),
- }),
- onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
- },
- this.context.t('connect')
- ),
- ])
- }
-
- render () {
- return h('div.new-account', {}, [
- h('div.new-account__header', [
- h('div.new-account__title', this.context.t('newAccount')),
- this.renderTabs(),
- ]),
- h('div.new-account__form', [
- h(Switch, [
- h(Route, {
- exact: true,
- path: NEW_ACCOUNT_ROUTE,
- component: NewAccountCreateForm,
- }),
- h(Route, {
- exact: true,
- path: IMPORT_ACCOUNT_ROUTE,
- component: NewAccountImportForm,
- }),
- h(Route, {
- exact: true,
- path: CONNECT_HARDWARE_ROUTE,
- component: ConnectHardwareForm,
- }),
- ]),
- ]),
- ])
- }
-}
-
-CreateAccountPage.propTypes = {
- location: PropTypes.object,
- history: PropTypes.object,
- t: PropTypes.func,
-}
-
-CreateAccountPage.contextTypes = {
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => ({
- displayedForm: getCurrentViewContext(state),
-})
-
-const mapDispatchToProps = dispatch => ({
- displayForm: form => dispatch(actions.setNewAccountForm(form)),
- showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
- showExportPrivateKeyModal: () => {
- dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
- },
- hideModal: () => dispatch(actions.hideModal()),
- setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
-})
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
deleted file mode 100644
index a7595e346..000000000
--- a/ui/app/components/pages/create-account/new-account.js
+++ /dev/null
@@ -1,130 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-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) {
- super(props)
-
- const { numberOfExistingAccounts = 0 } = props
- const newAccountNumber = numberOfExistingAccounts + 1
-
- this.state = {
- newAccountName: '',
- defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]),
- }
- }
-
- render () {
- const { newAccountName, defaultAccountName } = this.state
- const { history, createAccount } = this.props
-
- return h('div.new-account-create-form', [
-
- h('div.new-account-create-form__input-label', {}, [
- this.context.t('accountName'),
- ]),
-
- h('div.new-account-create-form__input-wrapper', {}, [
- h('input.new-account-create-form__input', {
- value: newAccountName,
- placeholder: defaultAccountName,
- onChange: event => this.setState({ newAccountName: event.target.value }),
- }, []),
- ]),
-
- h('div.new-account-create-form__buttons', {}, [
-
- h(Button, {
- type: 'default',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => history.push(DEFAULT_ROUTE),
- }, [this.context.t('cancel')]),
-
- h(Button, {
- type: 'primary',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => {
- createAccount(newAccountName || defaultAccountName)
- .then(() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Add New Account',
- name: 'Added New Account',
- },
- })
- history.push(DEFAULT_ROUTE)
- })
- .catch((e) => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Accounts',
- action: 'Add New Account',
- name: 'Error',
- },
- customVariables: {
- errorMessage: e.message,
- },
- })
- })
- },
- }, [this.context.t('create')]),
-
- ]),
-
- ])
- }
-}
-
-NewAccountCreateForm.propTypes = {
- hideModal: PropTypes.func,
- showImportPage: PropTypes.func,
- showConnectPage: PropTypes.func,
- createAccount: PropTypes.func,
- numberOfExistingAccounts: PropTypes.number,
- history: PropTypes.object,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const { metamask: { network, selectedAddress, identities = {} } } = state
- const numberOfExistingAccounts = Object.keys(identities).length
-
- return {
- network,
- address: selectedAddress,
- numberOfExistingAccounts,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
- hideModal: () => dispatch(actions.hideModal()),
- createAccount: newAccountName => {
- return dispatch(actions.addNewAccount())
- .then(newAccountAddress => {
- if (newAccountName) {
- dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
- }
- })
- },
- showImportPage: () => dispatch(actions.showImportPage()),
- showConnectPage: () => dispatch(actions.showConnectPage()),
- }
-}
-
-NewAccountCreateForm.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)
-
diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js
deleted file mode 100644
index 070361652..000000000
--- a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { Switch, Route } from 'react-router-dom'
-import NewAccount from './new-account'
-import ImportWithSeedPhrase from './import-with-seed-phrase'
-import {
- INITIALIZE_CREATE_PASSWORD_ROUTE,
- INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
- INITIALIZE_SEED_PHRASE_ROUTE,
-} from '../../../../routes'
-
-export default class CreatePassword extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- isInitialized: PropTypes.bool,
- onCreateNewAccount: PropTypes.func,
- onCreateNewAccountFromSeed: PropTypes.func,
- }
-
- componentDidMount () {
- const { isInitialized, history } = this.props
-
- if (isInitialized) {
- history.push(INITIALIZE_SEED_PHRASE_ROUTE)
- }
- }
-
- render () {
- const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
-
- return (
- <div className="first-time-flow__wrapper">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <Switch>
- <Route
- exact
- path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
- render={props => (
- <ImportWithSeedPhrase
- { ...props }
- onSubmit={onCreateNewAccountFromSeed}
- />
- )}
- />
- <Route
- exact
- path={INITIALIZE_CREATE_PASSWORD_ROUTE}
- render={props => (
- <NewAccount
- { ...props }
- onSubmit={onCreateNewAccount}
- />
- )}
- />
- </Switch>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
deleted file mode 100644
index 4ecaa5895..000000000
--- a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
+++ /dev/null
@@ -1,256 +0,0 @@
-import {validateMnemonic} from 'bip39'
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import TextField from '../../../../text-field'
-import Button from '../../../../button'
-import {
- INITIALIZE_SELECT_ACTION_ROUTE,
- INITIALIZE_END_OF_FLOW_ROUTE,
-} from '../../../../../routes'
-
-export default class ImportWithSeedPhrase extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- }
-
- state = {
- seedPhrase: '',
- password: '',
- confirmPassword: '',
- seedPhraseError: '',
- passwordError: '',
- confirmPasswordError: '',
- termsChecked: false,
- }
-
- parseSeedPhrase = (seedPhrase) => {
- return seedPhrase
- .trim()
- .match(/\w+/g)
- .join(' ')
- }
-
- handleSeedPhraseChange (seedPhrase) {
- let seedPhraseError = ''
-
- if (seedPhrase) {
- const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase)
- if (parsedSeedPhrase.split(' ').length !== 12) {
- seedPhraseError = this.context.t('seedPhraseReq')
- } else if (!validateMnemonic(parsedSeedPhrase)) {
- seedPhraseError = this.context.t('invalidSeedPhrase')
- }
- }
-
- this.setState({ seedPhrase, seedPhraseError })
- }
-
- handlePasswordChange (password) {
- const { t } = this.context
-
- this.setState(state => {
- const { confirmPassword } = state
- let confirmPasswordError = ''
- let passwordError = ''
-
- if (password && password.length < 8) {
- passwordError = t('passwordNotLongEnough')
- }
-
- if (confirmPassword && password !== confirmPassword) {
- confirmPasswordError = t('passwordsDontMatch')
- }
-
- return {
- password,
- passwordError,
- confirmPasswordError,
- }
- })
- }
-
- handleConfirmPasswordChange (confirmPassword) {
- const { t } = this.context
-
- this.setState(state => {
- const { password } = state
- let confirmPasswordError = ''
-
- if (password !== confirmPassword) {
- confirmPasswordError = t('passwordsDontMatch')
- }
-
- return {
- confirmPassword,
- confirmPasswordError,
- }
- })
- }
-
- handleImport = async event => {
- event.preventDefault()
-
- if (!this.isValid()) {
- return
- }
-
- const { password, seedPhrase } = this.state
- const { history, onSubmit } = this.props
-
- try {
- await onSubmit(password, this.parseSeedPhrase(seedPhrase))
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Import Seed Phrase',
- name: 'Import Complete',
- },
- })
- history.push(INITIALIZE_END_OF_FLOW_ROUTE)
- } catch (error) {
- this.setState({ seedPhraseError: error.message })
- }
- }
-
- isValid () {
- const {
- seedPhrase,
- password,
- confirmPassword,
- passwordError,
- confirmPasswordError,
- seedPhraseError,
- } = this.state
-
- if (!password || !confirmPassword || !seedPhrase || password !== confirmPassword) {
- return false
- }
-
- if (password.length < 8) {
- return false
- }
-
- return !passwordError && !confirmPasswordError && !seedPhraseError
- }
-
- toggleTermsCheck = () => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Import Seed Phrase',
- name: 'Check ToS',
- },
- })
-
- this.setState((prevState) => ({
- termsChecked: !prevState.termsChecked,
- }))
- }
-
- render () {
- const { t } = this.context
- const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state
-
- return (
- <form
- className="first-time-flow__form"
- onSubmit={this.handleImport}
- >
- <div className="first-time-flow__create-back">
- <a
- onClick={e => {
- e.preventDefault()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Import Seed Phrase',
- name: 'Go Back from Onboarding Import',
- },
- })
- this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
- }}
- href="#"
- >
- {`< Back`}
- </a>
- </div>
- <div className="first-time-flow__header">
- { t('importAccountSeedPhrase') }
- </div>
- <div className="first-time-flow__text-block">
- { t('secretPhrase') }
- </div>
- <div className="first-time-flow__textarea-wrapper">
- <label>{ t('walletSeed') }</label>
- <textarea
- className="first-time-flow__textarea"
- onChange={e => this.handleSeedPhraseChange(e.target.value)}
- value={this.state.seedPhrase}
- placeholder={t('seedPhrasePlaceholder')}
- />
- </div>
- {
- seedPhraseError && (
- <span className="error">
- { seedPhraseError }
- </span>
- )
- }
- <TextField
- id="password"
- label={t('newPassword')}
- type="password"
- className="first-time-flow__input"
- value={this.state.password}
- onChange={event => this.handlePasswordChange(event.target.value)}
- error={passwordError}
- autoComplete="new-password"
- margin="normal"
- largeLabel
- />
- <TextField
- id="confirm-password"
- label={t('confirmPassword')}
- type="password"
- className="first-time-flow__input"
- value={this.state.confirmPassword}
- onChange={event => this.handleConfirmPasswordChange(event.target.value)}
- error={confirmPasswordError}
- autoComplete="confirm-password"
- margin="normal"
- largeLabel
- />
- <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
- <div className="first-time-flow__checkbox">
- {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
- </div>
- <span className="first-time-flow__checkbox-label">
- I have read and agree to the <a
- href="https://metamask.io/terms.html"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span className="first-time-flow__link-text">
- { 'Terms of Use' }
- </span>
- </a>
- </span>
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- disabled={!this.isValid() || !termsChecked}
- onClick={this.handleImport}
- >
- { t('import') }
- </Button>
- </form>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
deleted file mode 100644
index 11d10e2d9..000000000
--- a/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../../../button'
-import {
- INITIALIZE_SEED_PHRASE_ROUTE,
- INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
- INITIALIZE_SELECT_ACTION_ROUTE,
-} from '../../../../../routes'
-import TextField from '../../../../text-field'
-
-export default class NewAccount extends PureComponent {
- static contextTypes = {
- metricsEvent: PropTypes.func,
- t: PropTypes.func,
- }
-
- static propTypes = {
- onSubmit: PropTypes.func.isRequired,
- history: PropTypes.object.isRequired,
- }
-
- state = {
- password: '',
- confirmPassword: '',
- passwordError: '',
- confirmPasswordError: '',
- termsChecked: false,
- }
-
- isValid () {
- const {
- password,
- confirmPassword,
- passwordError,
- confirmPasswordError,
- } = this.state
-
- if (!password || !confirmPassword || password !== confirmPassword) {
- return false
- }
-
- if (password.length < 8) {
- return false
- }
-
- return !passwordError && !confirmPasswordError
- }
-
- handlePasswordChange (password) {
- const { t } = this.context
-
- this.setState(state => {
- const { confirmPassword } = state
- let passwordError = ''
- let confirmPasswordError = ''
-
- if (password && password.length < 8) {
- passwordError = t('passwordNotLongEnough')
- }
-
- if (confirmPassword && password !== confirmPassword) {
- confirmPasswordError = t('passwordsDontMatch')
- }
-
- return {
- password,
- passwordError,
- confirmPasswordError,
- }
- })
- }
-
- handleConfirmPasswordChange (confirmPassword) {
- const { t } = this.context
-
- this.setState(state => {
- const { password } = state
- let confirmPasswordError = ''
-
- if (password !== confirmPassword) {
- confirmPasswordError = t('passwordsDontMatch')
- }
-
- return {
- confirmPassword,
- confirmPasswordError,
- }
- })
- }
-
- handleCreate = async event => {
- event.preventDefault()
-
- if (!this.isValid()) {
- return
- }
-
- const { password } = this.state
- const { onSubmit, history } = this.props
-
- try {
- await onSubmit(password)
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Create Password',
- name: 'Submit Password',
- },
- })
-
- history.push(INITIALIZE_SEED_PHRASE_ROUTE)
- } catch (error) {
- this.setState({ passwordError: error.message })
- }
- }
-
- handleImportWithSeedPhrase = event => {
- const { history } = this.props
-
- event.preventDefault()
- history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
- }
-
- toggleTermsCheck = () => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Create Password',
- name: 'Check ToS',
- },
- })
-
- this.setState((prevState) => ({
- termsChecked: !prevState.termsChecked,
- }))
- }
-
- render () {
- const { t } = this.context
- const { password, confirmPassword, passwordError, confirmPasswordError, termsChecked } = this.state
-
- return (
- <div>
- <div className="first-time-flow__create-back">
- <a
- onClick={e => {
- e.preventDefault()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Create Password',
- name: 'Go Back from Onboarding Create',
- },
- })
- this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
- }}
- href="#"
- >
- {`< Back`}
- </a>
- </div>
- <div className="first-time-flow__header">
- { t('createPassword') }
- </div>
- <form
- className="first-time-flow__form"
- onSubmit={this.handleCreate}
- >
- <TextField
- id="create-password"
- label={t('newPassword')}
- type="password"
- className="first-time-flow__input"
- value={password}
- onChange={event => this.handlePasswordChange(event.target.value)}
- error={passwordError}
- autoFocus
- autoComplete="new-password"
- margin="normal"
- fullWidth
- largeLabel
- />
- <TextField
- id="confirm-password"
- label={t('confirmPassword')}
- type="password"
- className="first-time-flow__input"
- value={confirmPassword}
- onChange={event => this.handleConfirmPasswordChange(event.target.value)}
- error={confirmPasswordError}
- autoComplete="confirm-password"
- margin="normal"
- fullWidth
- largeLabel
- />
- <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
- <div className="first-time-flow__checkbox">
- {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
- </div>
- <span className="first-time-flow__checkbox-label">
- I have read and agree to the <a
- href="https://metamask.io/terms.html"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span className="first-time-flow__link-text">
- { 'Terms of Use' }
- </span>
- </a>
- </span>
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- disabled={!this.isValid() || !termsChecked}
- onClick={this.handleCreate}
- >
- { t('create') }
- </Button>
- </form>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
deleted file mode 100644
index cbc85c0e4..000000000
--- a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../../../button'
-import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../../routes'
-
-export default class UniqueImageScreen extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- }
-
- render () {
- const { t } = this.context
- const { history } = this.props
-
- return (
- <div>
- <img
- src="/images/sleuth.svg"
- height={42}
- width={42}
- />
- <div className="first-time-flow__header">
- { t('protectYourKeys') }
- </div>
- <div className="first-time-flow__text-block">
- { t('protectYourKeysMessage1') }
- </div>
- <div className="first-time-flow__text-block">
- { t('protectYourKeysMessage2') }
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Agree to Phishing Warning',
- name: 'Agree to Phishing Warning',
- },
- })
- history.push(INITIALIZE_END_OF_FLOW_ROUTE)
- }}
- >
- { t('next') }
- </Button>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
deleted file mode 100644
index c0e2f59d9..000000000
--- a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../../button'
-import { DEFAULT_ROUTE } from '../../../../routes'
-
-export default class EndOfFlowScreen extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- completeOnboarding: PropTypes.func,
- completionMetaMetricsName: PropTypes.string,
- }
-
- render () {
- const { t } = this.context
- const { history, completeOnboarding, completionMetaMetricsName } = this.props
-
- return (
- <div className="end-of-flow">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <div className="end-of-flow__emoji">🎉</div>
- <div className="first-time-flow__header">
- { t('congratulations') }
- </div>
- <div className="first-time-flow__text-block end-of-flow__text-1">
- { t('endOfFlowMessage1') }
- </div>
- <div className="first-time-flow__text-block end-of-flow__text-2">
- { t('endOfFlowMessage2') }
- </div>
- <div className="end-of-flow__text-3">
- { '• ' + t('endOfFlowMessage3') }
- </div>
- <div className="end-of-flow__text-3">
- { '• ' + t('endOfFlowMessage4') }
- </div>
- <div className="end-of-flow__text-3">
- { '• ' + t('endOfFlowMessage5') }
- </div>
- <div className="end-of-flow__text-3">
- { '• ' + t('endOfFlowMessage6') }
- </div>
- <div className="end-of-flow__text-3">
- { '• ' + t('endOfFlowMessage7') }
- </div>
- <div className="first-time-flow__text-block end-of-flow__text-4">
- *MetaMask cannot recover your seedphrase. <a
- href="https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-Tips"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span className="first-time-flow__link-text">
- Learn More
- </span>
- </a>.
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={async () => {
- await completeOnboarding()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Onboarding Complete',
- name: completionMetaMetricsName,
- },
- })
- history.push(DEFAULT_ROUTE)
- }}
- >
- { 'All Done' }
- </Button>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
deleted file mode 100644
index 91ae5a941..000000000
--- a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { connect } from 'react-redux'
-import EndOfFlow from './end-of-flow.component'
-import { setCompletedOnboarding } from '../../../../actions'
-
-const firstTimeFlowTypeNameMap = {
- create: 'New Wallet Created',
- 'import': 'New Wallet Imported',
-}
-
-const mapStateToProps = ({ metamask }) => {
- const { firstTimeFlowType } = metamask
-
- return {
- completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
- }
-}
-
-
-const mapDispatchToProps = dispatch => {
- return {
- completeOnboarding: () => dispatch(setCompletedOnboarding()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
deleted file mode 100644
index 5c2294393..000000000
--- a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { Redirect } from 'react-router-dom'
-import {
- DEFAULT_ROUTE,
- LOCK_ROUTE,
- INITIALIZE_WELCOME_ROUTE,
- INITIALIZE_UNLOCK_ROUTE,
- INITIALIZE_SEED_PHRASE_ROUTE,
- INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
-} from '../../../../routes'
-
-export default class FirstTimeFlowSwitch extends PureComponent {
- static propTypes = {
- completedOnboarding: PropTypes.bool,
- isInitialized: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- seedPhrase: PropTypes.string,
- optInMetaMetrics: PropTypes.bool,
- }
-
- render () {
- const {
- completedOnboarding,
- isInitialized,
- isUnlocked,
- seedPhrase,
- optInMetaMetrics,
- } = this.props
-
- if (completedOnboarding) {
- return <Redirect to={{ pathname: DEFAULT_ROUTE }} />
- }
-
- if (isUnlocked && !seedPhrase) {
- return <Redirect to={{ pathname: LOCK_ROUTE }} />
- }
-
- if (!isInitialized) {
- return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
- }
-
- if (!isUnlocked) {
- return <Redirect to={{ pathname: INITIALIZE_UNLOCK_ROUTE }} />
- }
-
- if (seedPhrase) {
- return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
- }
-
- if (optInMetaMetrics === null) {
- return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
- }
-
- return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.component.js b/ui/app/components/pages/first-time-flow/first-time-flow.component.js
deleted file mode 100644
index a1f629431..000000000
--- a/ui/app/components/pages/first-time-flow/first-time-flow.component.js
+++ /dev/null
@@ -1,152 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { Switch, Route } from 'react-router-dom'
-import FirstTimeFlowSwitch from './first-time-flow-switch'
-import Welcome from './welcome'
-import SelectAction from './select-action'
-import EndOfFlow from './end-of-flow'
-import Unlock from '../unlock-page'
-import CreatePassword from './create-password'
-import SeedPhrase from './seed-phrase'
-import MetaMetricsOptInScreen from './metametrics-opt-in'
-import {
- DEFAULT_ROUTE,
- INITIALIZE_WELCOME_ROUTE,
- INITIALIZE_CREATE_PASSWORD_ROUTE,
- INITIALIZE_SEED_PHRASE_ROUTE,
- INITIALIZE_UNLOCK_ROUTE,
- INITIALIZE_SELECT_ACTION_ROUTE,
- INITIALIZE_END_OF_FLOW_ROUTE,
- INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
-} from '../../../routes'
-
-export default class FirstTimeFlow extends PureComponent {
- static propTypes = {
- completedOnboarding: PropTypes.bool,
- createNewAccount: PropTypes.func,
- createNewAccountFromSeed: PropTypes.func,
- history: PropTypes.object,
- isInitialized: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- unlockAccount: PropTypes.func,
- nextRoute: PropTypes.func,
- }
-
- state = {
- seedPhrase: '',
- isImportedKeyring: false,
- }
-
- componentDidMount () {
- const { completedOnboarding, history, isInitialized, isUnlocked } = this.props
-
- if (completedOnboarding) {
- history.push(DEFAULT_ROUTE)
- return
- }
-
- if (isInitialized && !isUnlocked) {
- history.push(INITIALIZE_UNLOCK_ROUTE)
- return
- }
- }
-
- handleCreateNewAccount = async password => {
- const { createNewAccount } = this.props
-
- try {
- const seedPhrase = await createNewAccount(password)
- this.setState({ seedPhrase })
- } catch (error) {
- throw new Error(error.message)
- }
- }
-
- handleImportWithSeedPhrase = async (password, seedPhrase) => {
- const { createNewAccountFromSeed } = this.props
-
- try {
- await createNewAccountFromSeed(password, seedPhrase)
- this.setState({ isImportedKeyring: true })
- } catch (error) {
- throw new Error(error.message)
- }
- }
-
- handleUnlock = async password => {
- const { unlockAccount, history, nextRoute } = this.props
-
- try {
- const seedPhrase = await unlockAccount(password)
- this.setState({ seedPhrase }, () => {
- history.push(nextRoute)
- })
- } catch (error) {
- throw new Error(error.message)
- }
- }
-
- render () {
- const { seedPhrase, isImportedKeyring } = this.state
-
- return (
- <div className="first-time-flow">
- <Switch>
- <Route
- path={INITIALIZE_SEED_PHRASE_ROUTE}
- render={props => (
- <SeedPhrase
- { ...props }
- seedPhrase={seedPhrase}
- />
- )}
- />
- <Route
- path={INITIALIZE_CREATE_PASSWORD_ROUTE}
- render={props => (
- <CreatePassword
- { ...props }
- isImportedKeyring={isImportedKeyring}
- onCreateNewAccount={this.handleCreateNewAccount}
- onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
- />
- )}
- />
- <Route
- path={INITIALIZE_SELECT_ACTION_ROUTE}
- component={SelectAction}
- />
- <Route
- path={INITIALIZE_UNLOCK_ROUTE}
- render={props => (
- <Unlock
- { ...props }
- onSubmit={this.handleUnlock}
- />
- )}
- />
- <Route
- exact
- path={INITIALIZE_END_OF_FLOW_ROUTE}
- component={EndOfFlow}
- />
- <Route
- exact
- path={INITIALIZE_WELCOME_ROUTE}
- component={Welcome}
- />
- <Route
- exact
- path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
- component={MetaMetricsOptInScreen}
- />
- <Route
- exact
- path="*"
- component={FirstTimeFlowSwitch}
- />
- </Switch>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.container.js b/ui/app/components/pages/first-time-flow/first-time-flow.container.js
deleted file mode 100644
index 293f94c47..000000000
--- a/ui/app/components/pages/first-time-flow/first-time-flow.container.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { connect } from 'react-redux'
-import FirstTimeFlow from './first-time-flow.component'
-import { getFirstTimeFlowTypeRoute } from './first-time-flow.selectors'
-import {
- createNewVaultAndGetSeedPhrase,
- createNewVaultAndRestore,
- unlockAndGetSeedPhrase,
-} from '../../../actions'
-
-const mapStateToProps = state => {
- const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
-
- return {
- completedOnboarding,
- isInitialized,
- isUnlocked,
- nextRoute: getFirstTimeFlowTypeRoute(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- createNewAccount: password => dispatch(createNewVaultAndGetSeedPhrase(password)),
- createNewAccountFromSeed: (password, seedPhrase) => {
- return dispatch(createNewVaultAndRestore(password, seedPhrase))
- },
- unlockAccount: password => dispatch(unlockAndGetSeedPhrase(password)),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(FirstTimeFlow)
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js
deleted file mode 100644
index 1286afed9..000000000
--- a/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import {
- INITIALIZE_CREATE_PASSWORD_ROUTE,
- INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
- DEFAULT_ROUTE,
-} from '../../../routes'
-
-const selectors = {
- getFirstTimeFlowTypeRoute,
-}
-
-module.exports = selectors
-
-function getFirstTimeFlowTypeRoute (state) {
- const { firstTimeFlowType } = state.metamask
-
- let nextRoute
- if (firstTimeFlowType === 'create') {
- nextRoute = INITIALIZE_CREATE_PASSWORD_ROUTE
- } else if (firstTimeFlowType === 'import') {
- nextRoute = INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE
- } else {
- nextRoute = DEFAULT_ROUTE
- }
-
- return nextRoute
-}
diff --git a/ui/app/components/pages/first-time-flow/index.scss b/ui/app/components/pages/first-time-flow/index.scss
deleted file mode 100644
index d41748575..000000000
--- a/ui/app/components/pages/first-time-flow/index.scss
+++ /dev/null
@@ -1,159 +0,0 @@
-@import './welcome/index';
-
-@import './select-action/index';
-
-@import './seed-phrase/index';
-
-@import './end-of-flow/index';
-
-@import './metametrics-opt-in/index';
-
-
-.first-time-flow {
- width: 100%;
- background-color: $white;
- display: flex;
- justify-content: center;
-
- &__wrapper {
- @media screen and (min-width: $break-large) {
- max-width: 742px;
- display: flex;
- flex-direction: column;
- width: 100%;
- margin-top: 2%;
- }
-
- .app-header__metafox-logo {
- margin-bottom: 40px;
- }
- }
-
- &__form {
- display: flex;
- flex-direction: column;
- }
-
- &__create-back {
- margin-bottom: 16px;
- }
-
- &__header {
- font-size: 2.5rem;
- margin-bottom: 24px;
- color: black;
- }
-
- &__subheader {
- margin-bottom: 16px;
- }
-
- &__input {
- max-width: 350px;
- }
-
- &__textarea-wrapper {
- margin-bottom: 8px;
- display: inline-flex;
- padding: 0;
- position: relative;
- min-width: 0;
- flex-direction: column;
- max-width: 350px;
- }
-
- &__textarea-label {
- margin-bottom: 9px;
- color: #1B344D;
- font-size: 18px;
- }
-
- &__textarea {
- font-size: 1rem;
- font-family: Roboto;
- height: 190px;
- border: 1px solid #CDCDCD;
- border-radius: 6px;
- background-color: #FFFFFF;
- padding: 16px;
- margin-top: 8px;
- }
-
- &__breadcrumbs {
- margin: 36px 0;
- }
-
- &__unique-image {
- margin-bottom: 20px;
- }
-
- &__markdown {
- border: 1px solid #979797;
- border-radius: 8px;
- background-color: $white;
- height: 200px;
- overflow-y: auto;
- color: #757575;
- font-size: .75rem;
- line-height: 15px;
- text-align: justify;
- margin: 0;
- padding: 16px 20px;
- height: 30vh;
- }
-
- &__text-block {
- margin-bottom: 24px;
- color: black;
-
- @media screen and (max-width: $break-small) {
- margin-bottom: 16px;
- font-size: .875rem;
- }
- }
-
- &__button {
- margin: 35px 0 14px;
- width: 140px;
- height: 44px;
- }
-
- &__checkbox-container {
- display: flex;
- align-items: center;
- margin-top: 24px;
- }
-
- &__checkbox {
- background: #FFFFFF;
- border: 1px solid #CDCDCD;
- box-sizing: border-box;
- height: 34px;
- width: 34px;
- display: flex;
- justify-content: center;
- align-items: center;
-
- &:hover {
- border: 1.5px solid #2f9ae0;
- }
-
- .fa-check {
- color: #2f9ae0
- }
- }
-
- &__checkbox-label {
- font-family: Roboto;
- font-style: normal;
- font-weight: normal;
- line-height: normal;
- font-size: 18px;
- color: #939090;
- margin-left: 18px;
- }
-
- &__link-text {
- color: $curious-blue;
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
deleted file mode 100644
index 58a03944e..000000000
--- a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainerFooter from '../../../page-container/page-container-footer'
-
-export default class MetaMetricsOptIn extends Component {
- static propTypes = {
- history: PropTypes.object,
- setParticipateInMetaMetrics: PropTypes.func,
- nextRoute: PropTypes.string,
- firstTimeSelectionMetaMetricsName: PropTypes.string,
- participateInMetaMetrics: PropTypes.bool,
- }
-
- static contextTypes = {
- metricsEvent: PropTypes.func,
- }
-
- render () {
- const { metricsEvent } = this.context
- const {
- nextRoute,
- history,
- setParticipateInMetaMetrics,
- firstTimeSelectionMetaMetricsName,
- participateInMetaMetrics,
- } = this.props
-
- return (
- <div className="metametrics-opt-in">
- <div className="metametrics-opt-in__main">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <div className="metametrics-opt-in__body-graphic">
- <img src="images/metrics-chart.svg" />
- </div>
- <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
- <div className="metametrics-opt-in__body">
- <div className="metametrics-opt-in__description">
- MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
- will be used to continually improve the usability and user experience of our product and the Etheruem ecosystem.
- </div>
- <div className="metametrics-opt-in__description">
- MetaMask will..
- </div>
-
- <div className="metametrics-opt-in__committments">
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Always allow you to opt-out via Settings
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Send anonymized click & pageview events
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-check" />
- <div className="metametrics-opt-in__row-description">
- Maintain a public aggregate dashboard to educate the community
- </div>
- </div>
- <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
- </div>
- </div>
- <div className="metametrics-opt-in__row">
- <i className="fa fa-times" />
- <div className="metametrics-opt-in__row-description">
- <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
- </div>
- </div>
- </div>
- </div>
- <div className="metametrics-opt-in__footer">
- <PageContainerFooter
- onCancel={() => {
- setParticipateInMetaMetrics(false)
- .then(() => {
- const promise = participateInMetaMetrics !== false
- ? metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Metrics Option',
- name: 'Metrics Opt Out',
- },
- isOptIn: true,
- })
- : Promise.resolve()
-
- promise
- .then(() => {
- history.push(nextRoute)
- })
- })
- }}
- cancelText={'No Thanks'}
- hideCancel={false}
- onSubmit={() => {
- setParticipateInMetaMetrics(true)
- .then(([participateStatus, metaMetricsId]) => {
- const promise = participateInMetaMetrics !== true
- ? metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Metrics Option',
- name: 'Metrics Opt In',
- },
- isOptIn: true,
- })
- : Promise.resolve()
-
- promise
- .then(() => {
- return metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Import or Create',
- name: firstTimeSelectionMetaMetricsName,
- },
- isOptIn: true,
- metaMetricsId,
- })
- })
- .then(() => {
- history.push(nextRoute)
- })
- })
- }}
- submitText={'I agree'}
- submitButtonType={'confirm'}
- disabled={false}
- />
- <div className="metametrics-opt-in__bottom-text">
- This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
- href="https://metamask.io/privacy.html"
- target="_blank"
- rel="noopener noreferrer"
- >
- Privacy Policy here
- </a>.
- </div>
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
deleted file mode 100644
index b13af8bf6..000000000
--- a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux'
-import MetaMetricsOptIn from './metametrics-opt-in.component'
-import { setParticipateInMetaMetrics } from '../../../../actions'
-import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
-
-const firstTimeFlowTypeNameMap = {
- create: 'Selected Create New Wallet',
- 'import': 'Selected Import Wallet',
-}
-
-const mapStateToProps = (state) => {
- const { firstTimeFlowType, participateInMetaMetrics } = state.metamask
-
- return {
- nextRoute: getFirstTimeFlowTypeRoute(state),
- firstTimeSelectionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
- participateInMetaMetrics,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(MetaMetricsOptIn)
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
deleted file mode 100644
index bd5ab8a84..000000000
--- a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import shuffle from 'lodash.shuffle'
-import Button from '../../../../button'
-import {
- INITIALIZE_END_OF_FLOW_ROUTE,
- INITIALIZE_SEED_PHRASE_ROUTE,
-} from '../../../../../routes'
-import { exportAsFile } from '../../../../../../app/util'
-import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
-
-export default class ConfirmSeedPhrase extends PureComponent {
- static contextTypes = {
- metricsEvent: PropTypes.func,
- t: PropTypes.func,
- }
-
- static defaultProps = {
- seedPhrase: '',
- }
-
- static propTypes = {
- history: PropTypes.object,
- onSubmit: PropTypes.func,
- seedPhrase: PropTypes.string,
- }
-
- state = {
- selectedSeedWords: [],
- shuffledSeedWords: [],
- // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number}
- selectedSeedWordsHash: {},
- }
-
- componentDidMount () {
- const { seedPhrase = '' } = this.props
- const shuffledSeedWords = shuffle(seedPhrase.split(' ')) || []
- this.setState({ shuffledSeedWords })
- }
-
- handleExport = () => {
- exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
- }
-
- handleSubmit = async () => {
- const { history } = this.props
-
- if (!this.isValid()) {
- return
- }
-
- try {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Seed Phrase Setup',
- name: 'Verify Complete',
- },
- })
- history.push(INITIALIZE_END_OF_FLOW_ROUTE)
- } catch (error) {
- console.error(error.message)
- }
- }
-
- handleSelectSeedWord = (word, shuffledIndex) => {
- this.setState(selectSeedWord(word, shuffledIndex))
- }
-
- handleDeselectSeedWord = shuffledIndex => {
- this.setState(deselectSeedWord(shuffledIndex))
- }
-
- isValid () {
- const { seedPhrase } = this.props
- const { selectedSeedWords } = this.state
- return seedPhrase === selectedSeedWords.join(' ')
- }
-
- render () {
- const { t } = this.context
- const { history } = this.props
- const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
-
- return (
- <div className="confirm-seed-phrase">
- <div className="confirm-seed-phrase__back-button">
- <a
- onClick={e => {
- e.preventDefault()
- history.push(INITIALIZE_SEED_PHRASE_ROUTE)
- }}
- href="#"
- >
- {`< Back`}
- </a>
- </div>
- <div className="first-time-flow__header">
- { t('confirmSecretBackupPhrase') }
- </div>
- <div className="first-time-flow__text-block">
- { t('selectEachPhrase') }
- </div>
- <div className="confirm-seed-phrase__selected-seed-words">
- {
- selectedSeedWords.map((word, index) => (
- <div
- key={index}
- className="confirm-seed-phrase__seed-word"
- >
- { word }
- </div>
- ))
- }
- </div>
- <div className="confirm-seed-phrase__shuffled-seed-words">
- {
- shuffledSeedWords.map((word, index) => {
- const isSelected = index in selectedSeedWordsHash
-
- return (
- <div
- key={index}
- className={classnames(
- 'confirm-seed-phrase__seed-word',
- 'confirm-seed-phrase__seed-word--shuffled',
- { 'confirm-seed-phrase__seed-word--selected': isSelected }
- )}
- onClick={() => {
- if (!isSelected) {
- this.handleSelectSeedWord(word, index)
- } else {
- this.handleDeselectSeedWord(index)
- }
- }}
- >
- { word }
- </div>
- )
- })
- }
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={this.handleSubmit}
- disabled={!this.isValid()}
- >
- { t('confirm') }
- </Button>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/index.scss
deleted file mode 100644
index e4fd7be4f..000000000
--- a/ui/app/components/pages/first-time-flow/seed-phrase/index.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-@import './confirm-seed-phrase/index';
-
-@import './reveal-seed-phrase/index';
-
-.seed-phrase {
-
- &__sections {
- display: flex;
-
- @media screen and (min-width: $break-large) {
- flex-direction: row;
- }
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- }
- }
-
- &__main {
- flex: 3;
- min-width: 0;
- }
-
- &__side {
- flex: 2;
- min-width: 0;
-
- @media screen and (min-width: $break-large) {
- margin-left: 81px;
- }
-
- @media screen and (max-width: $break-small) {
- margin-top: 24px;
- }
-
- .first-time-flow__text-block {
- color: #5A5A5A;
- }
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
deleted file mode 100644
index cb8a01322..000000000
--- a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import LockIcon from '../../../../lock-icon'
-import Button from '../../../../button'
-import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../../routes'
-import { exportAsFile } from '../../../../../../app/util'
-
-export default class RevealSeedPhrase extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- seedPhrase: PropTypes.string,
- }
-
- state = {
- isShowingSeedPhrase: false,
- }
-
- handleExport = () => {
- exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
- }
-
- handleNext = event => {
- event.preventDefault()
- const { isShowingSeedPhrase } = this.state
- const { history } = this.props
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Seed Phrase Setup',
- name: 'Advance to Verify',
- },
- })
-
- if (!isShowingSeedPhrase) {
- return
- }
-
- history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE)
- }
-
- renderSecretWordsContainer () {
- const { t } = this.context
- const { seedPhrase } = this.props
- const { isShowingSeedPhrase } = this.state
-
- return (
- <div className="reveal-seed-phrase__secret">
- <div className={classnames(
- 'reveal-seed-phrase__secret-words',
- { 'reveal-seed-phrase__secret-words--hidden': !isShowingSeedPhrase }
- )}>
- { seedPhrase }
- </div>
- {
- !isShowingSeedPhrase && (
- <div
- className="reveal-seed-phrase__secret-blocker"
- onClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Onboarding',
- action: 'Seed Phrase Setup',
- name: 'Revealed Words',
- },
- })
- this.setState({ isShowingSeedPhrase: true })
- }}
- >
- <LockIcon
- width="28px"
- height="35px"
- fill="#FFFFFF"
- />
- <div className="reveal-seed-phrase__reveal-button">
- { t('clickToRevealSeed') }
- </div>
- </div>
- )
- }
- </div>
- )
- }
-
- render () {
- const { t } = this.context
- const { isShowingSeedPhrase } = this.state
-
- return (
- <div className="reveal-seed-phrase">
- <div className="seed-phrase__sections">
- <div className="seed-phrase__main">
- <div className="first-time-flow__header">
- { t('secretBackupPhrase') }
- </div>
- <div className="first-time-flow__text-block">
- { t('secretBackupPhraseDescription') }
- </div>
- <div className="first-time-flow__text-block">
- { t('secretBackupPhraseWarning') }
- </div>
- { this.renderSecretWordsContainer() }
- </div>
- <div className="seed-phrase__side">
- <div className="first-time-flow__text-block">
- { `${t('tips')}:` }
- </div>
- <div className="first-time-flow__text-block">
- { t('storePhrase') }
- </div>
- <div className="first-time-flow__text-block">
- { t('writePhrase') }
- </div>
- <div className="first-time-flow__text-block">
- { t('memorizePhrase') }
- </div>
- <div className="first-time-flow__text-block">
- <a
- className="reveal-seed-phrase__export-text"
- onClick={this.handleExport}>
- { t('downloadSecretBackup') }
- </a>
- </div>
- </div>
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={this.handleNext}
- disabled={!isShowingSeedPhrase}
- >
- { t('next') }
- </Button>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js
deleted file mode 100644
index 9eec89cdd..000000000
--- a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { Switch, Route } from 'react-router-dom'
-import RevealSeedPhrase from './reveal-seed-phrase'
-import ConfirmSeedPhrase from './confirm-seed-phrase'
-import {
- INITIALIZE_SEED_PHRASE_ROUTE,
- INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
- DEFAULT_ROUTE,
-} from '../../../../routes'
-
-export default class SeedPhrase extends PureComponent {
- static propTypes = {
- address: PropTypes.string,
- history: PropTypes.object,
- seedPhrase: PropTypes.string,
- }
-
- componentDidMount () {
- const { seedPhrase, history } = this.props
-
- if (!seedPhrase) {
- history.push(DEFAULT_ROUTE)
- }
- }
-
- render () {
- const { seedPhrase } = this.props
-
- return (
- <div className="first-time-flow__wrapper">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
- <Switch>
- <Route
- exact
- path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
- render={props => (
- <ConfirmSeedPhrase
- { ...props }
- seedPhrase={seedPhrase}
- />
- )}
- />
- <Route
- exact
- path={INITIALIZE_SEED_PHRASE_ROUTE}
- render={props => (
- <RevealSeedPhrase
- { ...props }
- seedPhrase={seedPhrase}
- />
- )}
- />
- </Switch>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/select-action/select-action.component.js b/ui/app/components/pages/first-time-flow/select-action/select-action.component.js
deleted file mode 100644
index b6a6942c3..000000000
--- a/ui/app/components/pages/first-time-flow/select-action/select-action.component.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../../button'
-import {
- INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
-} from '../../../../routes'
-
-export default class SelectAction extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- isInitialized: PropTypes.bool,
- setFirstTimeFlowType: PropTypes.func,
- nextRoute: PropTypes.string,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- componentDidMount () {
- const { history, isInitialized, nextRoute } = this.props
-
- if (isInitialized) {
- history.push(nextRoute)
- }
- }
-
- handleCreate = () => {
- this.props.setFirstTimeFlowType('create')
- this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
- }
-
- handleImport = () => {
- this.props.setFirstTimeFlowType('import')
- this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
- }
-
- render () {
- const { t } = this.context
-
- return (
- <div className="select-action">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.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>
-
- <div className="select-action__wrapper">
-
-
- <div className="select-action__body">
- <div className="select-action__body-header">
- { t('newToMetaMask') }
- </div>
- <div className="select-action__select-buttons">
- <div className="select-action__select-button">
- <div className="select-action__button-content">
- <div className="select-action__button-symbol">
- <img src="/images/download-alt.svg" />
- </div>
- <div className="select-action__button-text-big">
- { t('noAlreadyHaveSeed') }
- </div>
- <div className="select-action__button-text-small">
- { t('importYourExisting') }
- </div>
- </div>
- <Button
- type="primary"
- className="first-time-flow__button"
- onClick={this.handleImport}
- >
- { t('importWallet') }
- </Button>
- </div>
- <div className="select-action__select-button">
- <div className="select-action__button-content">
- <div className="select-action__button-symbol">
- <img src="/images/thin-plus.svg" />
- </div>
- <div className="select-action__button-text-big">
- { t('letsGoSetUp') }
- </div>
- <div className="select-action__button-text-small">
- { t('thisWillCreate') }
- </div>
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={this.handleCreate}
- >
- { t('createAWallet') }
- </Button>
- </div>
- </div>
- </div>
-
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/select-action/select-action.container.js b/ui/app/components/pages/first-time-flow/select-action/select-action.container.js
deleted file mode 100644
index 42fac7af2..000000000
--- a/ui/app/components/pages/first-time-flow/select-action/select-action.container.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import { setFirstTimeFlowType } from '../../../../actions'
-import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
-import Welcome from './select-action.component'
-
-const mapStateToProps = (state) => {
- return {
- nextRoute: getFirstTimeFlowTypeRoute(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setFirstTimeFlowType: type => dispatch(setFirstTimeFlowType(type)),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(Welcome)
diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.component.js b/ui/app/components/pages/first-time-flow/welcome/welcome.component.js
deleted file mode 100644
index 88cdb936c..000000000
--- a/ui/app/components/pages/first-time-flow/welcome/welcome.component.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import EventEmitter from 'events'
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Mascot from '../../../mascot'
-import Button from '../../../button'
-import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE } from '../../../../routes'
-
-export default class Welcome extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- isInitialized: PropTypes.bool,
- participateInMetaMetrics: PropTypes.bool,
- welcomeScreenSeen: PropTypes.bool,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- constructor (props) {
- super(props)
-
- this.animationEventEmitter = new EventEmitter()
- }
-
- componentDidMount () {
- const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props
-
- if (welcomeScreenSeen && participateInMetaMetrics !== null) {
- history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
- } else if (welcomeScreenSeen) {
- history.push(INITIALIZE_SELECT_ACTION_ROUTE)
- }
- }
-
- handleContinue = () => {
- this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
- }
-
- render () {
- const { t } = this.context
-
- return (
- <div className="welcome-page__wrapper">
- <div className="welcome-page">
- <Mascot
- animationEventEmitter={this.animationEventEmitter}
- width="125"
- height="125"
- />
- <div className="welcome-page__header">
- { t('welcome') }
- </div>
- <div className="welcome-page__description">
- <div>{ t('metamaskDescription') }</div>
- <div>{ t('happyToSeeYou') }</div>
- </div>
- <Button
- type="confirm"
- className="first-time-flow__button"
- onClick={this.handleContinue}
- >
- { t('getStarted') }
- </Button>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.container.js b/ui/app/components/pages/first-time-flow/welcome/welcome.container.js
deleted file mode 100644
index 47753e16f..000000000
--- a/ui/app/components/pages/first-time-flow/welcome/welcome.container.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import { closeWelcomeScreen } from '../../../../actions'
-import Welcome from './welcome.component'
-
-const mapStateToProps = ({ metamask }) => {
- const { welcomeScreenSeen, isInitialized, participateInMetaMetrics } = metamask
-
- return {
- welcomeScreenSeen,
- isInitialized,
- participateInMetaMetrics,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(Welcome)
diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js
deleted file mode 100644
index 953d43aba..000000000
--- a/ui/app/components/pages/home/home.component.js
+++ /dev/null
@@ -1,77 +0,0 @@
-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 ProviderApproval from '../provider-approval'
-
-import {
- INITIALIZE_SEED_PHRASE_ROUTE,
- RESTORE_VAULT_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
-} from '../../../routes'
-
-export default class Home extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- forgottenPassword: PropTypes.bool,
- seedWords: PropTypes.string,
- suggestedTokens: PropTypes.object,
- unconfirmedTransactionsCount: PropTypes.number,
- providerRequests: PropTypes.array,
- }
-
- 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 {
- forgottenPassword,
- seedWords,
- providerRequests,
- } = this.props
-
- // seed words
- if (seedWords) {
- return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/>
- }
-
- if (forgottenPassword) {
- return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
- }
-
- if (providerRequests && providerRequests.length > 0) {
- return (
- <ProviderApproval providerRequest={providerRequests[0]} />
- )
- }
-
- 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
deleted file mode 100644
index bb8cf5e81..000000000
--- a/ui/app/components/pages/home/home.container.js
+++ /dev/null
@@ -1,32 +0,0 @@
-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,
- providerRequests,
- } = metamask
- const { forgottenPassword } = appState
-
- return {
- noActiveNotices,
- lostAccounts,
- forgottenPassword,
- seedWords,
- suggestedTokens,
- unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
- providerRequests,
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps)
-)(Home)
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
deleted file mode 100644
index 6a0680f32..000000000
--- a/ui/app/components/pages/index.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import './unlock-page/index';
-
-@import './add-token/index';
-
-@import './confirm-add-token/index';
-
-@import './settings/index';
-
-@import './first-time-flow/index';
-
-@import './keychains/index';
diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js
deleted file mode 100644
index 73ff5191a..000000000
--- a/ui/app/components/pages/keychains/restore-vault.js
+++ /dev/null
@@ -1,197 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import {connect} from 'react-redux'
-import {
- createNewVaultAndRestore,
- unMarkPasswordForgotten,
-} from '../../../actions'
-import { DEFAULT_ROUTE } from '../../../routes'
-import TextField from '../../text-field'
-import Button from '../../button'
-
-class RestoreVaultPage extends Component {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- warning: PropTypes.string,
- createNewVaultAndRestore: PropTypes.func.isRequired,
- leaveImportSeedScreenState: PropTypes.func,
- history: PropTypes.object,
- isLoading: PropTypes.bool,
- };
-
- state = {
- seedPhrase: '',
- password: '',
- confirmPassword: '',
- seedPhraseError: null,
- passwordError: null,
- confirmPasswordError: null,
- }
-
- parseSeedPhrase = (seedPhrase) => {
- return seedPhrase
- .match(/\w+/g)
- .join(' ')
- }
-
- handleSeedPhraseChange (seedPhrase) {
- let seedPhraseError = null
-
- if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
- seedPhraseError = this.context.t('seedPhraseReq')
- }
-
- this.setState({ seedPhrase, seedPhraseError })
- }
-
- handlePasswordChange (password) {
- const { confirmPassword } = this.state
- let confirmPasswordError = null
- let passwordError = null
-
- if (password && password.length < 8) {
- passwordError = this.context.t('passwordNotLongEnough')
- }
-
- if (confirmPassword && password !== confirmPassword) {
- confirmPasswordError = this.context.t('passwordsDontMatch')
- }
-
- this.setState({ password, passwordError, confirmPasswordError })
- }
-
- handleConfirmPasswordChange (confirmPassword) {
- const { password } = this.state
- let confirmPasswordError = null
-
- if (password !== confirmPassword) {
- confirmPasswordError = this.context.t('passwordsDontMatch')
- }
-
- this.setState({ confirmPassword, confirmPasswordError })
- }
-
- onClick = () => {
- const { password, seedPhrase } = this.state
- const {
- createNewVaultAndRestore,
- leaveImportSeedScreenState,
- history,
- } = this.props
-
- leaveImportSeedScreenState()
- createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
- .then(() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Retention',
- action: 'userEntersSeedPhrase',
- name: 'onboardingRestoredVault',
- },
- })
- history.push(DEFAULT_ROUTE)
- })
- }
-
- hasError () {
- const { passwordError, confirmPasswordError, seedPhraseError } = this.state
- return passwordError || confirmPasswordError || seedPhraseError
- }
-
- render () {
- const {
- seedPhrase,
- password,
- confirmPassword,
- seedPhraseError,
- passwordError,
- confirmPasswordError,
- } = this.state
- const { t } = this.context
- const { isLoading } = this.props
- const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
-
- return (
- <div className="first-view-main-wrapper">
- <div className="first-view-main">
- <div className="import-account">
- <a
- className="import-account__back-button"
- onClick={e => {
- e.preventDefault()
- this.props.history.goBack()
- }}
- href="#"
- >
- {`< Back`}
- </a>
- <div className="import-account__title">
- { this.context.t('restoreAccountWithSeed') }
- </div>
- <div className="import-account__selector-label">
- { this.context.t('secretPhrase') }
- </div>
- <div className="import-account__input-wrapper">
- <label className="import-account__input-label">Wallet Seed</label>
- <textarea
- className="import-account__secret-phrase"
- onChange={e => this.handleSeedPhraseChange(e.target.value)}
- value={this.state.seedPhrase}
- placeholder={this.context.t('separateEachWord')}
- />
- </div>
- <span className="error">
- { seedPhraseError }
- </span>
- <TextField
- id="password"
- label={t('newPassword')}
- type="password"
- className="first-time-flow__input"
- value={this.state.password}
- onChange={event => this.handlePasswordChange(event.target.value)}
- error={passwordError}
- autoComplete="new-password"
- margin="normal"
- largeLabel
- />
- <TextField
- id="confirm-password"
- label={t('confirmPassword')}
- type="password"
- className="first-time-flow__input"
- value={this.state.confirmPassword}
- onChange={event => this.handleConfirmPasswordChange(event.target.value)}
- error={confirmPasswordError}
- autoComplete="confirm-password"
- margin="normal"
- largeLabel
- />
- <Button
- type="first-time"
- className="first-time-flow__button"
- onClick={() => !disabled && this.onClick()}
- disabled={disabled}
- >
- {this.context.t('restore')}
- </Button>
- </div>
- </div>
- </div>
- )
- }
-}
-
-export default connect(
- ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
- dispatch => ({
- leaveImportSeedScreenState: () => {
- dispatch(unMarkPasswordForgotten())
- },
- createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
- })
-)(RestoreVaultPage)
diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js
deleted file mode 100644
index 32557066f..000000000
--- a/ui/app/components/pages/keychains/reveal-seed.js
+++ /dev/null
@@ -1,177 +0,0 @@
-const { Component } = require('react')
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const classnames = require('classnames')
-
-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'
-
-class RevealSeedPage extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- screen: PASSWORD_PROMPT_SCREEN,
- password: '',
- seedWords: null,
- error: null,
- }
- }
-
- componentDidMount () {
- const passwordBox = document.getElementById('password-box')
- if (passwordBox) {
- passwordBox.focus()
- }
- }
-
- handleSubmit (event) {
- event.preventDefault()
- this.setState({ seedWords: null, error: null })
- this.props.requestRevealSeedWords(this.state.password)
- .then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
- .catch(error => this.setState({ error: error.message }))
- }
-
- renderWarning () {
- return (
- h('.page-container__warning-container', [
- h('img.page-container__warning-icon', {
- src: 'images/warning.svg',
- }),
- h('.page-container__warning-message', [
- h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
- h('div', [this.context.t('revealSeedWordsWarning')]),
- ]),
- ])
- )
- }
-
- renderContent () {
- return this.state.screen === PASSWORD_PROMPT_SCREEN
- ? this.renderPasswordPromptContent()
- : this.renderRevealSeedContent()
- }
-
- renderPasswordPromptContent () {
- const { t } = this.context
-
- return (
- h('form', {
- onSubmit: event => this.handleSubmit(event),
- }, [
- h('label.input-label', {
- htmlFor: 'password-box',
- }, t('enterPasswordContinue')),
- h('.input-group', [
- h('input.form-control', {
- type: 'password',
- placeholder: t('password'),
- id: 'password-box',
- value: this.state.password,
- onChange: event => this.setState({ password: event.target.value }),
- className: classnames({ 'form-control--error': this.state.error }),
- }),
- ]),
- this.state.error && h('.reveal-seed__error', this.state.error),
- ])
- )
- }
-
- renderRevealSeedContent () {
- const { t } = this.context
-
- return (
- h('div', [
- h('label.reveal-seed__label', t('yourPrivateSeedPhrase')),
- h(ExportTextContainer, {
- text: this.state.seedWords,
- filename: t('metamaskSeedWords'),
- }),
- ])
- )
- }
-
- renderFooter () {
- return this.state.screen === PASSWORD_PROMPT_SCREEN
- ? this.renderPasswordPromptFooter()
- : this.renderRevealSeedFooter()
- }
-
- renderPasswordPromptFooter () {
- return (
- h('.page-container__footer', [
- 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')),
- ]),
- ])
- )
- }
-
- renderRevealSeedFooter () {
- return (
- h('.page-container__footer', [
- h(Button, {
- type: 'default',
- large: true,
- className: 'page-container__footer-button',
- onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, this.context.t('close')),
- ])
- )
- }
-
- render () {
- return (
- h('.page-container', [
- h('.page-container__header', [
- h('.page-container__title', this.context.t('revealSeedWordsTitle')),
- h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')),
- ]),
- h('.page-container__content', [
- this.renderWarning(),
- h('.reveal-seed__content', [
- this.renderContent(),
- ]),
- ]),
- this.renderFooter(),
- ])
- )
- }
-}
-
-RevealSeedPage.propTypes = {
- requestRevealSeedWords: PropTypes.func,
- history: PropTypes.object,
-}
-
-RevealSeedPage.contextTypes = {
- t: PropTypes.func,
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
- }
-}
-
-module.exports = connect(null, mapDispatchToProps)(RevealSeedPage)
diff --git a/ui/app/components/pages/lock/lock.component.js b/ui/app/components/pages/lock/lock.component.js
deleted file mode 100644
index 51f8742ed..000000000
--- a/ui/app/components/pages/lock/lock.component.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Loading from '../../loading-screen'
-import { DEFAULT_ROUTE } from '../../../routes'
-
-export default class Lock extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- lockMetamask: PropTypes.func,
- }
-
- componentDidMount () {
- const { lockMetamask, isUnlocked, history } = this.props
-
- if (isUnlocked) {
- lockMetamask().then(() => history.push(DEFAULT_ROUTE))
- } else {
- history.replace(DEFAULT_ROUTE)
- }
- }
-
- render () {
- return <Loading />
- }
-}
diff --git a/ui/app/components/pages/lock/lock.container.js b/ui/app/components/pages/lock/lock.container.js
deleted file mode 100644
index 81d89ba21..000000000
--- a/ui/app/components/pages/lock/lock.container.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Lock from './lock.component'
-import { compose } from 'recompose'
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { lockMetamask } from '../../../actions'
-
-const mapStateToProps = state => {
- const { metamask: { isUnlocked } } = state
-
- return {
- isUnlocked,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- lockMetamask: () => dispatch(lockMetamask()),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(Lock)
diff --git a/ui/app/components/pages/mobile-sync/index.js b/ui/app/components/pages/mobile-sync/index.js
deleted file mode 100644
index 22a69d092..000000000
--- a/ui/app/components/pages/mobile-sync/index.js
+++ /dev/null
@@ -1,387 +0,0 @@
-const { Component } = require('react')
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const classnames = require('classnames')
-const PubNub = require('pubnub')
-
-const { requestRevealSeedWords, fetchInfoToSync } = require('../../../actions')
-const { DEFAULT_ROUTE } = require('../../../routes')
-const actions = require('../../../actions')
-
-const qrCode = require('qrcode-generator')
-
-import Button from '../../button'
-import LoadingScreen from '../../loading-screen'
-
-const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
-const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
-
-class MobileSyncPage extends Component {
- static propTypes = {
- history: PropTypes.object,
- selectedAddress: PropTypes.string,
- displayWarning: PropTypes.func,
- fetchInfoToSync: PropTypes.func,
- requestRevealSeedWords: PropTypes.func,
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- screen: PASSWORD_PROMPT_SCREEN,
- password: '',
- seedWords: null,
- error: null,
- syncing: false,
- completed: false,
- }
-
- this.syncing = false
- }
-
- componentDidMount () {
- const passwordBox = document.getElementById('password-box')
- if (passwordBox) {
- passwordBox.focus()
- }
- }
-
- handleSubmit (event) {
- event.preventDefault()
- this.setState({ seedWords: null, error: null })
- this.props.requestRevealSeedWords(this.state.password)
- .then(seedWords => {
- this.generateCipherKeyAndChannelName()
- this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })
- this.initWebsockets()
- })
- .catch(error => this.setState({ error: error.message }))
- }
-
- generateCipherKeyAndChannelName () {
- this.cipherKey = `${this.props.selectedAddress.substr(-4)}-${PubNub.generateUUID()}`
- this.channelName = `mm-${PubNub.generateUUID()}`
- }
-
- initWebsockets () {
- this.pubnub = new PubNub({
- subscribeKey: process.env.PUBNUB_SUB_KEY,
- publishKey: process.env.PUBNUB_PUB_KEY,
- cipherKey: this.cipherKey,
- ssl: true,
- })
-
- this.pubnubListener = this.pubnub.addListener({
- message: (data) => {
- const {channel, message} = data
- // handle message
- if (channel !== this.channelName || !message) {
- return false
- }
-
- if (message.event === 'start-sync') {
- this.startSyncing()
- } else if (message.event === 'end-sync') {
- this.disconnectWebsockets()
- this.setState({syncing: false, completed: true})
- }
- },
- })
-
- this.pubnub.subscribe({
- channels: [this.channelName],
- withPresence: false,
- })
-
- }
-
- disconnectWebsockets () {
- if (this.pubnub && this.pubnubListener) {
- this.pubnub.disconnect(this.pubnubListener)
- }
- }
-
- // Calculating a PubNub Message Payload Size.
- calculatePayloadSize (channel, message) {
- return encodeURIComponent(
- channel + JSON.stringify(message)
- ).length + 100
- }
-
- chunkString (str, size) {
- const numChunks = Math.ceil(str.length / size)
- const chunks = new Array(numChunks)
- for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
- chunks[i] = str.substr(o, size)
- }
- return chunks
- }
-
- notifyError (errorMsg) {
- return new Promise((resolve, reject) => {
- this.pubnub.publish(
- {
- message: {
- event: 'error-sync',
- data: errorMsg,
- },
- channel: this.channelName,
- sendByPost: false, // true to send via post
- storeInHistory: false,
- },
- (status, response) => {
- if (!status.error) {
- resolve()
- } else {
- reject(response)
- }
- })
- })
- }
-
- async startSyncing () {
- if (this.syncing) return false
- this.syncing = true
- this.setState({syncing: true})
-
- const { accounts, network, preferences, transactions } = await this.props.fetchInfoToSync()
-
- const allDataStr = JSON.stringify({
- accounts,
- network,
- preferences,
- transactions,
- udata: {
- pwd: this.state.password,
- seed: this.state.seedWords,
- },
- })
-
- const chunks = this.chunkString(allDataStr, 17000)
- const totalChunks = chunks.length
- try {
- for (let i = 0; i < totalChunks; i++) {
- await this.sendMessage(chunks[i], i + 1, totalChunks)
- }
- } catch (e) {
- this.props.displayWarning('Sync failed :(')
- this.setState({syncing: false})
- this.syncing = false
- this.notifyError(e.toString())
- }
- }
-
- sendMessage (data, pkg, count) {
- return new Promise((resolve, reject) => {
- this.pubnub.publish(
- {
- message: {
- event: 'syncing-data',
- data,
- totalPkg: count,
- currentPkg: pkg,
- },
- channel: this.channelName,
- sendByPost: false, // true to send via post
- storeInHistory: false,
- },
- (status, response) => {
- if (!status.error) {
- resolve()
- } else {
- reject(response)
- }
- }
- )
- })
- }
-
-
- componentWillUnmount () {
- this.disconnectWebsockets()
- }
-
- renderWarning (text) {
- return (
- h('.page-container__warning-container', [
- h('.page-container__warning-message', [
- h('div', [text]),
- ]),
- ])
- )
- }
-
- renderContent () {
- const { t } = this.context
-
- if (this.state.syncing) {
- return h(LoadingScreen, {loadingMessage: 'Sync in progress'})
- }
-
- if (this.state.completed) {
- return h('div.reveal-seed__content', {},
- h('label.reveal-seed__label', {
- style: {
- width: '100%',
- textAlign: 'center',
- },
- }, t('syncWithMobileComplete')),
- )
- }
-
- return this.state.screen === PASSWORD_PROMPT_SCREEN
- ? h('div', {}, [
- this.renderWarning(this.context.t('mobileSyncText')),
- h('.reveal-seed__content', [
- this.renderPasswordPromptContent(),
- ]),
- ])
- : h('div', {}, [
- this.renderWarning(this.context.t('syncWithMobileBeCareful')),
- h('.reveal-seed__content', [ this.renderRevealSeedContent() ]),
- ])
- }
-
- renderPasswordPromptContent () {
- const { t } = this.context
-
- return (
- h('form', {
- onSubmit: event => this.handleSubmit(event),
- }, [
- h('label.input-label', {
- htmlFor: 'password-box',
- }, t('enterPasswordContinue')),
- h('.input-group', [
- h('input.form-control', {
- type: 'password',
- placeholder: t('password'),
- id: 'password-box',
- value: this.state.password,
- onChange: event => this.setState({ password: event.target.value }),
- className: classnames({ 'form-control--error': this.state.error }),
- }),
- ]),
- this.state.error && h('.reveal-seed__error', this.state.error),
- ])
- )
- }
-
- renderRevealSeedContent () {
-
- const qrImage = qrCode(0, 'M')
- qrImage.addData(`metamask-sync:${this.channelName}|@|${this.cipherKey}`)
- qrImage.make()
-
- const { t } = this.context
- return (
- h('div', [
- h('label.reveal-seed__label', {
- style: {
- width: '100%',
- textAlign: 'center',
- },
- }, t('syncWithMobileScanThisCode')),
- h('.div.qr-wrapper', {
- style: {
- display: 'flex',
- justifyContent: 'center',
- },
- dangerouslySetInnerHTML: {
- __html: qrImage.createTableTag(4),
- },
- }),
- ])
- )
- }
-
- renderFooter () {
- return this.state.screen === PASSWORD_PROMPT_SCREEN
- ? this.renderPasswordPromptFooter()
- : this.renderRevealSeedFooter()
- }
-
- renderPasswordPromptFooter () {
- return (
- h('div.new-account-import-form__buttons', {style: {padding: 30}}, [
-
- h(Button, {
- type: 'default',
- large: true,
- className: 'new-account-create-form__button',
- onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, this.context.t('cancel')),
-
- h(Button, {
- type: 'primary',
- large: true,
- className: 'new-account-create-form__button',
- onClick: event => this.handleSubmit(event),
- disabled: this.state.password === '',
- }, this.context.t('next')),
- ])
- )
- }
-
- renderRevealSeedFooter () {
- return (
- h('.page-container__footer', {style: {padding: 30}}, [
- h(Button, {
- type: 'default',
- large: true,
- className: 'page-container__footer-button',
- onClick: () => this.props.history.push(DEFAULT_ROUTE),
- }, this.context.t('close')),
- ])
- )
- }
-
- render () {
- return (
- h('.page-container', [
- h('.page-container__header', [
- h('.page-container__title', this.context.t('syncWithMobileTitle')),
- this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDesc')) : null,
- this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDescNewUsers')) : null,
- ]),
- h('.page-container__content', [
- this.renderContent(),
- ]),
- this.renderFooter(),
- ])
- )
- }
-}
-
-MobileSyncPage.propTypes = {
- requestRevealSeedWords: PropTypes.func,
- fetchInfoToSync: PropTypes.func,
- history: PropTypes.object,
-}
-
-MobileSyncPage.contextTypes = {
- t: PropTypes.func,
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
- fetchInfoToSync: () => dispatch(fetchInfoToSync()),
- displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
- }
-
-}
-
-const mapStateToProps = state => {
- const {
- metamask: { selectedAddress },
- } = state
-
- return {
- selectedAddress,
- }
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(MobileSyncPage)
diff --git a/ui/app/components/pages/notice.js b/ui/app/components/pages/notice.js
deleted file mode 100644
index a9077b98b..000000000
--- a/ui/app/components/pages/notice.js
+++ /dev/null
@@ -1,203 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const ReactMarkdown = require('react-markdown')
-const linker = require('extension-link-enabler')
-const generateLostAccountsNotice = require('../../../lib/lost-accounts-notice')
-const findDOMNode = require('react-dom').findDOMNode
-const actions = require('../../actions')
-const { DEFAULT_ROUTE } = require('../../routes')
-
-class Notice extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- disclaimerDisabled: true,
- }
- }
-
- componentWillMount () {
- if (!this.props.notice) {
- this.props.history.push(DEFAULT_ROUTE)
- }
- }
-
- componentDidMount () {
- // eslint-disable-next-line react/no-find-dom-node
- var node = findDOMNode(this)
- linker.setupListener(node)
- if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
- this.setState({ disclaimerDisabled: false })
- }
- }
-
- componentWillReceiveProps (nextProps) {
- if (!nextProps.notice) {
- this.props.history.push(DEFAULT_ROUTE)
- }
- }
-
- componentWillUnmount () {
- // eslint-disable-next-line react/no-find-dom-node
- var node = findDOMNode(this)
- linker.teardownListener(node)
- }
-
- handleAccept () {
- this.setState({ disclaimerDisabled: true })
- this.props.onConfirm()
- }
-
- render () {
- const { notice = {} } = this.props
- const { title, date, body } = notice
- const { disclaimerDisabled } = this.state
-
- return (
- h('.flex-column.flex-center.flex-grow', {
- style: {
- width: '100%',
- },
- }, [
- h('h3.flex-center.text-transform-uppercase.terms-header', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- width: '100%',
- fontSize: '20px',
- textAlign: 'center',
- padding: 6,
- },
- }, [
- title,
- ]),
-
- h('h5.flex-center.text-transform-uppercase.terms-header', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginBottom: 24,
- width: '100%',
- fontSize: '20px',
- textAlign: 'center',
- padding: 6,
- },
- }, [
- date,
- ]),
-
- h('style', `
-
- .markdown {
- overflow-x: hidden;
- }
-
- .markdown h1, .markdown h2, .markdown h3 {
- margin: 10px 0;
- font-weight: bold;
- }
-
- .markdown strong {
- font-weight: bold;
- }
- .markdown em {
- font-style: italic;
- }
-
- .markdown p {
- margin: 10px 0;
- }
-
- .markdown a {
- color: #df6b0e;
- }
-
- `),
-
- h('div.markdown', {
- onScroll: (e) => {
- var object = e.currentTarget
- if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) {
- this.setState({ disclaimerDisabled: false })
- }
- },
- style: {
- background: 'rgb(235, 235, 235)',
- height: '310px',
- padding: '6px',
- width: '90%',
- overflowY: 'scroll',
- scroll: 'auto',
- },
- }, [
- h(ReactMarkdown, {
- className: 'notice-box',
- source: body,
- skipHtml: true,
- }),
- ]),
-
- h('button.primary', {
- disabled: disclaimerDisabled,
- onClick: () => this.handleAccept(),
- style: {
- marginTop: '18px',
- },
- }, 'Accept'),
- ])
- )
- }
-
-}
-
-const mapStateToProps = state => {
- const { metamask } = state
- const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask
-
- return {
- noActiveNotices,
- nextUnreadNotice,
- lostAccounts,
- }
-}
-
-Notice.propTypes = {
- notice: PropTypes.object,
- onConfirm: PropTypes.func,
- history: PropTypes.object,
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)),
- markAccountsFound: () => dispatch(actions.markAccountsFound()),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps
- const { markNoticeRead, markAccountsFound } = dispatchProps
-
- let notice
- let onConfirm
-
- if (!noActiveNotices) {
- notice = nextUnreadNotice
- onConfirm = () => markNoticeRead(nextUnreadNotice)
- } else if (lostAccounts && lostAccounts.length > 0) {
- notice = generateLostAccountsNotice(lostAccounts)
- onConfirm = () => markAccountsFound()
- }
-
- return {
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- notice,
- onConfirm,
- }
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notice)
diff --git a/ui/app/components/pages/provider-approval/provider-approval.component.js b/ui/app/components/pages/provider-approval/provider-approval.component.js
deleted file mode 100644
index 11895327a..000000000
--- a/ui/app/components/pages/provider-approval/provider-approval.component.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { Component } from 'react'
-import ProviderPageContainer from '../../provider-page-container'
-
-export default class ProviderApproval extends Component {
- static propTypes = {
- approveProviderRequest: PropTypes.func.isRequired,
- providerRequest: PropTypes.object.isRequired,
- rejectProviderRequest: PropTypes.func.isRequired,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props
- return (
- <ProviderPageContainer
- approveProviderRequest={approveProviderRequest}
- origin={providerRequest.origin}
- tabID={providerRequest.tabID}
- rejectProviderRequest={rejectProviderRequest}
- siteImage={providerRequest.siteImage}
- siteTitle={providerRequest.siteTitle}
- />
- )
- }
-}
diff --git a/ui/app/components/pages/provider-approval/provider-approval.container.js b/ui/app/components/pages/provider-approval/provider-approval.container.js
deleted file mode 100644
index 28e4531a9..000000000
--- a/ui/app/components/pages/provider-approval/provider-approval.container.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux'
-import ProviderApproval from './provider-approval.component'
-import { approveProviderRequest, rejectProviderRequest } from '../../../actions'
-
-function mapDispatchToProps (dispatch) {
- return {
- approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)),
- rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)),
- }
-}
-
-export default connect(null, mapDispatchToProps)(ProviderApproval)
diff --git a/ui/app/components/pages/settings/index.scss b/ui/app/components/pages/settings/index.scss
deleted file mode 100644
index 138ebcfc5..000000000
--- a/ui/app/components/pages/settings/index.scss
+++ /dev/null
@@ -1,80 +0,0 @@
-@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/info-tab.component.js b/ui/app/components/pages/settings/info-tab/info-tab.component.js
deleted file mode 100644
index 72f7d835e..000000000
--- a/ui/app/components/pages/settings/info-tab/info-tab.component.js
+++ /dev/null
@@ -1,136 +0,0 @@
-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/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
deleted file mode 100644
index abddaaee8..000000000
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
+++ /dev/null
@@ -1,679 +0,0 @@
-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, MOBILE_SYNC_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,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- metamask: PropTypes.object,
- setUseBlockie: PropTypes.func,
- setHexDataFeatureFlag: PropTypes.func,
- setPrivacyMode: PropTypes.func,
- privacyMode: PropTypes.bool,
- setCurrentCurrency: PropTypes.func,
- setRpcTarget: PropTypes.func,
- delRpcTarget: PropTypes.func,
- displayWarning: PropTypes.func,
- revealSeedConfirmation: PropTypes.func,
- setFeatureFlagToBeta: PropTypes.func,
- showClearApprovalModal: PropTypes.func,
- showResetAccountConfirmationModal: PropTypes.func,
- warning: PropTypes.string,
- history: PropTypes.object,
- updateCurrentLocale: PropTypes.func,
- currentLocale: PropTypes.string,
- useBlockie: PropTypes.bool,
- sendHexData: PropTypes.bool,
- currentCurrency: PropTypes.string,
- conversionDate: PropTypes.number,
- nativeCurrency: PropTypes.string,
- useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
- setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
- setAdvancedInlineGasFeatureFlag: PropTypes.func,
- advancedInlineGas: PropTypes.bool,
- mobileSync: PropTypes.bool,
- showFiatInTestnets: PropTypes.bool,
- setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
- participateInMetaMetrics: PropTypes.bool,
- setParticipateInMetaMetrics: PropTypes.func,
- }
-
- state = {
- newRpc: '',
- chainId: '',
- showOptions: false,
- ticker: '',
- nickname: '',
- }
-
- 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('currencyConversion') }</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, chainId, ticker, nickname } = this.state
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('newNetwork') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <TextField
- type="text"
- id="new-rpc"
- placeholder={t('rpcURL')}
- value={newRpc}
- onChange={e => this.setState({ newRpc: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="chainid"
- placeholder={t('optionalChainId')}
- value={chainId}
- onChange={e => this.setState({ chainId: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="ticker"
- placeholder={t('optionalSymbol')}
- value={ticker}
- onChange={e => this.setState({ ticker: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="nickname"
- placeholder={t('optionalNickname')}
- value={nickname}
- onChange={e => this.setState({ nickname: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <div className="flex-row flex-align-center space-between">
- <span className="settings-tab__advanced-link"
- onClick={e => {
- e.preventDefault()
- this.setState({ showOptions: !this.state.showOptions })
- }}
- >
- { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') }
- </span>
- <button
- className="button btn-primary settings-tab__rpc-save-button"
- onClick={e => {
- e.preventDefault()
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }}
- >
- { t('save') }
- </button>
- </div>
- </div>
- </div>
- </div>
- )
- }
-
- validateRpc (newRpc, chainId, ticker = 'ETH', nickname) {
- const { setRpcTarget, displayWarning } = this.props
- if (validUrl.isWebUri(newRpc)) {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Custom RPC',
- name: 'Success',
- },
- customVariables: {
- networkId: newRpc,
- chainId,
- },
- })
- if (!!chainId && Number.isNaN(parseInt(chainId))) {
- return displayWarning(`${this.context.t('invalidInput')} chainId`)
- }
-
- setRpcTarget(newRpc, chainId, ticker, nickname)
- } else {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Custom RPC',
- name: 'Error',
- },
- customVariables: {
- networkId: newRpc,
- chainId,
- },
- })
- 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>
- )
- }
-
- renderClearApproval () {
- const { t } = this.context
- const { showClearApprovalModal } = this.props
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('approvalData') }</span>
- <span className="settings-page__content-description">
- { t('approvalDataDescription') }
- </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()
- showClearApprovalModal()
- }}
- >
- { t('clearApprovalData') }
- </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()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Reveal Seed Phrase',
- name: 'Reveal Seed Phrase',
- },
- })
- history.push(REVEAL_SEED_ROUTE)
- }}
- >
- { t('revealSeedWords') }
- </Button>
- </div>
- </div>
- </div>
- )
- }
-
-
- renderMobileSync () {
- const { t } = this.context
- const { history, mobileSync } = this.props
-
- if (!mobileSync) {
- return
- }
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('syncWithMobile') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <Button
- type="primary"
- large
- onClick={event => {
- event.preventDefault()
- history.push(MOBILE_SYNC_ROUTE)
- }}
- >
- { t('syncWithMobile') }
- </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()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Reset Account',
- name: 'Reset Account',
- },
- })
- 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>
- )
- }
-
- renderAdvancedGasInputInline () {
- const { t } = this.context
- const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('showAdvancedGasInline') }</span>
- <div className="settings-page__content-description">
- { t('showAdvancedGasInlineDescription') }
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={advancedInlineGas}
- onToggle={value => setAdvancedInlineGasFeatureFlag(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderUsePrimaryCurrencyOptions () {
- const { t } = this.context
- const {
- nativeCurrency,
- setUseNativeCurrencyAsPrimaryCurrencyPreference,
- useNativeCurrencyAsPrimaryCurrency,
- } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('primaryCurrencySetting') }</span>
- <div className="settings-page__content-description">
- { t('primaryCurrencySettingDescription') }
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <div className="settings-tab__radio-buttons">
- <div className="settings-tab__radio-button">
- <input
- type="radio"
- id="native-primary-currency"
- onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)}
- checked={Boolean(useNativeCurrencyAsPrimaryCurrency)}
- />
- <label
- htmlFor="native-primary-currency"
- className="settings-tab__radio-label"
- >
- { nativeCurrency }
- </label>
- </div>
- <div className="settings-tab__radio-button">
- <input
- type="radio"
- id="fiat-primary-currency"
- onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)}
- checked={!useNativeCurrencyAsPrimaryCurrency}
- />
- <label
- htmlFor="fiat-primary-currency"
- className="settings-tab__radio-label"
- >
- { t('fiat') }
- </label>
- </div>
- </div>
- </div>
- </div>
- </div>
- )
- }
-
- renderShowConversionInTestnets () {
- const { t } = this.context
- const {
- showFiatInTestnets,
- setShowFiatConversionOnTestnetsPreference,
- } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('showFiatConversionInTestnets') }</span>
- <div className="settings-page__content-description">
- { t('showFiatConversionInTestnetsDescription') }
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={showFiatInTestnets}
- onToggle={value => setShowFiatConversionOnTestnetsPreference(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderPrivacyOptIn () {
- const { t } = this.context
- const { privacyMode, setPrivacyMode } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('privacyMode') }</span>
- <div className="settings-page__content-description">
- { t('privacyModeDescription') }
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={privacyMode}
- onToggle={value => setPrivacyMode(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderMetaMetricsOptIn () {
- const { t } = this.context
- const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('participateInMetaMetrics') }</span>
- <div className="settings-page__content-description">
- <span>{ t('participateInMetaMetricsDescription') }</span>
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={participateInMetaMetrics}
- onToggle={value => setParticipateInMetaMetrics(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- render () {
- const { warning } = this.props
-
- return (
- <div className="settings-page__content">
- { warning && <div className="settings-tab__error">{ warning }</div> }
- { this.renderCurrentConversion() }
- { this.renderUsePrimaryCurrencyOptions() }
- { this.renderShowConversionInTestnets() }
- { this.renderCurrentLocale() }
- { this.renderNewRpcUrl() }
- { this.renderStateLogs() }
- { this.renderSeedWords() }
- { this.renderResetAccount() }
- { this.renderClearApproval() }
- { this.renderPrivacyOptIn() }
- { this.renderHexDataOptIn() }
- { this.renderAdvancedGasInputInline() }
- { this.renderBlockieOptIn() }
- { this.renderMobileSync() }
- { this.renderMetaMetricsOptIn() }
- </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
deleted file mode 100644
index 64c256412..000000000
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import SettingsTab from './settings-tab.component'
-import { compose } from 'recompose'
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import {
- setCurrentCurrency,
- updateAndSetCustomRpc,
- displayWarning,
- revealSeedConfirmation,
- setUseBlockie,
- updateCurrentLocale,
- setFeatureFlag,
- showModal,
- setUseNativeCurrencyAsPrimaryCurrencyPreference,
- setShowFiatConversionOnTestnetsPreference,
- setParticipateInMetaMetrics,
-} from '../../../../actions'
-import { preferencesSelector } from '../../../../selectors'
-
-const mapStateToProps = state => {
- const { appState: { warning }, metamask } = state
- const {
- currentCurrency,
- conversionDate,
- nativeCurrency,
- useBlockie,
- featureFlags: {
- sendHexData,
- privacyMode,
- advancedInlineGas,
- mobileSync,
- } = {},
- provider = {},
- currentLocale,
- participateInMetaMetrics,
- } = metamask
- const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets } = preferencesSelector(state)
-
- return {
- warning,
- currentLocale,
- currentCurrency,
- conversionDate,
- nativeCurrency,
- useBlockie,
- sendHexData,
- advancedInlineGas,
- privacyMode,
- provider,
- useNativeCurrencyAsPrimaryCurrency,
- mobileSync,
- showFiatInTestnets,
- participateInMetaMetrics,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)),
- setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)),
- displayWarning: warning => dispatch(displayWarning(warning)),
- revealSeedConfirmation: () => dispatch(revealSeedConfirmation()),
- setUseBlockie: value => dispatch(setUseBlockie(value)),
- updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
- setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
- setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
- setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)),
- showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
- setUseNativeCurrencyAsPrimaryCurrencyPreference: value => {
- return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value))
- },
- setShowFiatConversionOnTestnetsPreference: value => {
- return dispatch(setShowFiatConversionOnTestnetsPreference(value))
- },
- showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })),
- setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
- }
-}
-
-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
deleted file mode 100644
index 94a97bba1..000000000
--- a/ui/app/components/pages/settings/settings.component.js
+++ /dev/null
@@ -1,54 +0,0 @@
-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/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
deleted file mode 100644
index cc86d5872..000000000
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ /dev/null
@@ -1,191 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import Button from '@material-ui/core/Button'
-import TextField from '../../text-field'
-import getCaretCoordinates from 'textarea-caret'
-import { EventEmitter } from 'events'
-import Mascot from '../../mascot'
-import { DEFAULT_ROUTE } from '../../../routes'
-
-export default class UnlockPage extends Component {
- static contextTypes = {
- metricsEvent: PropTypes.func,
- t: PropTypes.func,
- }
-
- static propTypes = {
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- onImport: PropTypes.func,
- onRestore: PropTypes.func,
- onSubmit: PropTypes.func,
- forceUpdateMetamaskState: PropTypes.func,
- showOptInModal: PropTypes.func,
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- password: '',
- error: null,
- }
-
- this.submitting = false
- this.animationEventEmitter = new EventEmitter()
- }
-
- componentWillMount () {
- const { isUnlocked, history } = this.props
-
- if (isUnlocked) {
- history.push(DEFAULT_ROUTE)
- }
- }
-
- handleSubmit = async event => {
- event.preventDefault()
- event.stopPropagation()
-
- const { password } = this.state
- const { onSubmit, forceUpdateMetamaskState, showOptInModal } = this.props
-
- if (password === '' || this.submitting) {
- return
- }
-
- this.setState({ error: null })
- this.submitting = true
-
- try {
- await onSubmit(password)
- const newState = await forceUpdateMetamaskState()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Unlock',
- name: 'Success',
- },
- isNewVisit: true,
- })
-
- if (newState.participateInMetaMetrics === null || newState.participateInMetaMetrics === undefined) {
- showOptInModal()
- }
- } catch ({ message }) {
- if (message === 'Incorrect password') {
- const newState = await forceUpdateMetamaskState()
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Unlock',
- name: 'Incorrect Passowrd',
- },
- customVariables: {
- numberOfTokens: newState.tokens.length,
- numberOfAccounts: Object.keys(newState.accounts).length,
- },
- })
- }
-
- this.setState({ error: message })
- this.submitting = false
- }
- }
-
- handleInputChange ({ target }) {
- this.setState({ password: target.value, error: null })
-
- // tell mascot to look at page action
- const element = target
- const boundingRect = element.getBoundingClientRect()
- const coordinates = getCaretCoordinates(element, element.selectionEnd)
- this.animationEventEmitter.emit('point', {
- x: boundingRect.left + coordinates.left - element.scrollLeft,
- y: boundingRect.top + coordinates.top - element.scrollTop,
- })
- }
-
- renderSubmitButton () {
- const style = {
- backgroundColor: '#f7861c',
- color: 'white',
- marginTop: '20px',
- height: '60px',
- fontWeight: '400',
- boxShadow: 'none',
- borderRadius: '4px',
- }
-
- return (
- <Button
- type="submit"
- style={style}
- disabled={!this.state.password}
- fullWidth
- variant="raised"
- size="large"
- onClick={this.handleSubmit}
- disableRipple
- >
- { this.context.t('login') }
- </Button>
- )
- }
-
- render () {
- const { password, error } = this.state
- const { t } = this.context
- const { onImport, onRestore } = this.props
-
- return (
- <div className="unlock-page__container">
- <div className="unlock-page">
- <div className="unlock-page__mascot-container">
- <Mascot
- animationEventEmitter={this.animationEventEmitter}
- width="120"
- height="120"
- />
- </div>
- <h1 className="unlock-page__title">
- { t('welcomeBack') }
- </h1>
- <div>{ t('unlockMessage') }</div>
- <form
- className="unlock-page__form"
- onSubmit={this.handleSubmit}
- >
- <TextField
- id="password"
- label={t('password')}
- type="password"
- value={password}
- onChange={event => this.handleInputChange(event)}
- error={error}
- autoFocus
- autoComplete="current-password"
- material
- fullWidth
- />
- </form>
- { this.renderSubmitButton() }
- <div className="unlock-page__links">
- <div
- className="unlock-page__link"
- onClick={() => onRestore()}
- >
- { t('restoreFromSeed') }
- </div>
- <div
- className="unlock-page__link unlock-page__link--import"
- onClick={() => onImport()}
- >
- { t('importUsingSeed') }
- </div>
- </div>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js
deleted file mode 100644
index fe51c8095..000000000
--- a/ui/app/components/pages/unlock-page/unlock-page.container.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
-import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
-import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
-import {
- tryUnlockMetamask,
- forgotPassword,
- markPasswordForgotten,
- forceUpdateMetamaskState,
- showModal,
-} from '../../../actions'
-import UnlockPage from './unlock-page.component'
-
-const mapStateToProps = state => {
- const { metamask: { isUnlocked } } = state
- return {
- isUnlocked,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- forgotPassword: () => dispatch(forgotPassword()),
- tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
- markPasswordForgotten: () => dispatch(markPasswordForgotten()),
- forceUpdateMetamaskState: () => forceUpdateMetamaskState(dispatch),
- showOptInModal: () => dispatch(showModal({ name: 'METAMETRICS_OPT_IN_MODAL' })),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { markPasswordForgotten, tryUnlockMetamask, ...restDispatchProps } = dispatchProps
- const { history, onSubmit: ownPropsSubmit, ...restOwnProps } = ownProps
-
- const onImport = () => {
- markPasswordForgotten()
- history.push(RESTORE_VAULT_ROUTE)
-
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
- }
-
- const onSubmit = async password => {
- await tryUnlockMetamask(password)
- history.push(DEFAULT_ROUTE)
- }
-
- return {
- ...stateProps,
- ...restDispatchProps,
- ...restOwnProps,
- onImport,
- onRestore: onImport,
- onSubmit: ownPropsSubmit || onSubmit,
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps, mergeProps)
-)(UnlockPage)
diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
deleted file mode 100644
index 268db613f..000000000
--- a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import PropTypes from 'prop-types'
-import React, {PureComponent} from 'react'
-import Identicon from '../../identicon'
-
-export default class ProviderPageContainerContent extends PureComponent {
- static propTypes = {
- origin: PropTypes.string.isRequired,
- selectedIdentity: PropTypes.string.isRequired,
- siteImage: PropTypes.string,
- siteTitle: PropTypes.string.isRequired,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- renderConnectVisual = () => {
- const { origin, selectedIdentity, siteImage, siteTitle } = this.props
-
- return (
- <div className="provider-approval-visual">
- <section>
- {siteImage ? (
- <img
- className="provider-approval-visual__identicon"
- src={siteImage}
- />
- ) : (
- <i className="provider-approval-visual__identicon--default">
- {siteTitle.charAt(0).toUpperCase()}
- </i>
- )}
- <h1>{siteTitle}</h1>
- <h2>{origin}</h2>
- </section>
- <span className="provider-approval-visual__check" />
- <section>
- <Identicon
- className="provider-approval-visual__identicon"
- address={selectedIdentity.address}
- diameter={64}
- />
- <h1>{selectedIdentity.name}</h1>
- </section>
- </div>
- )
- }
-
- render () {
- const { siteTitle } = this.props
- const { t } = this.context
-
- return (
- <div className="provider-approval-container__content">
- <section>
- <h2>{t('connectRequest')}</h2>
- {this.renderConnectVisual()}
- <h1>{t('providerRequest', [siteTitle])}</h1>
- <p>
- {t('providerRequestInfo')}
- <br/>
- <a
- href="https://medium.com/metamask/introducing-privacy-mode-42549d4870fa"
- target="_blank"
- rel="noopener noreferrer"
- >
- {t('learnMore')}.
- </a>
- </p>
- </section>
- <section className="secure-badge">
- <img src="/images/mm-secure.svg" />
- </section>
- </div>
- )
- }
-}
diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
deleted file mode 100644
index 3ea1ce20e..000000000
--- a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { connect } from 'react-redux'
-import ProviderPageContainerContent from './provider-page-container-content.component'
-import { getSelectedIdentity } from '../../../selectors'
-
-const mapStateToProps = (state) => {
- return {
- selectedIdentity: getSelectedIdentity(state),
- }
-}
-
-export default connect(mapStateToProps)(ProviderPageContainerContent)
diff --git a/ui/app/components/provider-page-container/provider-page-container.component.js b/ui/app/components/provider-page-container/provider-page-container.component.js
deleted file mode 100644
index ff063166d..000000000
--- a/ui/app/components/provider-page-container/provider-page-container.component.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import PropTypes from 'prop-types'
-import React, {PureComponent} from 'react'
-import { ProviderPageContainerContent, ProviderPageContainerHeader } from './'
-import { PageContainerFooter } from '../page-container'
-
-export default class ProviderPageContainer extends PureComponent {
- static propTypes = {
- approveProviderRequest: PropTypes.func.isRequired,
- origin: PropTypes.string.isRequired,
- rejectProviderRequest: PropTypes.func.isRequired,
- siteImage: PropTypes.string,
- siteTitle: PropTypes.string.isRequired,
- tabID: PropTypes.string.isRequired,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- };
-
- componentDidMount () {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Popup Opened',
- },
- })
- }
-
- onCancel = () => {
- const { tabID, rejectProviderRequest } = this.props
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Canceled',
- },
- })
- rejectProviderRequest(tabID)
- }
-
- onSubmit = () => {
- const { approveProviderRequest, tabID } = this.props
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Confirmed',
- },
- })
- approveProviderRequest(tabID)
- }
-
- render () {
- const {origin, siteImage, siteTitle} = this.props
-
- return (
- <div className="page-container provider-approval-container">
- <ProviderPageContainerHeader />
- <ProviderPageContainerContent
- origin={origin}
- siteImage={siteImage}
- siteTitle={siteTitle}
- />
- <PageContainerFooter
- onCancel={() => this.onCancel()}
- cancelText={this.context.t('cancel')}
- onSubmit={() => this.onSubmit()}
- submitText={this.context.t('connect')}
- submitButtonType="confirm"
- />
- </div>
- )
- }
-}
diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js
deleted file mode 100644
index 312815891..000000000
--- a/ui/app/components/qr-code.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const qrCode = require('qrcode-generator')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const { isHexPrefixed } = require('ethereumjs-util')
-const ReadOnlyInput = require('./readonly-input')
-const { checksumAddress } = require('../util')
-
-module.exports = connect(mapStateToProps)(QrCodeView)
-
-function mapStateToProps (state) {
- return {
- // Qr code is not fetched from state. 'message' and 'data' props are passed instead.
- buyView: state.appState.buyView,
- warning: state.appState.warning,
- }
-}
-
-inherits(QrCodeView, Component)
-
-function QrCodeView () {
- Component.call(this)
-}
-
-QrCodeView.prototype.render = function () {
- const props = this.props
- const { message, data, network } = props.Qr
- const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data, network)}`
- const qrImage = qrCode(4, 'M')
- qrImage.addData(address)
- qrImage.make()
-
- return h('.div.flex-column.flex-center', [
- Array.isArray(message)
- ? h('.message-container', this.renderMultiMessage())
- : message && h('.qr-header', message),
-
- this.props.warning ? this.props.warning && h('span.error.flex-center', {
- style: {
- },
- },
- this.props.warning) : null,
-
- h('.div.qr-wrapper', {
- style: {},
- dangerouslySetInnerHTML: {
- __html: qrImage.createTableTag(4),
- },
- }),
- h(ReadOnlyInput, {
- wrapperClass: 'ellip-address-wrapper',
- inputClass: 'qr-ellip-address',
- value: checksumAddress(data, network),
- }),
- ])
-}
-
-QrCodeView.prototype.renderMultiMessage = function () {
- var Qr = this.props.Qr
- var multiMessage = Qr.message.map((message) => h('.qr-message', message))
- return multiMessage
-}
diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js
deleted file mode 100644
index 47c56e312..000000000
--- a/ui/app/components/selected-account/selected-account.component.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import copyToClipboard from 'copy-to-clipboard'
-import { addressSlicer, checksumAddress } from '../../util'
-
-const Tooltip = require('../tooltip-v2.js').default
-
-class SelectedAccount extends Component {
- state = {
- copied: false,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- selectedAddress: PropTypes.string,
- selectedIdentity: PropTypes.object,
- network: PropTypes.string,
- }
-
- render () {
- const { t } = this.context
- const { selectedAddress, selectedIdentity, network } = this.props
- const checksummedAddress = checksumAddress(selectedAddress, network)
-
- return (
- <div className="selected-account">
- <Tooltip
- position="bottom"
- title={this.state.copied ? t('copiedExclamation') : t('copyToClipboard')}
- >
- <div
- className="selected-account__clickable"
- onClick={() => {
- this.setState({ copied: true })
- setTimeout(() => this.setState({ copied: false }), 3000)
- copyToClipboard(checksummedAddress)
- }}
- >
- <div className="selected-account__name">
- { selectedIdentity.name }
- </div>
- <div className="selected-account__address">
- { addressSlicer(checksummedAddress) }
- </div>
- </div>
- </Tooltip>
- </div>
- )
- }
-}
-
-export default SelectedAccount
diff --git a/ui/app/components/selected-account/selected-account.container.js b/ui/app/components/selected-account/selected-account.container.js
deleted file mode 100644
index 1c8d17d8d..000000000
--- a/ui/app/components/selected-account/selected-account.container.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { connect } from 'react-redux'
-import SelectedAccount from './selected-account.component'
-
-const selectors = require('../../selectors')
-
-const mapStateToProps = state => {
- return {
- selectedAddress: selectors.getSelectedAddress(state),
- selectedIdentity: selectors.getSelectedIdentity(state),
- network: state.metamask.network,
- }
-}
-
-export default connect(mapStateToProps)(SelectedAccount)
diff --git a/ui/app/components/send/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js
deleted file mode 100644
index 0420af46b..000000000
--- a/ui/app/components/send/account-list-item/account-list-item.component.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { checksumAddress } from '../../../util'
-import Identicon from '../../identicon'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../../constants/common'
-import Tooltip from '../../tooltip-v2'
-
-export default class AccountListItem extends Component {
-
- static propTypes = {
- account: PropTypes.object,
- className: PropTypes.string,
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- displayAddress: PropTypes.bool,
- displayBalance: PropTypes.bool,
- handleClick: PropTypes.func,
- icon: PropTypes.node,
- balanceIsCached: PropTypes.bool,
- showFiat: PropTypes.bool,
- };
-
- static defaultProps = {
- showFiat: true,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- account,
- className,
- displayAddress = false,
- displayBalance = true,
- handleClick,
- icon = null,
- balanceIsCached,
- showFiat,
- } = this.props
-
- const { name, address, balance } = account || {}
-
- return (<div
- className={`account-list-item ${className}`}
- onClick={() => handleClick && handleClick({ name, address, balance })}
- >
-
- <div className="account-list-item__top-row">
- <Identicon
- address={address}
- className="account-list-item__identicon"
- diameter={18}
- />
-
- <div className="account-list-item__account-name">{ name || address }</div>
-
- {icon && <div className="account-list-item__icon">{ icon }</div>}
-
- </div>
-
- {displayAddress && name && <div className="account-list-item__account-address">
- { checksumAddress(address) }
- </div>}
-
- {
- displayBalance && (
- <Tooltip
- position="left"
- title={this.context.t('balanceOutdated')}
- disabled={!balanceIsCached}
- style={{
- left: '-20px !important',
- }}
- >
- <div className={classnames('account-list-item__account-balances', {
- 'account-list-item__cached-balances': balanceIsCached,
- })}>
- <div className="account-list-item__primary-cached-container">
- <UserPreferencedCurrencyDisplay
- type={PRIMARY}
- value={balance}
- hideTitle={true}
- />
- {
- balanceIsCached ? <span className="account-list-item__cached-star">*</span> : null
- }
- </div>
- {
- showFiat && (
- <UserPreferencedCurrencyDisplay
- type={SECONDARY}
- value={balance}
- hideTitle={true}
- />
- )
- }
- </div>
- </Tooltip>
- )
- }
-
- </div>)
- }
-}
diff --git a/ui/app/components/send/account-list-item/account-list-item.container.js b/ui/app/components/send/account-list-item/account-list-item.container.js
deleted file mode 100644
index c045ef14f..000000000
--- a/ui/app/components/send/account-list-item/account-list-item.container.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getConversionRate,
- getCurrentCurrency,
- getNativeCurrency,
-} from '../send.selectors.js'
-import {
- getIsMainnet,
- isBalanceCached,
- preferencesSelector,
-} from '../../../selectors'
-import AccountListItem from './account-list-item.component'
-
-export default connect(mapStateToProps)(AccountListItem)
-
-function mapStateToProps (state) {
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
-
- return {
- conversionRate: getConversionRate(state),
- currentCurrency: getCurrentCurrency(state),
- nativeCurrency: getNativeCurrency(state),
- balanceIsCached: isBalanceCached(state),
- showFiat: (isMainnet || !!showFiatInTestnets),
- }
-}
diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
deleted file mode 100644
index 2bd2ce0c5..000000000
--- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
+++ /dev/null
@@ -1,148 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import proxyquire from 'proxyquire'
-import Identicon from '../../../identicon'
-import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
-
-const utilsMethodStubs = {
- checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
-}
-
-const AccountListItem = proxyquire('../account-list-item.component.js', {
- '../../../util': utilsMethodStubs,
-}).default
-
-
-const propsMethodSpies = {
- handleClick: sinon.spy(),
-}
-
-describe('AccountListItem Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<AccountListItem
- account={ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' } }
- className={'mockClassName'}
- conversionRate={4}
- currentCurrency={'mockCurrentyCurrency'}
- nativeCurrency={'ETH'}
- displayAddress={false}
- displayBalance={false}
- handleClick={propsMethodSpies.handleClick}
- icon={<i className="mockIcon" />}
- />, { context: { t: str => str + '_t' } })
- })
-
- afterEach(() => {
- propsMethodSpies.handleClick.resetHistory()
- })
-
- describe('render', () => {
- it('should render a div with the passed className', () => {
- assert.equal(wrapper.find('.mockClassName').length, 1)
- assert(wrapper.find('.mockClassName').is('div'))
- assert(wrapper.find('.mockClassName').hasClass('account-list-item'))
- })
-
- it('should call handleClick with the expected props when the root div is clicked', () => {
- const { onClick } = wrapper.find('.mockClassName').props()
- assert.equal(propsMethodSpies.handleClick.callCount, 0)
- onClick()
- assert.equal(propsMethodSpies.handleClick.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.handleClick.getCall(0).args,
- [{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }]
- )
- })
-
- it('should have a top row div', () => {
- assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1)
- assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div'))
- })
-
- it('should have an identicon, name and icon in the top row', () => {
- const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
- assert.equal(topRow.find(Identicon).length, 1)
- assert.equal(topRow.find('.account-list-item__account-name').length, 1)
- assert.equal(topRow.find('.account-list-item__icon').length, 1)
- })
-
- it('should show the account name if it exists', () => {
- const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
- assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName')
- })
-
- it('should show the account address if there is no name', () => {
- wrapper.setProps({ account: { address: 'addressButNoName' } })
- const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
- assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName')
- })
-
- it('should render the passed icon', () => {
- const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
- assert(topRow.find('.account-list-item__icon').childAt(0).is('i'))
- assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon'))
- })
-
- it('should not render an icon if none is passed', () => {
- wrapper.setProps({ icon: null })
- const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
- assert.equal(topRow.find('.account-list-item__icon').length, 0)
- })
-
- it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => {
- wrapper.setProps({ displayAddress: true })
- assert.equal(wrapper.find('.account-list-item__account-address').length, 1)
- assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress')
- assert.deepEqual(
- utilsMethodStubs.checksumAddress.getCall(0).args,
- ['mockAddress']
- )
- })
-
- it('should not render the account address as a checksumAddress if displayAddress is false', () => {
- wrapper.setProps({ displayAddress: false })
- assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
- })
-
- it('should not render the account address as a checksumAddress if name is not provided', () => {
- wrapper.setProps({ account: { address: 'someAddressButNoName' } })
- assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
- })
-
- it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => {
- wrapper.setProps({ displayBalance: true })
- assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
- assert.deepEqual(
- wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(),
- {
- type: 'PRIMARY',
- value: 'mockBalance',
- hideTitle: true,
- }
- )
- })
-
- it('should only render one CurrencyDisplay if showFiat is false', () => {
- wrapper.setProps({ showFiat: false, displayBalance: true })
- assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 1)
- assert.deepEqual(
- wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(),
- {
- type: 'PRIMARY',
- value: 'mockBalance',
- hideTitle: true,
- }
- )
- })
-
- it('should not render a CurrencyDisplay if displayBalance is false', () => {
- wrapper.setProps({ displayBalance: false })
- assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0)
- })
-
- })
-})
diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
deleted file mode 100644
index 662880aa0..000000000
--- a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-let mapStateToProps
-
-proxyquire('../account-list-item.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- return () => ({})
- },
- },
- '../send.selectors.js': {
- getConversionRate: () => `mockConversionRate`,
- getCurrentCurrency: () => `mockCurrentCurrency`,
- getNativeCurrency: () => `mockNativeCurrency`,
- },
- '../../../selectors.js': {
- isBalanceCached: () => `mockBalanceIsCached`,
- preferencesSelector: ({ showFiatInTestnets }) => ({
- showFiatInTestnets,
- }),
- getIsMainnet: ({ isMainnet }) => isMainnet,
- },
-})
-
-describe('account-list-item container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: false }), {
- conversionRate: 'mockConversionRate',
- currentCurrency: 'mockCurrentCurrency',
- nativeCurrency: 'mockNativeCurrency',
- balanceIsCached: 'mockBalanceIsCached',
- showFiat: true,
- })
- })
-
- it('should map the correct properties to props when in mainnet and showFiatInTestnet is true', () => {
- assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: true }), {
- conversionRate: 'mockConversionRate',
- currentCurrency: 'mockCurrentCurrency',
- nativeCurrency: 'mockNativeCurrency',
- balanceIsCached: 'mockBalanceIsCached',
- showFiat: true,
- })
- })
-
- it('should map the correct properties to props when not in mainnet and showFiatInTestnet is true', () => {
- assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: true }), {
- conversionRate: 'mockConversionRate',
- currentCurrency: 'mockCurrentCurrency',
- nativeCurrency: 'mockNativeCurrency',
- balanceIsCached: 'mockBalanceIsCached',
- showFiat: true,
- })
- })
-
- it('should map the correct properties to props when not in mainnet and showFiatInTestnet is false', () => {
- assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: false }), {
- conversionRate: 'mockConversionRate',
- currentCurrency: 'mockCurrentCurrency',
- nativeCurrency: 'mockNativeCurrency',
- balanceIsCached: 'mockBalanceIsCached',
- showFiat: false,
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
deleted file mode 100644
index 2d2ec42f7..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getGasTotal,
- getSelectedToken,
- getSendFromBalance,
- getTokenBalance,
-} from '../../../send.selectors.js'
-import { getMaxModeOn } from './amount-max-button.selectors.js'
-import { calcMaxAmount } from './amount-max-button.utils.js'
-import {
- updateSendAmount,
- setMaxModeTo,
-} from '../../../../../actions'
-import AmountMaxButton from './amount-max-button.component'
-import {
- updateSendErrors,
-} from '../../../../../ducks/send.duck'
-
-export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
-
-function mapStateToProps (state) {
-
- return {
- balance: getSendFromBalance(state),
- gasTotal: getGasTotal(state),
- maxModeOn: getMaxModeOn(state),
- selectedToken: getSelectedToken(state),
- tokenBalance: getTokenBalance(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setAmountToMax: maxAmountDataObject => {
- dispatch(updateSendErrors({ amount: null }))
- dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
- },
- setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
- }
-}
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
deleted file mode 100644
index 27181d2f5..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const {
- multiplyCurrencies,
- subtractCurrencies,
-} = require('../../../../../conversion-util')
-const ethUtil = require('ethereumjs-util')
-
-function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) {
- const { decimals } = selectedToken || {}
- const multiplier = Math.pow(10, Number(decimals || 0))
-
- return selectedToken
- ? multiplyCurrencies(
- tokenBalance,
- multiplier,
- {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- }
- )
- : subtractCurrencies(
- ethUtil.addHexPrefix(balance),
- ethUtil.addHexPrefix(gasTotal),
- { toNumericBase: 'hex' }
- )
-}
-
-module.exports = {
- calcMaxAmount,
-}
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
deleted file mode 100644
index 2cc00d6d6..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- setMaxModeTo: sinon.spy(),
- updateSendAmount: sinon.spy(),
-}
-const duckActionSpies = {
- updateSendErrors: sinon.spy(),
-}
-
-proxyquire('../amount-max-button.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../../send.selectors.js': {
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getSelectedToken: (s) => `mockSelectedToken:${s}`,
- getSendFromBalance: (s) => `mockBalance:${s}`,
- getTokenBalance: (s) => `mockTokenBalance:${s}`,
- },
- './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` },
- './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
- '../../../../../actions': actionSpies,
- '../../../../../ducks/send.duck': duckActionSpies,
-})
-
-describe('amount-max-button container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- balance: 'mockBalance:mockState',
- gasTotal: 'mockGasTotal:mockState',
- maxModeOn: 'mockMaxModeOn:mockState',
- selectedToken: 'mockSelectedToken:mockState',
- tokenBalance: 'mockTokenBalance:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('setAmountToMax()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' })
- assert(dispatchSpy.calledTwice)
- assert(duckActionSpies.updateSendErrors.calledOnce)
- assert.deepEqual(
- duckActionSpies.updateSendErrors.getCall(0).args[0],
- { amount: null }
- )
- assert(actionSpies.updateSendAmount.calledOnce)
- assert.equal(
- actionSpies.updateSendAmount.getCall(0).args[0],
- 12
- )
- })
- })
-
- describe('setMaxModeTo()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.setMaxModeTo('mockVal')
- assert(dispatchSpy.calledOnce)
- assert.equal(
- actionSpies.setMaxModeTo.getCall(0).args[0],
- 'mockVal'
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
deleted file mode 100644
index 4df1e0ffa..000000000
--- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowWrapper from '../send-row-wrapper/'
-import AmountMaxButton from './amount-max-button/'
-import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input'
-import UserPreferencedTokenInput from '../../../user-preferenced-token-input'
-
-export default class SendAmountRow extends Component {
-
- static propTypes = {
- amount: PropTypes.string,
- amountConversionRate: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- balance: PropTypes.string,
- conversionRate: PropTypes.number,
- convertedCurrency: PropTypes.string,
- gasTotal: PropTypes.string,
- inError: PropTypes.bool,
- primaryCurrency: PropTypes.string,
- selectedToken: PropTypes.object,
- setMaxModeTo: PropTypes.func,
- tokenBalance: PropTypes.string,
- updateGasFeeError: PropTypes.func,
- updateSendAmount: PropTypes.func,
- updateSendAmountError: PropTypes.func,
- updateGas: PropTypes.func,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- validateAmount (amount) {
- const {
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- updateGasFeeError,
- updateSendAmountError,
- } = this.props
-
- updateSendAmountError({
- amount,
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- })
-
- if (selectedToken) {
- updateGasFeeError({
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- })
- }
- }
-
- updateAmount (amount) {
- const { updateSendAmount, setMaxModeTo } = this.props
-
- setMaxModeTo(false)
- updateSendAmount(amount)
- }
-
- updateGas (amount) {
- const { selectedToken, updateGas } = this.props
-
- if (selectedToken) {
- updateGas({ amount })
- }
- }
-
- renderInput () {
- const { amount, inError, selectedToken } = this.props
- const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput
-
- return (
- <Component
- onChange={newAmount => this.validateAmount(newAmount)}
- onBlur={newAmount => {
- this.updateGas(newAmount)
- this.updateAmount(newAmount)
- }}
- error={inError}
- value={amount}
- />
- )
- }
-
- render () {
- const { gasTotal, inError } = this.props
-
- return (
- <SendRowWrapper
- label={`${this.context.t('amount')}:`}
- showError={inError}
- errorType={'amount'}
- >
- {!inError && gasTotal && <AmountMaxButton />}
- { this.renderInput() }
- </SendRowWrapper>
- )
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
deleted file mode 100644
index 2b6fe0f6c..000000000
--- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getAmountConversionRate,
- getConversionRate,
- getCurrentCurrency,
- getGasTotal,
- getPrimaryCurrency,
- getSelectedToken,
- getSendAmount,
- getSendFromBalance,
- getTokenBalance,
-} from '../../send.selectors'
-import {
- sendAmountIsInError,
-} from './send-amount-row.selectors'
-import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils'
-import {
- setMaxModeTo,
- updateSendAmount,
-} from '../../../../actions'
-import {
- updateSendErrors,
-} from '../../../../ducks/send.duck'
-import SendAmountRow from './send-amount-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
-
-function mapStateToProps (state) {
- return {
- amount: getSendAmount(state),
- amountConversionRate: getAmountConversionRate(state),
- balance: getSendFromBalance(state),
- conversionRate: getConversionRate(state),
- convertedCurrency: getCurrentCurrency(state),
- gasTotal: getGasTotal(state),
- inError: sendAmountIsInError(state),
- primaryCurrency: getPrimaryCurrency(state),
- selectedToken: getSelectedToken(state),
- tokenBalance: getTokenBalance(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
- updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
- updateGasFeeError: (amountDataObject) => {
- dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject)))
- },
- updateSendAmountError: (amountDataObject) => {
- dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
- },
- }
-}
diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
deleted file mode 100644
index 52e351aee..000000000
--- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- setMaxModeTo: sinon.spy(),
- updateSendAmount: sinon.spy(),
-}
-const duckActionSpies = {
- updateSendErrors: sinon.spy(),
-}
-
-proxyquire('../send-amount-row.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../send.selectors': {
- getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
- getConversionRate: (s) => `mockConversionRate:${s}`,
- getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
- getSelectedToken: (s) => `mockSelectedToken:${s}`,
- getSendAmount: (s) => `mockAmount:${s}`,
- getSendFromBalance: (s) => `mockBalance:${s}`,
- getTokenBalance: (s) => `mockTokenBalance:${s}`,
- },
- './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
- '../../send.utils': {
- getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }),
- getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }),
- },
- '../../../../actions': actionSpies,
- '../../../../ducks/send.duck': duckActionSpies,
-})
-
-describe('send-amount-row container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- amount: 'mockAmount:mockState',
- amountConversionRate: 'mockAmountConversionRate:mockState',
- balance: 'mockBalance:mockState',
- conversionRate: 'mockConversionRate:mockState',
- convertedCurrency: 'mockConvertedCurrency:mockState',
- gasTotal: 'mockGasTotal:mockState',
- inError: 'mockInError:mockState',
- primaryCurrency: 'mockPrimaryCurrency:mockState',
- selectedToken: 'mockSelectedToken:mockState',
- tokenBalance: 'mockTokenBalance:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- duckActionSpies.updateSendErrors.resetHistory()
- })
-
- describe('setMaxModeTo()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.setMaxModeTo('mockBool')
- assert(dispatchSpy.calledOnce)
- assert(actionSpies.setMaxModeTo.calledOnce)
- assert.equal(
- actionSpies.setMaxModeTo.getCall(0).args[0],
- 'mockBool'
- )
- })
- })
-
- describe('updateSendAmount()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendAmount('mockAmount')
- assert(dispatchSpy.calledOnce)
- assert(actionSpies.updateSendAmount.calledOnce)
- assert.equal(
- actionSpies.updateSendAmount.getCall(0).args[0],
- 'mockAmount'
- )
- })
- })
-
- describe('updateGasFeeError()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateGasFeeError({ some: 'data' })
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.updateSendErrors.calledOnce)
- assert.deepEqual(
- duckActionSpies.updateSendErrors.getCall(0).args[0],
- { some: 'data', mockGasFeeErrorChange: true }
- )
- })
- })
-
- describe('updateSendAmountError()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendAmountError({ some: 'data' })
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.updateSendErrors.calledOnce)
- assert.deepEqual(
- duckActionSpies.updateSendErrors.getCall(0).args[0],
- { some: 'data', mockChange: true }
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
deleted file mode 100644
index c780c88f5..000000000
--- a/ui/app/components/send/send-content/send-content.component.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainerContent from '../../page-container/page-container-content.component'
-import SendAmountRow from './send-amount-row/'
-import SendFromRow from './send-from-row/'
-import SendGasRow from './send-gas-row/'
-import SendHexDataRow from './send-hex-data-row'
-import SendToRow from './send-to-row/'
-
-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={this.updateGas}
- scanQrCode={ _ => this.props.scanQrCode()}
- />
- <SendAmountRow updateGas={this.updateGas} />
- <SendGasRow />
- {(this.props.showHexData && (
- <SendHexDataRow
- updateGas={this.updateGas}
- />
- ))}
- </div>
- </PageContainerContent>
- )
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
deleted file mode 100644
index bedac1259..000000000
--- a/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import AccountListItem from '../../account-list-item/'
-
-export default class SendDropdownList extends Component {
-
- static propTypes = {
- accounts: PropTypes.array,
- closeDropdown: PropTypes.func,
- onSelect: PropTypes.func,
- activeAddress: PropTypes.string,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- getListItemIcon (accountAddress, activeAddress) {
- return accountAddress === activeAddress
- ? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
- : null
- }
-
- render () {
- const {
- accounts,
- closeDropdown,
- onSelect,
- activeAddress,
- } = this.props
-
- return (<div>
- <div
- className="send-v2__from-dropdown__close-area"
- onClick={() => closeDropdown()}
- />
- <div className="send-v2__from-dropdown__list">
- {accounts.map((account, index) => <AccountListItem
- account={account}
- className="account-list-item__dropdown"
- handleClick={() => {
- onSelect(account)
- closeDropdown()
- }}
- icon={this.getListItemIcon(account.address, activeAddress)}
- key={`send-dropdown-account-#${index}`}
- />)}
- </div>
- </div>)
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
deleted file mode 100644
index f8aa084d8..000000000
--- a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowWrapper from '../send-row-wrapper/'
-import AccountListItem from '../../account-list-item'
-
-export default class SendFromRow extends Component {
- static propTypes = {
- from: PropTypes.object,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- render () {
- const { t } = this.context
- const { from } = this.props
-
- return (
- <SendRowWrapper label={`${t('from')}:`}>
- <div className="send-v2__from-dropdown">
- <AccountListItem account={from} />
- </div>
- </SendRowWrapper>
- )
- }
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
deleted file mode 100644
index b667aa037..000000000
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, {Component} from 'react'
-import PropTypes from 'prop-types'
-import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../../../../constants/common'
-
-export default class GasFeeDisplay extends Component {
-
- static propTypes = {
- conversionRate: PropTypes.number,
- primaryCurrency: PropTypes.string,
- convertedCurrency: PropTypes.string,
- gasLoadingError: PropTypes.bool,
- gasTotal: PropTypes.string,
- onReset: PropTypes.func,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const { gasTotal, gasLoadingError, onReset } = this.props
-
- return (
- <div className="send-v2__gas-fee-display">
- {gasTotal
- ? (
- <div className="currency-display">
- <UserPreferencedCurrencyDisplay
- value={gasTotal}
- type={PRIMARY}
- />
- <UserPreferencedCurrencyDisplay
- className="currency-display__converted-value"
- value={gasTotal}
- type={SECONDARY}
- />
- </div>
- )
- : gasLoadingError
- ? <div className="currency-display.currency-display--message">
- {this.context.t('setGasPrice')}
- </div>
- : <div className="currency-display">
- {this.context.t('loading')}
- </div>
- }
- <button
- className="gas-fee-reset"
- onClick={onReset}
- >
- { this.context.t('reset') }
- </button>
- </div>
- )
- }
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
deleted file mode 100644
index bf7446626..000000000
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowWrapper from '../send-row-wrapper/'
-import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
-import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group'
-import AdvancedGasInputs from '../../../gas-customization/advanced-gas-inputs'
-
-export default class SendGasRow extends Component {
-
- static propTypes = {
- conversionRate: PropTypes.number,
- convertedCurrency: PropTypes.string,
- gasFeeError: PropTypes.bool,
- gasLoadingError: PropTypes.bool,
- gasTotal: PropTypes.string,
- showCustomizeGasModal: PropTypes.func,
- setGasPrice: PropTypes.func,
- setGasLimit: PropTypes.func,
- gasPriceButtonGroupProps: PropTypes.object,
- gasButtonGroupShown: PropTypes.bool,
- advancedInlineGasShown: PropTypes.bool,
- resetGasButtons: PropTypes.func,
- gasPrice: PropTypes.number,
- gasLimit: PropTypes.number,
- insufficientBalance: PropTypes.bool,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- };
-
- renderAdvancedOptionsButton () {
- const { metricsEvent } = this.context
- const { showCustomizeGasModal } = this.props
- return <div className="advanced-gas-options-btn" onClick={() => {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Clicked "Advanced Options"',
- },
- })
- showCustomizeGasModal()
- }}>
- { this.context.t('advancedOptions') }
- </div>
- }
-
- renderContent () {
- const {
- conversionRate,
- convertedCurrency,
- gasLoadingError,
- gasTotal,
- showCustomizeGasModal,
- gasPriceButtonGroupProps,
- gasButtonGroupShown,
- advancedInlineGasShown,
- resetGasButtons,
- setGasPrice,
- setGasLimit,
- gasPrice,
- gasLimit,
- insufficientBalance,
- } = this.props
- const { metricsEvent } = this.context
-
- const gasPriceButtonGroup = <div>
- <GasPriceButtonGroup
- className="gas-price-button-group--small"
- showCheck={false}
- {...gasPriceButtonGroupProps}
- handleGasPriceSelection={(...args) => {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Changed Gas Button',
- },
- })
- gasPriceButtonGroupProps.handleGasPriceSelection(...args)
- }}
- />
- { this.renderAdvancedOptionsButton() }
- </div>
- const gasFeeDisplay = <GasFeeDisplay
- conversionRate={conversionRate}
- convertedCurrency={convertedCurrency}
- gasLoadingError={gasLoadingError}
- gasTotal={gasTotal}
- onReset={resetGasButtons}
- onClick={() => showCustomizeGasModal()}
- />
- const advancedGasInputs = <div>
- <AdvancedGasInputs
- updateCustomGasPrice={newGasPrice => setGasPrice(newGasPrice, gasLimit)}
- updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)}
- customGasPrice={gasPrice}
- customGasLimit={gasLimit}
- insufficientBalance={insufficientBalance}
- customPriceIsSafe={true}
- isSpeedUp={false}
- />
- { this.renderAdvancedOptionsButton() }
- </div>
-
- if (advancedInlineGasShown) {
- return advancedGasInputs
- } else if (gasButtonGroupShown) {
- return gasPriceButtonGroup
- } else {
- return gasFeeDisplay
- }
- }
-
- render () {
- const { gasFeeError } = this.props
-
- return (
- <SendRowWrapper
- label={`${this.context.t('transactionFee')}:`}
- showError={gasFeeError}
- errorType={'gasFee'}
- >
- { this.renderContent() }
- </SendRowWrapper>
- )
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
deleted file mode 100644
index a187d61a2..000000000
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getConversionRate,
- getCurrentCurrency,
- getGasTotal,
- getGasPrice,
- getGasLimit,
- getSendAmount,
-} from '../../send.selectors.js'
-import {
- isBalanceSufficient,
- calcGasTotal,
-} from '../../send.utils.js'
-import {
- getBasicGasEstimateLoadingStatus,
- getRenderableEstimateDataForSmallButtonsFromGWEI,
- getDefaultActiveButtonIndex,
-} from '../../../../selectors/custom-gas'
-import {
- showGasButtonGroup,
-} from '../../../../ducks/send.duck'
-import {
- resetCustomData,
- setCustomGasPrice,
- setCustomGasLimit,
-} from '../../../../ducks/gas.duck'
-import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
-import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../actions'
-import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors'
-import SendGasRow from './send-gas-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
-
-function mapStateToProps (state) {
- const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
- const gasPrice = getGasPrice(state)
- const gasLimit = getGasLimit(state)
- const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice)
-
- const gasTotal = getGasTotal(state)
- const conversionRate = getConversionRate(state)
- const balance = getCurrentEthBalance(state)
-
- const insufficientBalance = !isBalanceSufficient({
- amount: getSelectedToken(state) ? '0x0' : getSendAmount(state),
- gasTotal,
- balance,
- conversionRate,
- })
-
- return {
- conversionRate,
- convertedCurrency: getCurrentCurrency(state),
- gasTotal,
- gasFeeError: gasFeeIsInError(state),
- gasLoadingError: getGasLoadingError(state),
- gasPriceButtonGroupProps: {
- buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
- defaultActiveButtonIndex: 1,
- newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null,
- gasButtonInfo,
- },
- gasButtonGroupShown: getGasButtonGroupShown(state),
- advancedInlineGasShown: getAdvancedInlineGasShown(state),
- gasPrice,
- gasLimit,
- insufficientBalance,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
- setGasPrice: (newPrice, gasLimit) => {
- dispatch(setGasPrice(newPrice))
- dispatch(setCustomGasPrice(newPrice))
- if (gasLimit) {
- dispatch(setGasTotal(calcGasTotal(gasLimit, newPrice)))
- }
- },
- setGasLimit: (newLimit, gasPrice) => {
- dispatch(setGasLimit(newLimit))
- dispatch(setCustomGasLimit(newLimit))
- if (gasPrice) {
- dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)))
- }
- },
- showGasButtonGroup: () => dispatch(showGasButtonGroup()),
- resetCustomData: () => dispatch(resetCustomData()),
- }
-}
-
-function mergeProps (stateProps, dispatchProps, ownProps) {
- const { gasPriceButtonGroupProps } = stateProps
- const { gasButtonInfo } = gasPriceButtonGroupProps
- const {
- setGasPrice: dispatchSetGasPrice,
- showGasButtonGroup: dispatchShowGasButtonGroup,
- resetCustomData: dispatchResetCustomData,
- ...otherDispatchProps
- } = dispatchProps
-
- return {
- ...stateProps,
- ...otherDispatchProps,
- ...ownProps,
- gasPriceButtonGroupProps: {
- ...gasPriceButtonGroupProps,
- handleGasPriceSelection: dispatchSetGasPrice,
- },
- resetGasButtons: () => {
- dispatchResetCustomData()
- dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei)
- dispatchShowGasButtonGroup()
- },
- setGasPrice: dispatchSetGasPrice,
- }
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
deleted file mode 100644
index 12e78657b..000000000
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
+++ /dev/null
@@ -1,200 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-let mergeProps
-
-const actionSpies = {
- showModal: sinon.spy(),
- setGasPrice: sinon.spy(),
- setGasTotal: sinon.spy(),
- setGasLimit: sinon.spy(),
-}
-
-const sendDuckSpies = {
- showGasButtonGroup: sinon.spy(),
-}
-
-const gasDuckSpies = {
- resetCustomData: sinon.spy(),
- setCustomGasPrice: sinon.spy(),
- setCustomGasLimit: sinon.spy(),
-}
-
-proxyquire('../send-gas-row.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- mergeProps = mp
- return () => ({})
- },
- },
- '../../../../selectors': {
- getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
- getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
- getSelectedToken: () => false,
- },
- '../../send.selectors.js': {
- getConversionRate: (s) => `mockConversionRate:${s}`,
- getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getGasPrice: (s) => `mockGasPrice:${s}`,
- getGasLimit: (s) => `mockGasLimit:${s}`,
- getSendAmount: (s) => `mockSendAmount:${s}`,
- },
- '../../send.utils.js': {
- isBalanceSufficient: ({
- amount,
- gasTotal,
- balance,
- conversionRate,
- }) => `${amount}:${gasTotal}:${balance}:${conversionRate}`,
- calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
- },
- './send-gas-row.selectors.js': {
- getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
- gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
- getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`,
- },
- '../../../../actions': actionSpies,
- '../../../../selectors/custom-gas': {
- getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`,
- getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`,
- getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length,
- },
- '../../../../ducks/send.duck': sendDuckSpies,
- '../../../../ducks/gas.duck': gasDuckSpies,
-})
-
-describe('send-gas-row container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- conversionRate: 'mockConversionRate:mockState',
- convertedCurrency: 'mockConvertedCurrency:mockState',
- gasTotal: 'mockGasTotal:mockState',
- gasFeeError: 'mockGasFeeError:mockState',
- gasLoadingError: 'mockGasLoadingError:mockState',
- gasPriceButtonGroupProps: {
- buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`,
- defaultActiveButtonIndex: 1,
- newActiveButtonIndex: 49,
- gasButtonInfo: `mockGasButtonInfo:mockState`,
- },
- gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`,
- advancedInlineGasShown: 'mockAdvancedInlineGasShown:mockState',
- gasLimit: 'mockGasLimit:mockState',
- gasPrice: 'mockGasPrice:mockState',
- insufficientBalance: false,
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- actionSpies.setGasTotal.resetHistory()
- })
-
- describe('showCustomizeGasModal()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.showCustomizeGasModal()
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- actionSpies.showModal.getCall(0).args[0],
- { name: 'CUSTOMIZE_GAS', hideBasic: true }
- )
- })
- })
-
- describe('setGasPrice()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit')
- assert(dispatchSpy.calledThrice)
- assert(actionSpies.setGasPrice.calledOnce)
- assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice')
- assert.equal(gasDuckSpies.setCustomGasPrice.getCall(0).args[0], 'mockNewPrice')
- assert(actionSpies.setGasTotal.calledOnce)
- assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockLimitmockNewPrice')
- })
- })
-
- describe('setGasLimit()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice')
- assert(dispatchSpy.calledThrice)
- assert(actionSpies.setGasLimit.calledOnce)
- assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'mockNewLimit')
- assert.equal(gasDuckSpies.setCustomGasLimit.getCall(0).args[0], 'mockNewLimit')
- assert(actionSpies.setGasTotal.calledOnce)
- assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockNewLimitmockPrice')
- })
- })
-
- describe('showGasButtonGroup()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.showGasButtonGroup()
- assert(dispatchSpy.calledOnce)
- assert(sendDuckSpies.showGasButtonGroup.calledOnce)
- })
- })
-
- describe('resetCustomData()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.resetCustomData()
- assert(dispatchSpy.calledOnce)
- assert(gasDuckSpies.resetCustomData.calledOnce)
- })
- })
-
- })
-
- describe('mergeProps', () => {
- let stateProps
- let dispatchProps
- let ownProps
-
- beforeEach(() => {
- stateProps = {
- gasPriceButtonGroupProps: {
- someGasPriceButtonGroupProp: 'foo',
- anotherGasPriceButtonGroupProp: 'bar',
- },
- someOtherStateProp: 'baz',
- }
- dispatchProps = {
- setGasPrice: sinon.spy(),
- someOtherDispatchProp: sinon.spy(),
- }
- ownProps = { someOwnProp: 123 }
- })
-
- it('should return the expected props when isConfirm is true', () => {
- const result = mergeProps(stateProps, dispatchProps, ownProps)
-
- assert.equal(result.someOtherStateProp, 'baz')
- assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
- assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
- assert.equal(result.someOwnProp, 123)
-
- assert.equal(dispatchProps.setGasPrice.callCount, 0)
- result.gasPriceButtonGroupProps.handleGasPriceSelection()
- assert.equal(dispatchProps.setGasPrice.callCount, 1)
-
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
- result.someOtherDispatchProp()
- assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
- })
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
deleted file mode 100644
index df554ca5f..000000000
--- a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { connect } from 'react-redux'
-import {
- updateSendHexData,
-} from '../../../../actions'
-import SendHexDataRow from './send-hex-data-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow)
-
-function mapStateToProps (state) {
- return {
- data: state.metamask.send.data,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- updateSendHexData (data) {
- return dispatch(updateSendHexData(data))
- },
- }
-}
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
deleted file mode 100644
index 0146ce645..000000000
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowErrorMessage from './send-row-error-message/'
-import SendRowWarningMessage from './send-row-warning-message/'
-
-export default class SendRowWrapper extends Component {
-
- static propTypes = {
- children: PropTypes.node,
- errorType: PropTypes.string,
- label: PropTypes.string,
- showError: PropTypes.bool,
- showWarning: PropTypes.bool,
- warningType: PropTypes.string,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- children,
- errorType = '',
- label,
- showError = false,
- showWarning = false,
- warningType = '',
- } = this.props
- const formField = Array.isArray(children) ? children[1] || children[0] : children
- const customLabelContent = children.length > 1 ? children[0] : null
-
- return (
- <div className="send-v2__form-row">
- <div className="send-v2__form-label">
- {label}
- {showError && <SendRowErrorMessage errorType={errorType}/>}
- {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />}
- {customLabelContent}
- </div>
- <div className="send-v2__form-field">
- {formField}
- </div>
- </div>
- )
- }
-
-}
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
deleted file mode 100644
index 434204490..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowWrapper from '../send-row-wrapper/'
-import EnsInput from '../../../ens-input'
-import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js'
-
-export default class SendToRow extends Component {
-
- static propTypes = {
- closeToDropdown: PropTypes.func,
- hasHexData: PropTypes.bool.isRequired,
- inError: PropTypes.bool,
- inWarning: PropTypes.bool,
- network: PropTypes.string,
- openToDropdown: PropTypes.func,
- selectedToken: PropTypes.object,
- to: PropTypes.string,
- toAccounts: PropTypes.array,
- toDropdownOpen: PropTypes.bool,
- tokens: PropTypes.array,
- updateGas: PropTypes.func,
- updateSendTo: PropTypes.func,
- updateSendToError: PropTypes.func,
- updateSendToWarning: PropTypes.func,
- scanQrCode: PropTypes.func,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- handleToChange (to, nickname = '', toError, toWarning, network) {
- const { hasHexData, updateSendTo, updateSendToError, updateGas, tokens, selectedToken, updateSendToWarning } = this.props
- const toErrorObject = getToErrorObject(to, toError, hasHexData, tokens, selectedToken, network)
- const toWarningObject = getToWarningObject(to, toWarning, tokens, selectedToken)
- updateSendTo(to, nickname)
- updateSendToError(toErrorObject)
- updateSendToWarning(toWarningObject)
- if (toErrorObject.to === null) {
- updateGas({ to })
- }
- }
-
- render () {
- const {
- closeToDropdown,
- inError,
- inWarning,
- network,
- openToDropdown,
- to,
- toAccounts,
- toDropdownOpen,
- } = this.props
-
- return (
- <SendRowWrapper
- errorType={'to'}
- label={`${this.context.t('to')}: `}
- showError={inError}
- showWarning={inWarning}
- warningType={'to'}
- >
- <EnsInput
- scanQrCode={_ => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Used QR scanner',
- },
- })
- this.props.scanQrCode()
- }}
- accounts={toAccounts}
- closeDropdown={() => closeToDropdown()}
- dropdownOpen={toDropdownOpen}
- inError={inError}
- name={'address'}
- network={network}
- onChange={({ toAddress, nickname, toError, toWarning }) => this.handleToChange(toAddress, nickname, toError, toWarning, this.props.network)}
- openDropdown={() => openToDropdown()}
- placeholder={this.context.t('recipientAddress')}
- to={to}
- />
- </SendRowWrapper>
- )
- }
-
-}
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
deleted file mode 100644
index fc6742df0..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getCurrentNetwork,
- getSelectedToken,
- getSendTo,
- getSendToAccounts,
- getSendHexData,
-} from '../../send.selectors.js'
-import {
- getToDropdownOpen,
- getTokens,
- sendToIsInError,
- sendToIsInWarning,
-} from './send-to-row.selectors.js'
-import {
- updateSendTo,
-} from '../../../../actions'
-import {
- updateSendErrors,
- updateSendWarnings,
- openToDropdown,
- closeToDropdown,
-} from '../../../../ducks/send.duck'
-import SendToRow from './send-to-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
-
-function mapStateToProps (state) {
- return {
- hasHexData: Boolean(getSendHexData(state)),
- inError: sendToIsInError(state),
- inWarning: sendToIsInWarning(state),
- network: getCurrentNetwork(state),
- selectedToken: getSelectedToken(state),
- to: getSendTo(state),
- toAccounts: getSendToAccounts(state),
- toDropdownOpen: getToDropdownOpen(state),
- tokens: getTokens(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- closeToDropdown: () => dispatch(closeToDropdown()),
- openToDropdown: () => dispatch(openToDropdown()),
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- updateSendToError: (toErrorObject) => {
- dispatch(updateSendErrors(toErrorObject))
- },
- updateSendToWarning: (toWarningObject) => {
- dispatch(updateSendWarnings(toWarningObject))
- },
- }
-}
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
deleted file mode 100644
index 2bd3ea45e..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const {
- REQUIRED_ERROR,
- INVALID_RECIPIENT_ADDRESS_ERROR,
- KNOWN_RECIPIENT_ADDRESS_ERROR,
- INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
-} = require('../../send.constants')
-const { isValidAddress, isEthNetwork } = require('../../../../util')
-import { checkExistingAddresses } from '../../../pages/add-token/util'
-
-const ethUtil = require('ethereumjs-util')
-const contractMap = require('eth-contract-metadata')
-
-function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) {
- if (!to) {
- if (!hasHexData) {
- toError = REQUIRED_ERROR
- }
- } else if (!isValidAddress(to, network) && !toError) {
- toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR
- } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
- toError = KNOWN_RECIPIENT_ADDRESS_ERROR
- }
- return { to: toError }
-}
-
-function getToWarningObject (to, toWarning = null, tokens = [], selectedToken = null) {
- if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
- toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR
- }
- return { to: toWarning }
-}
-
-module.exports = {
- getToErrorObject,
- getToWarningObject,
-}
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
deleted file mode 100644
index aa09f01a9..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- updateSendTo: sinon.spy(),
-}
-const duckActionSpies = {
- closeToDropdown: sinon.spy(),
- openToDropdown: sinon.spy(),
- updateSendErrors: sinon.spy(),
- updateSendWarnings: sinon.spy(),
-}
-
-proxyquire('../send-to-row.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../send.selectors.js': {
- getCurrentNetwork: (s) => `mockNetwork:${s}`,
- getSelectedToken: (s) => `mockSelectedToken:${s}`,
- getSendHexData: (s) => s,
- getSendTo: (s) => `mockTo:${s}`,
- getSendToAccounts: (s) => `mockToAccounts:${s}`,
- },
- './send-to-row.selectors.js': {
- getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`,
- sendToIsInError: (s) => `mockInError:${s}`,
- sendToIsInWarning: (s) => `mockInWarning:${s}`,
- getTokens: (s) => `mockTokens:${s}`,
- },
- '../../../../actions': actionSpies,
- '../../../../ducks/send.duck': duckActionSpies,
-})
-
-describe('send-to-row container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- hasHexData: true,
- inError: 'mockInError:mockState',
- inWarning: 'mockInWarning:mockState',
- network: 'mockNetwork:mockState',
- selectedToken: 'mockSelectedToken:mockState',
- to: 'mockTo:mockState',
- toAccounts: 'mockToAccounts:mockState',
- toDropdownOpen: 'mockToDropdownOpen:mockState',
- tokens: 'mockTokens:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('closeToDropdown()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.closeToDropdown()
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.closeToDropdown.calledOnce)
- assert.equal(
- duckActionSpies.closeToDropdown.getCall(0).args[0],
- undefined
- )
- })
- })
-
- describe('openToDropdown()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.openToDropdown()
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.openToDropdown.calledOnce)
- assert.equal(
- duckActionSpies.openToDropdown.getCall(0).args[0],
- undefined
- )
- })
- })
-
- describe('updateSendTo()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname')
- assert(dispatchSpy.calledOnce)
- assert(actionSpies.updateSendTo.calledOnce)
- assert.deepEqual(
- actionSpies.updateSendTo.getCall(0).args,
- ['mockTo', 'mockNickname']
- )
- })
- })
-
- describe('updateSendToError()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendToError('mockToErrorObject')
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.updateSendErrors.calledOnce)
- assert.equal(
- duckActionSpies.updateSendErrors.getCall(0).args[0],
- 'mockToErrorObject'
- )
- })
- })
-
- describe('updateSendToWarning()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendToWarning('mockToWarningObject')
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.updateSendWarnings.calledOnce)
- assert.equal(
- duckActionSpies.updateSendWarnings.getCall(0).args[0],
- 'mockToWarningObject'
- )
- })
- })
-
- })
-
-})
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
deleted file mode 100644
index f6abb26e6..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-import {
- REQUIRED_ERROR,
- INVALID_RECIPIENT_ADDRESS_ERROR,
- KNOWN_RECIPIENT_ADDRESS_ERROR,
-} from '../../../send.constants'
-
-const stubs = {
- isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))),
-}
-
-const toRowUtils = proxyquire('../send-to-row.utils.js', {
- '../../../../util': {
- isValidAddress: stubs.isValidAddress,
- },
-})
-const {
- getToErrorObject,
- getToWarningObject,
-} = toRowUtils
-
-describe('send-to-row utils', () => {
-
- describe('getToErrorObject()', () => {
- it('should return a required error if to is falsy', () => {
- assert.deepEqual(getToErrorObject(null), {
- to: REQUIRED_ERROR,
- })
- })
-
- 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,
- })
- })
-
- it('should return null if to is truthy and valid', () => {
- assert.deepEqual(getToErrorObject('0xabc123'), {
- to: null,
- })
- })
-
- it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => {
- assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), {
- to: 'someExplicitError',
- })
- })
-
- it('should return a known address recipient if to is truthy but part of state tokens', () => {
- assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
- })
- })
-
- it('should null if to is truthy part of tokens but selectedToken falsy', () => {
- assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}]), {
- to: null,
- })
- })
-
- it('should return a known address recipient if to is truthy but part of contract metadata', () => {
- assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
- })
- })
- it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
- assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
- })
- })
- })
-
- describe('getToWarningObject()', () => {
- it('should return a known address recipient if to is truthy but part of state tokens', () => {
- assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
- })
- })
-
- it('should null if to is truthy part of tokens but selectedToken falsy', () => {
- assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}]), {
- to: null,
- })
- })
-
- it('should return a known address recipient if to is truthy but part of contract metadata', () => {
- assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
- })
- })
- it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
- assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_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
deleted file mode 100644
index c5a11c8bb..000000000
--- a/ui/app/components/send/send-content/tests/send-content-component.test.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import SendContent from '../send-content.component.js'
-
-import PageContainerContent from '../../../page-container/page-container-content.component'
-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 showHexData={true} />)
- })
-
- describe('render', () => {
- it('should render a PageContainerContent component', () => {
- assert.equal(wrapper.find(PageContainerContent).length, 1)
- })
-
- it('should render a div with a .send-v2__form class as a child of PageContainerContent', () => {
- const PageContainerContentChild = wrapper.find(PageContainerContent).children()
- PageContainerContentChild.is('div')
- PageContainerContentChild.is('.send-v2__form')
- })
-
- it('should render the correct row components as grandchildren of the PageContainerContent component', () => {
- 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(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
deleted file mode 100644
index d943b4b22..000000000
--- a/ui/app/components/send/send-footer/send-footer.component.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainerFooter from '../../page-container/page-container-footer'
-import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes'
-
-export default class SendFooter extends Component {
-
- static propTypes = {
- addToAddressBookIfNew: PropTypes.func,
- amount: PropTypes.string,
- data: PropTypes.string,
- clearSend: PropTypes.func,
- disabled: PropTypes.bool,
- editingTransactionId: PropTypes.string,
- errors: PropTypes.object,
- from: PropTypes.object,
- gasLimit: PropTypes.string,
- gasPrice: PropTypes.string,
- gasTotal: PropTypes.string,
- history: PropTypes.object,
- inError: PropTypes.bool,
- selectedToken: PropTypes.object,
- sign: PropTypes.func,
- to: PropTypes.string,
- toAccounts: PropTypes.array,
- tokenBalance: PropTypes.string,
- unapprovedTxs: PropTypes.object,
- update: PropTypes.func,
- sendErrors: PropTypes.object,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- };
-
- onCancel () {
- this.props.clearSend()
- this.props.history.push(DEFAULT_ROUTE)
- }
-
- onSubmit (event) {
- event.preventDefault()
- const {
- addToAddressBookIfNew,
- amount,
- data,
- editingTransactionId,
- from: {address: from},
- gasLimit: gas,
- gasPrice,
- selectedToken,
- sign,
- to,
- unapprovedTxs,
- // updateTx,
- update,
- toAccounts,
- history,
- } = this.props
- const { metricsEvent } = this.context
-
- // Should not be needed because submit should be disabled if there are errors.
- // const noErrors = !amountError && toError === null
-
- // if (!noErrors) {
- // return
- // }
-
- // TODO: add nickname functionality
- addToAddressBookIfNew(to, toAccounts)
- const promise = editingTransactionId
- ? update({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- selectedToken,
- to,
- unapprovedTxs,
- })
- : sign({ data, selectedToken, to, amount, from, gas, gasPrice })
-
- Promise.resolve(promise)
- .then(() => {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Complete',
- },
- })
- history.push(CONFIRM_TRANSACTION_ROUTE)
- })
- }
-
- formShouldBeDisabled () {
- const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
- const missingTokenBalance = selectedToken && !tokenBalance
- const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to)
- return shouldBeDisabled
- }
-
- componentDidUpdate (prevProps) {
- const { inError, sendErrors } = this.props
- const { metricsEvent } = this.context
- if (!prevProps.inError && inError) {
- const errorField = Object.keys(sendErrors).find(key => sendErrors[key])
- const errorMessage = sendErrors[errorField]
-
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Error',
- },
- customVariables: {
- errorField,
- errorMessage,
- },
- })
- }
- }
-
- render () {
- return (
- <PageContainerFooter
- onCancel={() => this.onCancel()}
- onSubmit={e => this.onSubmit(e)}
- disabled={this.formShouldBeDisabled()}
- />
- )
- }
-
-}
diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js
deleted file mode 100644
index 0c6120cc5..000000000
--- a/ui/app/components/send/send-footer/send-footer.container.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { connect } from 'react-redux'
-import ethUtil from 'ethereumjs-util'
-import {
- addToAddressBook,
- clearSend,
- signTokenTx,
- signTx,
- updateTransaction,
-} from '../../../actions'
-import SendFooter from './send-footer.component'
-import {
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getSelectedToken,
- getSendAmount,
- getSendEditingTransactionId,
- getSendFromObject,
- getSendTo,
- getSendToAccounts,
- getSendHexData,
- getTokenBalance,
- getUnapprovedTxs,
- getSendErrors,
-} from '../send.selectors'
-import {
- isSendFormInError,
-} from './send-footer.selectors'
-import {
- addressIsNew,
- constructTxParams,
- constructUpdatedTx,
-} from './send-footer.utils'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
-
-function mapStateToProps (state) {
- return {
- amount: getSendAmount(state),
- data: getSendHexData(state),
- editingTransactionId: getSendEditingTransactionId(state),
- from: getSendFromObject(state),
- gasLimit: getGasLimit(state),
- gasPrice: getGasPrice(state),
- gasTotal: getGasTotal(state),
- inError: isSendFormInError(state),
- selectedToken: getSelectedToken(state),
- to: getSendTo(state),
- toAccounts: getSendToAccounts(state),
- tokenBalance: getTokenBalance(state),
- unapprovedTxs: getUnapprovedTxs(state),
- sendErrors: getSendErrors(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- clearSend: () => dispatch(clearSend()),
- sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => {
- const txParams = constructTxParams({
- amount,
- data,
- from,
- gas,
- gasPrice,
- selectedToken,
- to,
- })
-
- selectedToken
- ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams))
- : dispatch(signTx(txParams))
- },
- update: ({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- selectedToken,
- to,
- unapprovedTxs,
- }) => {
- const editingTx = constructUpdatedTx({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- selectedToken,
- to,
- unapprovedTxs,
- })
-
- return dispatch(updateTransaction(editingTx))
- },
- addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
- const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
- if (addressIsNew(toAccounts)) {
- // TODO: nickname, i.e. addToAddressBook(recipient, nickname)
- dispatch(addToAddressBook(hexPrefixedAddress, nickname))
- }
- },
- }
-}
diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
deleted file mode 100644
index 4b63e422d..000000000
--- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../routes'
-import SendFooter from '../send-footer.component.js'
-
-import PageContainerFooter from '../../../page-container/page-container-footer'
-
-const propsMethodSpies = {
- addToAddressBookIfNew: sinon.spy(),
- clearSend: sinon.spy(),
- sign: sinon.spy(),
- update: sinon.spy(),
-}
-const historySpies = {
- push: sinon.spy(),
-}
-const MOCK_EVENT = { preventDefault: () => {} }
-
-sinon.spy(SendFooter.prototype, 'onCancel')
-sinon.spy(SendFooter.prototype, 'onSubmit')
-
-describe('SendFooter Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<SendFooter
- addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
- amount={'mockAmount'}
- clearSend={propsMethodSpies.clearSend}
- disabled={true}
- editingTransactionId={'mockEditingTransactionId'}
- errors={{}}
- from={ { address: 'mockAddress', balance: 'mockBalance' } }
- gasLimit={'mockGasLimit'}
- gasPrice={'mockGasPrice'}
- gasTotal={'mockGasTotal'}
- history={historySpies}
- inError={false}
- selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
- sign={propsMethodSpies.sign}
- to={'mockTo'}
- toAccounts={['mockAccount']}
- tokenBalance={'mockTokenBalance'}
- unapprovedTxs={['mockTx']}
- update={propsMethodSpies.update}
- sendErrors={{}}
- />, { context: { t: str => str, metricsEvent: () => ({}) } })
- })
-
- afterEach(() => {
- propsMethodSpies.clearSend.resetHistory()
- propsMethodSpies.addToAddressBookIfNew.resetHistory()
- propsMethodSpies.clearSend.resetHistory()
- propsMethodSpies.sign.resetHistory()
- propsMethodSpies.update.resetHistory()
- historySpies.push.resetHistory()
- SendFooter.prototype.onCancel.resetHistory()
- SendFooter.prototype.onSubmit.resetHistory()
- })
-
- describe('onCancel', () => {
- it('should call clearSend', () => {
- assert.equal(propsMethodSpies.clearSend.callCount, 0)
- wrapper.instance().onCancel()
- assert.equal(propsMethodSpies.clearSend.callCount, 1)
- })
-
- it('should call history.push', () => {
- assert.equal(historySpies.push.callCount, 0)
- wrapper.instance().onCancel()
- assert.equal(historySpies.push.callCount, 1)
- assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE)
- })
- })
-
-
- describe('formShouldBeDisabled()', () => {
- const config = {
- 'should return true if inError is truthy': {
- inError: true,
- expectedResult: true,
- },
- 'should return true if gasTotal is falsy': {
- inError: false,
- gasTotal: false,
- expectedResult: true,
- },
- 'should return true if to is truthy': {
- to: '0xsomevalidAddress',
- inError: false,
- gasTotal: false,
- expectedResult: true,
- },
- 'should return true if selectedToken is truthy and tokenBalance is falsy': {
- selectedToken: true,
- tokenBalance: null,
- expectedResult: true,
- },
- 'should return false if inError is false and all other params are truthy': {
- inError: false,
- gasTotal: '0x123',
- selectedToken: true,
- tokenBalance: 123,
- expectedResult: false,
- },
- }
- Object.entries(config).map(([description, obj]) => {
- it(description, () => {
- wrapper.setProps(obj)
- assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult)
- })
- })
- })
-
- describe('onSubmit', () => {
- it('should call addToAddressBookIfNew with the correct params', () => {
- wrapper.instance().onSubmit(MOCK_EVENT)
- assert(propsMethodSpies.addToAddressBookIfNew.calledOnce)
- assert.deepEqual(
- propsMethodSpies.addToAddressBookIfNew.getCall(0).args,
- ['mockTo', ['mockAccount']]
- )
- })
-
- it('should call props.update if editingTransactionId is truthy', () => {
- wrapper.instance().onSubmit(MOCK_EVENT)
- assert(propsMethodSpies.update.calledOnce)
- assert.deepEqual(
- propsMethodSpies.update.getCall(0).args[0],
- {
- data: undefined,
- amount: 'mockAmount',
- editingTransactionId: 'mockEditingTransactionId',
- from: 'mockAddress',
- gas: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- selectedToken: { mockProp: 'mockSelectedTokenProp' },
- to: 'mockTo',
- unapprovedTxs: ['mockTx'],
- }
- )
- })
-
- it('should not call props.sign if editingTransactionId is truthy', () => {
- assert.equal(propsMethodSpies.sign.callCount, 0)
- })
-
- it('should call props.sign if editingTransactionId is falsy', () => {
- wrapper.setProps({ editingTransactionId: null })
- wrapper.instance().onSubmit(MOCK_EVENT)
- assert(propsMethodSpies.sign.calledOnce)
- assert.deepEqual(
- propsMethodSpies.sign.getCall(0).args[0],
- {
- data: undefined,
- amount: 'mockAmount',
- from: 'mockAddress',
- gas: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- selectedToken: { mockProp: 'mockSelectedTokenProp' },
- to: 'mockTo',
- }
- )
- })
-
- it('should not call props.update if editingTransactionId is falsy', () => {
- assert.equal(propsMethodSpies.update.callCount, 0)
- })
-
- it('should call history.push', done => {
- Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT))
- .then(() => {
- assert.equal(historySpies.push.callCount, 1)
- assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
- done()
- })
- })
- })
-
- describe('render', () => {
- beforeEach(() => {
- sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn')
- wrapper = shallow(<SendFooter
- addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
- amount={'mockAmount'}
- clearSend={propsMethodSpies.clearSend}
- disabled={true}
- editingTransactionId={'mockEditingTransactionId'}
- errors={{}}
- from={ { address: 'mockAddress', balance: 'mockBalance' } }
- gasLimit={'mockGasLimit'}
- gasPrice={'mockGasPrice'}
- gasTotal={'mockGasTotal'}
- history={historySpies}
- inError={false}
- selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
- sign={propsMethodSpies.sign}
- to={'mockTo'}
- toAccounts={['mockAccount']}
- tokenBalance={'mockTokenBalance'}
- unapprovedTxs={['mockTx']}
- update={propsMethodSpies.update}
- />, { context: { t: str => str, metricsEvent: () => ({}) } })
- })
-
- afterEach(() => {
- SendFooter.prototype.formShouldBeDisabled.restore()
- })
-
- it('should render a PageContainerFooter component', () => {
- assert.equal(wrapper.find(PageContainerFooter).length, 1)
- })
-
- it('should pass the correct props to PageContainerFooter', () => {
- const {
- onCancel,
- onSubmit,
- disabled,
- } = wrapper.find(PageContainerFooter).props()
- assert.equal(disabled, 'formShouldBeDisabledReturn')
-
- assert.equal(SendFooter.prototype.onSubmit.callCount, 0)
- onSubmit(MOCK_EVENT)
- assert.equal(SendFooter.prototype.onSubmit.callCount, 1)
-
- assert.equal(SendFooter.prototype.onCancel.callCount, 0)
- onCancel()
- assert.equal(SendFooter.prototype.onCancel.callCount, 1)
- })
- })
-})
diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
deleted file mode 100644
index 70cb28df3..000000000
--- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js
+++ /dev/null
@@ -1,200 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- addToAddressBook: sinon.spy(),
- clearSend: sinon.spy(),
- signTokenTx: sinon.spy(),
- signTx: sinon.spy(),
- updateTransaction: sinon.spy(),
-}
-const utilsStubs = {
- addressIsNew: sinon.stub().returns(true),
- constructTxParams: sinon.stub().returns({
- value: 'mockAmount',
- }),
- constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'),
-}
-
-proxyquire('../send-footer.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../../actions': actionSpies,
- '../send.selectors': {
- getGasLimit: (s) => `mockGasLimit:${s}`,
- getGasPrice: (s) => `mockGasPrice:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getSelectedToken: (s) => `mockSelectedToken:${s}`,
- getSendAmount: (s) => `mockAmount:${s}`,
- getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
- getSendFromObject: (s) => `mockFromObject:${s}`,
- getSendTo: (s) => `mockTo:${s}`,
- getSendToAccounts: (s) => `mockToAccounts:${s}`,
- getTokenBalance: (s) => `mockTokenBalance:${s}`,
- getSendHexData: (s) => `mockHexData:${s}`,
- getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
- getSendErrors: (s) => `mockSendErrors:${s}`,
- },
- './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
- './send-footer.utils': utilsStubs,
-})
-
-describe('send-footer container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- amount: 'mockAmount:mockState',
- data: 'mockHexData:mockState',
- selectedToken: 'mockSelectedToken:mockState',
- editingTransactionId: 'mockEditingTransactionId:mockState',
- from: 'mockFromObject:mockState',
- gasLimit: 'mockGasLimit:mockState',
- gasPrice: 'mockGasPrice:mockState',
- gasTotal: 'mockGasTotal:mockState',
- inError: 'mockInError:mockState',
- to: 'mockTo:mockState',
- toAccounts: 'mockToAccounts:mockState',
- tokenBalance: 'mockTokenBalance:mockState',
- unapprovedTxs: 'mockUnapprovedTxs:mockState',
- sendErrors: 'mockSendErrors:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('clearSend()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.clearSend()
- assert(dispatchSpy.calledOnce)
- assert(actionSpies.clearSend.calledOnce)
- })
- })
-
- describe('sign()', () => {
- it('should dispatch a signTokenTx action if selectedToken is defined', () => {
- mapDispatchToPropsObject.sign({
- selectedToken: {
- address: '0xabc',
- },
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- })
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- utilsStubs.constructTxParams.getCall(0).args[0],
- {
- data: undefined,
- selectedToken: {
- address: '0xabc',
- },
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- }
- )
- assert.deepEqual(
- actionSpies.signTokenTx.getCall(0).args,
- [ '0xabc', 'mockTo', 'mockAmount', { value: 'mockAmount' } ]
- )
- })
-
- it('should dispatch a sign action if selectedToken is not defined', () => {
- utilsStubs.constructTxParams.resetHistory()
- mapDispatchToPropsObject.sign({
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- })
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- utilsStubs.constructTxParams.getCall(0).args[0],
- {
- data: undefined,
- selectedToken: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- }
- )
- assert.deepEqual(
- actionSpies.signTx.getCall(0).args,
- [ { value: 'mockAmount' } ]
- )
- })
- })
-
- describe('update()', () => {
- it('should dispatch an updateTransaction action', () => {
- mapDispatchToPropsObject.update({
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- editingTransactionId: 'mockEditingTransactionId',
- selectedToken: 'mockSelectedToken',
- unapprovedTxs: 'mockUnapprovedTxs',
- })
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- utilsStubs.constructUpdatedTx.getCall(0).args[0],
- {
- data: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- editingTransactionId: 'mockEditingTransactionId',
- selectedToken: 'mockSelectedToken',
- unapprovedTxs: 'mockUnapprovedTxs',
- }
- )
- assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams')
- })
- })
-
- describe('addToAddressBookIfNew()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname')
- assert(dispatchSpy.calledOnce)
- assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts')
- assert.deepEqual(
- actionSpies.addToAddressBook.getCall(0).args,
- [ '0xmockNewAddress', 'mockNickname' ]
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-header/send-header.component.js b/ui/app/components/send/send-header/send-header.component.js
deleted file mode 100644
index efc4bbf27..000000000
--- a/ui/app/components/send/send-header/send-header.component.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import PageContainerHeader from '../../page-container/page-container-header'
-import { DEFAULT_ROUTE } from '../../../routes'
-
-export default class SendHeader extends Component {
-
- static propTypes = {
- clearSend: PropTypes.func,
- history: PropTypes.object,
- titleKey: PropTypes.string,
- subtitleParams: PropTypes.array,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- onClose () {
- this.props.clearSend()
- this.props.history.push(DEFAULT_ROUTE)
- }
-
- render () {
- return (
- <PageContainerHeader
- onClose={() => this.onClose()}
- subtitle={this.context.t(...this.props.subtitleParams)}
- title={this.context.t(this.props.titleKey)}
- />
- )
- }
-
-}
diff --git a/ui/app/components/send/send-header/send-header.container.js b/ui/app/components/send/send-header/send-header.container.js
deleted file mode 100644
index 4bcd0d1b6..000000000
--- a/ui/app/components/send/send-header/send-header.container.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { connect } from 'react-redux'
-import { clearSend } from '../../../actions'
-import SendHeader from './send-header.component'
-import { getSubtitleParams, getTitleKey } from './send-header.selectors'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendHeader)
-
-function mapStateToProps (state) {
- return {
- titleKey: getTitleKey(state),
- subtitleParams: getSubtitleParams(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- clearSend: () => dispatch(clearSend()),
- }
-}
diff --git a/ui/app/components/send/send-header/tests/send-header-component.test.js b/ui/app/components/send/send-header/tests/send-header-component.test.js
deleted file mode 100644
index 930bfa387..000000000
--- a/ui/app/components/send/send-header/tests/send-header-component.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import { DEFAULT_ROUTE } from '../../../../routes'
-import SendHeader from '../send-header.component.js'
-
-import PageContainerHeader from '../../../page-container/page-container-header'
-
-const propsMethodSpies = {
- clearSend: sinon.spy(),
-}
-const historySpies = {
- push: sinon.spy(),
-}
-
-sinon.spy(SendHeader.prototype, 'onClose')
-
-describe('SendHeader Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<SendHeader
- clearSend={propsMethodSpies.clearSend}
- history={historySpies}
- titleKey={'mockTitleKey'}
- subtitleParams={[ 'mockSubtitleKey', 'mockVal']}
- />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
- })
-
- afterEach(() => {
- propsMethodSpies.clearSend.resetHistory()
- historySpies.push.resetHistory()
- SendHeader.prototype.onClose.resetHistory()
- })
-
- describe('onClose', () => {
- it('should call clearSend', () => {
- assert.equal(propsMethodSpies.clearSend.callCount, 0)
- wrapper.instance().onClose()
- assert.equal(propsMethodSpies.clearSend.callCount, 1)
- })
-
- it('should call history.push', () => {
- assert.equal(historySpies.push.callCount, 0)
- wrapper.instance().onClose()
- assert.equal(historySpies.push.callCount, 1)
- assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE)
- })
- })
-
- describe('render', () => {
- it('should render a PageContainerHeader compenent', () => {
- assert.equal(wrapper.find(PageContainerHeader).length, 1)
- })
-
- it('should pass the correct props to PageContainerHeader', () => {
- const {
- onClose,
- subtitle,
- title,
- } = wrapper.find(PageContainerHeader).props()
- assert.equal(subtitle, 'mockSubtitleKeymockVal')
- assert.equal(title, 'mockTitleKey')
- assert.equal(SendHeader.prototype.onClose.callCount, 0)
- onClose()
- assert.equal(SendHeader.prototype.onClose.callCount, 1)
- })
- })
-})
diff --git a/ui/app/components/send/send-header/tests/send-header-container.test.js b/ui/app/components/send/send-header/tests/send-header-container.test.js
deleted file mode 100644
index 41a7e8a89..000000000
--- a/ui/app/components/send/send-header/tests/send-header-container.test.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- clearSend: sinon.spy(),
-}
-
-proxyquire('../send-header.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../../actions': actionSpies,
- './send-header.selectors': {
- getTitleKey: (s) => `mockTitleKey:${s}`,
- getSubtitleParams: (s) => `mockSubtitleParams:${s}`,
- },
-})
-
-describe('send-header container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- titleKey: 'mockTitleKey:mockState',
- subtitleParams: 'mockSubtitleParams:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('clearSend()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.clearSend()
- assert(dispatchSpy.calledOnce)
- assert(actionSpies.clearSend.calledOnce)
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js
deleted file mode 100644
index 9b512aaf6..000000000
--- a/ui/app/components/send/send.component.js
+++ /dev/null
@@ -1,218 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import PersistentForm from '../../../lib/persistent-form'
-import {
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
- doesAmountErrorRequireUpdate,
-} from './send.utils'
-
-import SendHeader from './send-header/'
-import SendContent from './send-content/'
-import SendFooter from './send-footer/'
-
-export default class SendTransactionScreen extends PersistentForm {
-
- static propTypes = {
- amount: PropTypes.string,
- amountConversionRate: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- blockGasLimit: PropTypes.string,
- conversionRate: PropTypes.number,
- editingTransactionId: PropTypes.string,
- from: PropTypes.object,
- gasLimit: PropTypes.string,
- gasPrice: PropTypes.string,
- gasTotal: PropTypes.string,
- history: PropTypes.object,
- network: PropTypes.string,
- primaryCurrency: PropTypes.string,
- recentBlocks: PropTypes.array,
- selectedAddress: PropTypes.string,
- selectedToken: PropTypes.object,
- tokenBalance: PropTypes.string,
- tokenContract: PropTypes.object,
- fetchBasicGasEstimates: PropTypes.func,
- updateAndSetGasTotal: PropTypes.func,
- updateSendErrors: PropTypes.func,
- updateSendTokenBalance: PropTypes.func,
- scanQrCode: PropTypes.func,
- qrCodeDetected: PropTypes.func,
- qrCodeData: PropTypes.object,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- componentWillReceiveProps (nextProps) {
- if (nextProps.qrCodeData) {
- if (nextProps.qrCodeData.type === 'address') {
- const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase()
- const currentAddress = this.props.to && this.props.to.toLowerCase()
- if (currentAddress !== scannedAddress) {
- this.props.updateSendTo(scannedAddress)
- this.updateGas({ to: scannedAddress })
- // Clean up QR code data after handling
- this.props.qrCodeDetected(null)
- }
- }
- }
- }
-
- updateGas ({ to: updatedToAddress, amount: value, data } = {}) {
- const {
- amount,
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- recentBlocks,
- selectedAddress,
- selectedToken = {},
- to: currentToAddress,
- updateAndSetGasLimit,
- } = this.props
-
- updateAndSetGasLimit({
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- recentBlocks,
- selectedAddress,
- selectedToken,
- to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
- value: value || amount,
- data,
- })
- }
-
- componentDidUpdate (prevProps) {
- const {
- amount,
- amountConversionRate,
- conversionRate,
- from: { address, balance },
- gasTotal,
- network,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- updateSendErrors,
- updateSendTokenBalance,
- tokenContract,
- } = this.props
-
- const {
- from: { balance: prevBalance },
- gasTotal: prevGasTotal,
- tokenBalance: prevTokenBalance,
- network: prevNetwork,
- } = prevProps
-
- const uninitialized = [prevBalance, prevGasTotal].every(n => n === null)
-
- const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({
- balance,
- gasTotal,
- prevBalance,
- prevGasTotal,
- prevTokenBalance,
- selectedToken,
- tokenBalance,
- })
-
- if (amountErrorRequiresUpdate) {
- const amountErrorObject = getAmountErrorObject({
- amount,
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- })
- const gasFeeErrorObject = selectedToken
- ? getGasFeeErrorObject({
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- })
- : { gasFee: null }
- updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject))
- }
-
- if (!uninitialized) {
-
- if (network !== prevNetwork && network !== 'loading') {
- updateSendTokenBalance({
- selectedToken,
- tokenContract,
- address,
- })
- this.updateGas()
- }
- }
- }
-
- componentDidMount () {
- this.props.fetchBasicGasEstimates()
- .then(() => {
- this.updateGas()
- })
- }
-
- componentWillMount () {
- const {
- from: { address },
- selectedToken,
- tokenContract,
- updateSendTokenBalance,
- } = this.props
-
- updateSendTokenBalance({
- selectedToken,
- tokenContract,
- address,
- })
-
- // Show QR Scanner modal if ?scan=true
- if (window.location.search === '?scan=true') {
- this.props.scanQrCode()
-
- // Clear the queryString param after showing the modal
- const cleanUrl = location.href.split('?')[0]
- history.pushState({}, null, `${cleanUrl}`)
- window.location.hash = '#send'
- }
- }
-
- componentWillUnmount () {
- this.props.resetSendState()
- }
-
- render () {
- const { history, showHexData } = this.props
-
- return (
- <div className="page-container">
- <SendHeader history={history}/>
- <SendContent
- updateGas={(updateData) => this.updateGas(updateData)}
- scanQrCode={_ => this.props.scanQrCode()}
- showHexData={showHexData}
- />
- <SendFooter history={history}/>
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/send/send.constants.js b/ui/app/components/send/send.constants.js
deleted file mode 100644
index 490bc6fd2..000000000
--- a/ui/app/components/send/send.constants.js
+++ /dev/null
@@ -1,61 +0,0 @@
-const ethUtil = require('ethereumjs-util')
-const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
-
-const MIN_GAS_PRICE_DEC = '0'
-const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16)
-const MIN_GAS_LIMIT_DEC = '21000'
-const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
-
-const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, {
- fromDenomination: 'WEI',
- toDenomination: 'GWEI',
- fromNumericBase: 'hex',
- toNumericBase: 'hex',
- numberOfDecimals: 1,
-}))
-
-const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
-})
-
-const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb'
-
-const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds'
-const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens'
-const NEGATIVE_ETH_ERROR = 'negativeETH'
-const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient'
-const INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR = 'invalidAddressRecipientNotEthNetwork'
-const REQUIRED_ERROR = 'required'
-const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient'
-
-const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
- fromDenomination: 'GWEI',
- toDenomination: 'WEI',
- fromNumericBase: 'hex',
- toNumericBase: 'hex',
-}))
-
-const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
-const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
-
-module.exports = {
- INSUFFICIENT_FUNDS_ERROR,
- INSUFFICIENT_TOKENS_ERROR,
- INVALID_RECIPIENT_ADDRESS_ERROR,
- KNOWN_RECIPIENT_ADDRESS_ERROR,
- INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
- MIN_GAS_LIMIT_DEC,
- MIN_GAS_LIMIT_HEX,
- MIN_GAS_PRICE_DEC,
- MIN_GAS_PRICE_GWEI,
- MIN_GAS_PRICE_HEX,
- MIN_GAS_TOTAL,
- NEGATIVE_ETH_ERROR,
- ONE_GWEI_IN_WEI_HEX,
- REQUIRED_ERROR,
- SIMPLE_GAS_COST,
- TOKEN_TRANSFER_FUNCTION_SIGNATURE,
- BASE_TOKEN_GAS_COST,
-}
diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js
deleted file mode 100644
index 402e4bbe5..000000000
--- a/ui/app/components/send/send.container.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { connect } from 'react-redux'
-import SendEther from './send.component'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import {
- getAmountConversionRate,
- getBlockGasLimit,
- getConversionRate,
- getCurrentNetwork,
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getPrimaryCurrency,
- getRecentBlocks,
- getSelectedAddress,
- getSelectedToken,
- getSelectedTokenContract,
- getSelectedTokenToFiatRate,
- getSendAmount,
- getSendEditingTransactionId,
- getSendHexDataFeatureFlagState,
- getSendFromObject,
- getSendTo,
- getTokenBalance,
- getQrCodeData,
-} from './send.selectors'
-import {
- updateSendTo,
- updateSendTokenBalance,
- updateGasData,
- setGasTotal,
- showQrScanner,
- qrCodeDetected,
-} from '../../actions'
-import {
- resetSendState,
- updateSendErrors,
-} from '../../ducks/send.duck'
-import {
- fetchBasicGasEstimates,
-} from '../../ducks/gas.duck'
-import {
- calcGasTotal,
-} from './send.utils.js'
-
-import {
- SEND_ROUTE,
-} from '../../routes'
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(SendEther)
-
-function mapStateToProps (state) {
- return {
- amount: getSendAmount(state),
- amountConversionRate: getAmountConversionRate(state),
- blockGasLimit: getBlockGasLimit(state),
- conversionRate: getConversionRate(state),
- editingTransactionId: getSendEditingTransactionId(state),
- from: getSendFromObject(state),
- gasLimit: getGasLimit(state),
- gasPrice: getGasPrice(state),
- gasTotal: getGasTotal(state),
- network: getCurrentNetwork(state),
- primaryCurrency: getPrimaryCurrency(state),
- recentBlocks: getRecentBlocks(state),
- selectedAddress: getSelectedAddress(state),
- selectedToken: getSelectedToken(state),
- showHexData: getSendHexDataFeatureFlagState(state),
- to: getSendTo(state),
- tokenBalance: getTokenBalance(state),
- tokenContract: getSelectedTokenContract(state),
- tokenToFiatRate: getSelectedTokenToFiatRate(state),
- qrCodeData: getQrCodeData(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- updateAndSetGasLimit: ({
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- recentBlocks,
- selectedAddress,
- selectedToken,
- to,
- value,
- data,
- }) => {
- !editingTransactionId
- ? dispatch(updateGasData({ gasPrice, recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data }))
- : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
- },
- updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {
- dispatch(updateSendTokenBalance({
- selectedToken,
- tokenContract,
- address,
- }))
- },
- updateSendErrors: newError => dispatch(updateSendErrors(newError)),
- resetSendState: () => dispatch(resetSendState()),
- scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
- qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
- }
-}
diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js
deleted file mode 100644
index 47a49500f..000000000
--- a/ui/app/components/send/send.selectors.js
+++ /dev/null
@@ -1,291 +0,0 @@
-const { valuesFor } = require('../../util')
-const abi = require('human-standard-token-abi')
-const {
- multiplyCurrencies,
-} = require('../../conversion-util')
-const {
- getMetaMaskAccounts,
-} = require('../../selectors')
-const {
- estimateGasPriceFromRecentBlocks,
- calcGasTotal,
-} = require('./send.utils')
-import {
- getFastPriceEstimateInHexWEI,
-} from '../../selectors/custom-gas'
-
-const selectors = {
- accountsWithSendEtherInfoSelector,
- getAddressBook,
- getAmountConversionRate,
- getBlockGasLimit,
- getConversionRate,
- getCurrentAccountWithSendEtherInfo,
- getCurrentCurrency,
- getCurrentNetwork,
- getCurrentViewContext,
- getForceGasMin,
- getNativeCurrency,
- getGasLimit,
- getGasPrice,
- getGasPriceFromRecentBlocks,
- getGasTotal,
- getPrimaryCurrency,
- getRecentBlocks,
- getSelectedAccount,
- getSelectedAddress,
- getSelectedIdentity,
- getSelectedToken,
- getSelectedTokenContract,
- getSelectedTokenExchangeRate,
- getSelectedTokenToFiatRate,
- getSendAmount,
- getSendHexData,
- getSendHexDataFeatureFlagState,
- getSendEditingTransactionId,
- getSendErrors,
- getSendFrom,
- getSendFromBalance,
- getSendFromObject,
- getSendMaxModeState,
- getSendTo,
- getSendToAccounts,
- getSendWarnings,
- getTokenBalance,
- getTokenExchangeRate,
- getUnapprovedTxs,
- transactionsSelector,
- getQrCodeData,
-}
-
-module.exports = selectors
-
-function accountsWithSendEtherInfoSelector (state) {
- const accounts = getMetaMaskAccounts(state)
- const { identities } = state.metamask
-
- const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
- return Object.assign({}, account, identities[key])
- })
-
- return accountsWithSendEtherInfo
-}
-
-function getAddressBook (state) {
- return state.metamask.addressBook
-}
-
-function getAmountConversionRate (state) {
- return getSelectedToken(state)
- ? getSelectedTokenToFiatRate(state)
- : getConversionRate(state)
-}
-
-function getBlockGasLimit (state) {
- return state.metamask.currentBlockGasLimit
-}
-
-function getConversionRate (state) {
- return state.metamask.conversionRate
-}
-
-function getCurrentAccountWithSendEtherInfo (state) {
- const currentAddress = getSelectedAddress(state)
- const accounts = accountsWithSendEtherInfoSelector(state)
-
- return accounts.find(({ address }) => address === currentAddress)
-}
-
-function getCurrentCurrency (state) {
- return state.metamask.currentCurrency
-}
-
-function getNativeCurrency (state) {
- return state.metamask.nativeCurrency
-}
-
-function getCurrentNetwork (state) {
- return state.metamask.network
-}
-
-function getCurrentViewContext (state) {
- const { currentView = {} } = state.appState
- return currentView.context
-}
-
-function getForceGasMin (state) {
- return state.metamask.send.forceGasMin
-}
-
-function getGasLimit (state) {
- return state.metamask.send.gasLimit || '0'
-}
-
-function getGasPrice (state) {
- return state.metamask.send.gasPrice || getFastPriceEstimateInHexWEI(state)
-}
-
-function getGasPriceFromRecentBlocks (state) {
- return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks)
-}
-
-function getGasTotal (state) {
- return calcGasTotal(getGasLimit(state), getGasPrice(state))
-}
-
-function getPrimaryCurrency (state) {
- const selectedToken = getSelectedToken(state)
- return selectedToken && selectedToken.symbol
-}
-
-function getRecentBlocks (state) {
- return state.metamask.recentBlocks
-}
-
-function getSelectedAccount (state) {
- const accounts = getMetaMaskAccounts(state)
- const selectedAddress = getSelectedAddress(state)
-
- return accounts[selectedAddress]
-}
-
-function getSelectedAddress (state) {
- const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
-
- return selectedAddress
-}
-
-function getSelectedIdentity (state) {
- const selectedAddress = getSelectedAddress(state)
- const identities = state.metamask.identities
-
- return identities[selectedAddress]
-}
-
-function getSelectedToken (state) {
- const tokens = state.metamask.tokens || []
- const selectedTokenAddress = state.metamask.selectedTokenAddress
- const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
- const sendToken = state.metamask.send.token
-
- return selectedToken || sendToken || null
-}
-
-function getSelectedTokenContract (state) {
- const selectedToken = getSelectedToken(state)
-
- return selectedToken
- ? global.eth.contract(abi).at(selectedToken.address)
- : null
-}
-
-function getSelectedTokenExchangeRate (state) {
- const tokenExchangeRates = state.metamask.tokenExchangeRates
- const selectedToken = getSelectedToken(state) || {}
- const { symbol = '' } = selectedToken
- const pair = `${symbol.toLowerCase()}_eth`
- const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {}
-
- return tokenExchangeRate
-}
-
-function getSelectedTokenToFiatRate (state) {
- const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
- const conversionRate = getConversionRate(state)
-
- const tokenToFiatRate = multiplyCurrencies(
- conversionRate,
- selectedTokenExchangeRate,
- { toNumericBase: 'dec' }
- )
-
- return tokenToFiatRate
-}
-
-function getSendAmount (state) {
- return state.metamask.send.amount
-}
-
-function getSendHexData (state) {
- return state.metamask.send.data
-}
-
-function getSendHexDataFeatureFlagState (state) {
- return state.metamask.featureFlags.sendHexData
-}
-
-function getSendEditingTransactionId (state) {
- return state.metamask.send.editingTransactionId
-}
-
-function getSendErrors (state) {
- return state.send.errors
-}
-
-function getSendFrom (state) {
- return state.metamask.send.from
-}
-
-function getSendFromBalance (state) {
- const from = getSendFrom(state) || getSelectedAccount(state)
- return from.balance
-}
-
-function getSendFromObject (state) {
- return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
-}
-
-function getSendMaxModeState (state) {
- return state.metamask.send.maxModeOn
-}
-
-function getSendTo (state) {
- return state.metamask.send.to
-}
-
-function getSendToAccounts (state) {
- const fromAccounts = accountsWithSendEtherInfoSelector(state)
- const addressBookAccounts = getAddressBook(state)
- const allAccounts = [...fromAccounts, ...addressBookAccounts]
- // TODO: figure out exactly what the below returns and put a descriptive variable name on it
- return Object.entries(allAccounts).map(([key, account]) => account)
-}
-
-function getSendWarnings (state) {
- return state.send.warnings
-}
-
-function getTokenBalance (state) {
- return state.metamask.send.tokenBalance
-}
-
-function getTokenExchangeRate (state, tokenSymbol) {
- const pair = `${tokenSymbol.toLowerCase()}_eth`
- const tokenExchangeRates = state.metamask.tokenExchangeRates
- const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
-
- return tokenExchangeRate
-}
-
-function getUnapprovedTxs (state) {
- return state.metamask.unapprovedTxs
-}
-
-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)
-
- 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 getQrCodeData (state) {
- return state.appState.qrCodeData
-}
diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js
deleted file mode 100644
index d78b7736f..000000000
--- a/ui/app/components/send/send.utils.js
+++ /dev/null
@@ -1,332 +0,0 @@
-const {
- addCurrencies,
- conversionUtil,
- conversionGTE,
- multiplyCurrencies,
- conversionGreaterThan,
- conversionLessThan,
-} = require('../../conversion-util')
-const {
- calcTokenAmount,
-} = require('../../token-util')
-const {
- BASE_TOKEN_GAS_COST,
- INSUFFICIENT_FUNDS_ERROR,
- INSUFFICIENT_TOKENS_ERROR,
- NEGATIVE_ETH_ERROR,
- ONE_GWEI_IN_WEI_HEX,
- SIMPLE_GAS_COST,
- TOKEN_TRANSFER_FUNCTION_SIGNATURE,
-} = require('./send.constants')
-const abi = require('ethereumjs-abi')
-const ethUtil = require('ethereumjs-util')
-
-module.exports = {
- addGasBuffer,
- calcGasTotal,
- calcTokenBalance,
- doesAmountErrorRequireUpdate,
- estimateGas,
- estimateGasPriceFromRecentBlocks,
- generateTokenTransferData,
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
- isBalanceSufficient,
- isTokenBalanceSufficient,
- removeLeadingZeroes,
-}
-
-function calcGasTotal (gasLimit = '0', gasPrice = '0') {
- return multiplyCurrencies(gasLimit, gasPrice, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-}
-
-function isBalanceSufficient ({
- amount = '0x0',
- amountConversionRate = 1,
- balance = '0x0',
- conversionRate = 1,
- gasTotal = '0x0',
- primaryCurrency,
-}) {
- const totalAmount = addCurrencies(amount, gasTotal, {
- aBase: 16,
- bBase: 16,
- toNumericBase: 'hex',
- })
-
- const balanceIsSufficient = conversionGTE(
- {
- value: balance,
- fromNumericBase: 'hex',
- fromCurrency: primaryCurrency,
- conversionRate,
- },
- {
- value: totalAmount,
- fromNumericBase: 'hex',
- conversionRate: Number(amountConversionRate) || conversionRate,
- fromCurrency: primaryCurrency,
- },
- )
-
- return balanceIsSufficient
-}
-
-function isTokenBalanceSufficient ({
- amount = '0x0',
- tokenBalance,
- decimals,
-}) {
- const amountInDec = conversionUtil(amount, {
- fromNumericBase: 'hex',
- })
-
- const tokenBalanceIsSufficient = conversionGTE(
- {
- value: tokenBalance,
- fromNumericBase: 'hex',
- },
- {
- value: calcTokenAmount(amountInDec, decimals),
- },
- )
-
- return tokenBalanceIsSufficient
-}
-
-function getAmountErrorObject ({
- amount,
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
-}) {
- let insufficientFunds = false
- if (gasTotal && conversionRate && !selectedToken) {
- insufficientFunds = !isBalanceSufficient({
- amount,
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- })
- }
-
- let inSufficientTokens = false
- if (selectedToken && tokenBalance !== null) {
- const { decimals } = selectedToken
- inSufficientTokens = !isTokenBalanceSufficient({
- tokenBalance,
- amount,
- decimals,
- })
- }
-
- const amountLessThanZero = conversionGreaterThan(
- { value: 0, fromNumericBase: 'dec' },
- { value: amount, fromNumericBase: 'hex' },
- )
-
- let amountError = null
-
- if (insufficientFunds) {
- amountError = INSUFFICIENT_FUNDS_ERROR
- } else if (inSufficientTokens) {
- amountError = INSUFFICIENT_TOKENS_ERROR
- } else if (amountLessThanZero) {
- amountError = NEGATIVE_ETH_ERROR
- }
-
- return { amount: amountError }
-}
-
-function getGasFeeErrorObject ({
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
-}) {
- let gasFeeError = null
-
- if (gasTotal && conversionRate) {
- const insufficientFunds = !isBalanceSufficient({
- amount: '0x0',
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- })
-
- if (insufficientFunds) {
- gasFeeError = INSUFFICIENT_FUNDS_ERROR
- }
- }
-
- return { gasFee: gasFeeError }
-}
-
-function calcTokenBalance ({ selectedToken, usersToken }) {
- const { decimals } = selectedToken || {}
- return calcTokenAmount(usersToken.balance.toString(), decimals).toString(16)
-}
-
-function doesAmountErrorRequireUpdate ({
- balance,
- gasTotal,
- prevBalance,
- prevGasTotal,
- prevTokenBalance,
- selectedToken,
- tokenBalance,
-}) {
- const balanceHasChanged = balance !== prevBalance
- const gasTotalHasChange = gasTotal !== prevGasTotal
- const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance
- const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged
-
- return amountErrorRequiresUpdate
-}
-
-async function estimateGas ({
- selectedAddress,
- selectedToken,
- blockGasLimit,
- to,
- value,
- data,
- gasPrice,
- estimateGasMethod,
-}) {
- const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }
-
- // if recipient has no code, gas is 21k max:
- if (!selectedToken && !data) {
- const code = Boolean(to) && await global.eth.getCode(to)
- // Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
- const codeIsEmpty = !code || code === '0x' || code === '0x0'
- if (codeIsEmpty) {
- return SIMPLE_GAS_COST
- }
- } else if (selectedToken && !to) {
- return BASE_TOKEN_GAS_COST
- }
-
- 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 (!value || value === '0') {
- paramsForGasEstimate.value = '0xff'
- }
- }
-
- // if not, fall back to block gasLimit
- paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit || '0x5208', 0.95, {
- multiplicandBase: 16,
- multiplierBase: 10,
- roundDown: '0',
- toNumericBase: 'hex',
- }))
- // run tx
- return new Promise((resolve, reject) => {
- return estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => {
- if (err) {
- const simulationFailed = (
- err.message.includes('Transaction execution error.') ||
- err.message.includes('gas required exceeds allowance or always failing transaction')
- )
- if (simulationFailed) {
- const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5)
- return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
- } else {
- return reject(err)
- }
- }
- const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5)
- return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
- })
- })
-}
-
-function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) {
- const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 10,
- numberOfDecimals: '0',
- })
- const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 10,
- numberOfDecimals: '0',
- })
-
- // if initialGasLimit is above blockGasLimit, dont modify it
- if (conversionGreaterThan(
- { value: initialGasLimitHex, fromNumericBase: 'hex' },
- { value: upperGasLimit, fromNumericBase: 'hex' },
- )) return initialGasLimitHex
- // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
- if (conversionLessThan(
- { value: bufferedGasLimit, fromNumericBase: 'hex' },
- { value: upperGasLimit, fromNumericBase: 'hex' },
- )) return bufferedGasLimit
- // otherwise use blockGasLimit
- return upperGasLimit
-}
-
-function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) {
- if (!selectedToken) return
- return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
- abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]),
- x => ('00' + x.toString(16)).slice(-2)
- ).join('')
-}
-
-function estimateGasPriceFromRecentBlocks (recentBlocks) {
- // Return 1 gwei if no blocks have been observed:
- if (!recentBlocks || recentBlocks.length === 0) {
- return ONE_GWEI_IN_WEI_HEX
- }
-
- const lowestPrices = recentBlocks.map((block) => {
- if (!block.gasPrices || block.gasPrices.length < 1) {
- return ONE_GWEI_IN_WEI_HEX
- }
- return block.gasPrices.reduce((currentLowest, next) => {
- return parseInt(next, 16) < parseInt(currentLowest, 16) ? next : currentLowest
- })
- })
- .sort((a, b) => parseInt(a, 16) > parseInt(b, 16) ? 1 : -1)
-
- return lowestPrices[Math.floor(lowestPrices.length / 2)]
-}
-
-function getToAddressForGasUpdate (...addresses) {
- return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase()
-}
-
-function removeLeadingZeroes (str) {
- return str.replace(/^0*(?=\d)/, '')
-}
diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js
deleted file mode 100644
index 81955cc1d..000000000
--- a/ui/app/components/send/tests/send-component.test.js
+++ /dev/null
@@ -1,354 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import timeout from '../../../../lib/test-timeout'
-
-import SendHeader from '../send-header/send-header.container'
-import SendContent from '../send-content/send-content.component'
-import SendFooter from '../send-footer/send-footer.container'
-
-const mockBasicGasEstimates = {
- blockTime: 'mockBlockTime',
-}
-
-const propsMethodSpies = {
- updateAndSetGasLimit: sinon.spy(),
- updateSendErrors: sinon.spy(),
- updateSendTokenBalance: sinon.spy(),
- resetSendState: sinon.spy(),
- fetchBasicGasEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
- fetchGasEstimates: sinon.spy(),
-}
-const utilsMethodStubs = {
- getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
- getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }),
- doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance),
-}
-
-const SendTransactionScreen = proxyquire('../send.component.js', {
- './send.utils': utilsMethodStubs,
-}).default
-
-sinon.spy(SendTransactionScreen.prototype, 'componentDidMount')
-sinon.spy(SendTransactionScreen.prototype, 'updateGas')
-
-describe('Send Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<SendTransactionScreen
- amount={'mockAmount'}
- amountConversionRate={'mockAmountConversionRate'}
- blockGasLimit={'mockBlockGasLimit'}
- conversionRate={10}
- editingTransactionId={'mockEditingTransactionId'}
- fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
- fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
- from={ { address: 'mockAddress', balance: 'mockBalance' } }
- gasLimit={'mockGasLimit'}
- gasPrice={'mockGasPrice'}
- gasTotal={'mockGasTotal'}
- history={{ mockProp: 'history-abc'}}
- network={'3'}
- primaryCurrency={'mockPrimaryCurrency'}
- recentBlocks={['mockBlock']}
- selectedAddress={'mockSelectedAddress'}
- selectedToken={'mockSelectedToken'}
- showHexData={true}
- tokenBalance={'mockTokenBalance'}
- tokenContract={'mockTokenContract'}
- updateAndSetGasLimit={propsMethodSpies.updateAndSetGasLimit}
- updateSendErrors={propsMethodSpies.updateSendErrors}
- updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
- resetSendState={propsMethodSpies.resetSendState}
- />)
- })
-
- afterEach(() => {
- SendTransactionScreen.prototype.componentDidMount.resetHistory()
- SendTransactionScreen.prototype.updateGas.resetHistory()
- utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
- utilsMethodStubs.getAmountErrorObject.resetHistory()
- utilsMethodStubs.getGasFeeErrorObject.resetHistory()
- propsMethodSpies.fetchBasicGasEstimates.resetHistory()
- propsMethodSpies.updateAndSetGasLimit.resetHistory()
- propsMethodSpies.updateSendErrors.resetHistory()
- propsMethodSpies.updateSendTokenBalance.resetHistory()
- })
-
- it('should call componentDidMount', () => {
- assert(SendTransactionScreen.prototype.componentDidMount.calledOnce)
- })
-
- describe('componentDidMount', () => {
- it('should call props.fetchBasicGasAndTimeEstimates', () => {
- propsMethodSpies.fetchBasicGasEstimates.resetHistory()
- assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0)
- wrapper.instance().componentDidMount()
- assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 1)
- })
-
- it('should call this.updateGas', async () => {
- SendTransactionScreen.prototype.updateGas.resetHistory()
- propsMethodSpies.updateSendErrors.resetHistory()
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
- wrapper.instance().componentDidMount()
- await timeout(250)
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
- })
- })
-
- describe('componentWillUnmount', () => {
- it('should call this.props.resetSendState', () => {
- propsMethodSpies.resetSendState.resetHistory()
- assert.equal(propsMethodSpies.resetSendState.callCount, 0)
- wrapper.instance().componentWillUnmount()
- assert.equal(propsMethodSpies.resetSendState.callCount, 1)
- })
- })
-
- describe('componentDidUpdate', () => {
- it('should call doesAmountErrorRequireUpdate with the expected params', () => {
- utilsMethodStubs.getAmountErrorObject.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: '',
- },
- })
- assert(utilsMethodStubs.doesAmountErrorRequireUpdate.calledOnce)
- assert.deepEqual(
- utilsMethodStubs.doesAmountErrorRequireUpdate.getCall(0).args[0],
- {
- balance: 'mockBalance',
- gasTotal: 'mockGasTotal',
- prevBalance: '',
- prevGasTotal: undefined,
- prevTokenBalance: undefined,
- selectedToken: 'mockSelectedToken',
- tokenBalance: 'mockTokenBalance',
- }
- )
- })
-
- it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => {
- utilsMethodStubs.getAmountErrorObject.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'mockBalance',
- },
- })
- assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 0)
- })
-
- it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => {
- utilsMethodStubs.getAmountErrorObject.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- })
- assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 1)
- assert.deepEqual(
- utilsMethodStubs.getAmountErrorObject.getCall(0).args[0],
- {
- amount: 'mockAmount',
- amountConversionRate: 'mockAmountConversionRate',
- balance: 'mockBalance',
- conversionRate: 10,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- selectedToken: 'mockSelectedToken',
- tokenBalance: 'mockTokenBalance',
- }
- )
- })
-
- it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => {
- utilsMethodStubs.getGasFeeErrorObject.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- })
- assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1)
- assert.deepEqual(
- utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0],
- {
- amountConversionRate: 'mockAmountConversionRate',
- balance: 'mockBalance',
- conversionRate: 10,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- selectedToken: 'mockSelectedToken',
- }
- )
- })
-
- it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => {
- utilsMethodStubs.getGasFeeErrorObject.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: { address: 'mockAddress', balance: 'mockBalance' },
- })
- assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
- })
-
- it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => {
- utilsMethodStubs.getGasFeeErrorObject.resetHistory()
- wrapper.setProps({ selectedToken: null })
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- })
- assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
- })
-
- it('should call updateSendErrors with the expected params if selectedToken is falsy', () => {
- propsMethodSpies.updateSendErrors.resetHistory()
- wrapper.setProps({ selectedToken: null })
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- })
- assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendErrors.getCall(0).args[0],
- { amount: 'mockAmountError', gasFee: null }
- )
- })
-
- it('should call updateSendErrors with the expected params if selectedToken is truthy', () => {
- propsMethodSpies.updateSendErrors.resetHistory()
- wrapper.setProps({ selectedToken: 'someToken' })
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- })
- assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendErrors.getCall(0).args[0],
- { amount: 'mockAmountError', gasFee: 'mockGasFeeError' }
- )
- })
-
- it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => {
- SendTransactionScreen.prototype.updateGas.resetHistory()
- propsMethodSpies.updateSendTokenBalance.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- network: '3',
- })
- assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
- })
-
- it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => {
- wrapper.setProps({ network: 'loading' })
- SendTransactionScreen.prototype.updateGas.resetHistory()
- propsMethodSpies.updateSendTokenBalance.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- network: '3',
- })
- assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
- })
-
- it('should call updateSendTokenBalance and this.updateGas with the correct params', () => {
- SendTransactionScreen.prototype.updateGas.resetHistory()
- propsMethodSpies.updateSendTokenBalance.resetHistory()
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- network: '2',
- })
- assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendTokenBalance.getCall(0).args[0],
- {
- selectedToken: 'mockSelectedToken',
- tokenContract: 'mockTokenContract',
- address: 'mockAddress',
- }
- )
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
- assert.deepEqual(
- SendTransactionScreen.prototype.updateGas.getCall(0).args,
- []
- )
- })
- })
-
- describe('updateGas', () => {
- it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => {
- propsMethodSpies.updateAndSetGasLimit.resetHistory()
- wrapper.instance().updateGas()
- assert.equal(propsMethodSpies.updateAndSetGasLimit.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0],
- {
- blockGasLimit: 'mockBlockGasLimit',
- editingTransactionId: 'mockEditingTransactionId',
- gasLimit: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- recentBlocks: ['mockBlock'],
- selectedAddress: 'mockSelectedAddress',
- selectedToken: 'mockSelectedToken',
- to: '',
- value: 'mockAmount',
- data: undefined,
- }
- )
- })
-
- it('should call updateAndSetGasLimit with the correct params if a to prop is passed', () => {
- propsMethodSpies.updateAndSetGasLimit.resetHistory()
- wrapper.setProps({ to: 'someAddress' })
- wrapper.instance().updateGas()
- assert.equal(
- propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to,
- 'someaddress',
- )
- })
-
- it('should call updateAndSetGasLimit with to set to lowercase if passed', () => {
- propsMethodSpies.updateAndSetGasLimit.resetHistory()
- wrapper.instance().updateGas({ to: '0xABC' })
- assert.equal(propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, '0xabc')
- })
- })
-
- describe('render', () => {
- it('should render a page-container class', () => {
- assert.equal(wrapper.find('.page-container').length, 1)
- })
-
- it('should render SendHeader, SendContent and SendFooter', () => {
- assert.equal(wrapper.find(SendHeader).length, 1)
- assert.equal(wrapper.find(SendContent).length, 1)
- assert.equal(wrapper.find(SendFooter).length, 1)
- })
-
- it('should pass the history prop to SendHeader and SendFooter', () => {
- assert.deepEqual(
- wrapper.find(SendFooter).props(),
- {
- history: { mockProp: 'history-abc' },
- }
- )
- })
-
- 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
deleted file mode 100644
index 19b6563e6..000000000
--- a/ui/app/components/send/tests/send-container.test.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- updateSendTokenBalance: sinon.spy(),
- updateGasData: sinon.spy(),
- setGasTotal: sinon.spy(),
-}
-const duckActionSpies = {
- updateSendErrors: sinon.spy(),
- resetSendState: sinon.spy(),
-}
-
-proxyquire('../send.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- 'react-router-dom': { withRouter: () => {} },
- 'recompose': { compose: (arg1, arg2) => () => arg2() },
- './send.selectors': {
- getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
- getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`,
- getConversionRate: (s) => `mockConversionRate:${s}`,
- getCurrentNetwork: (s) => `mockNetwork:${s}`,
- getGasLimit: (s) => `mockGasLimit:${s}`,
- getGasPrice: (s) => `mockGasPrice:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
- getRecentBlocks: (s) => `mockRecentBlocks:${s}`,
- getSelectedAddress: (s) => `mockSelectedAddress:${s}`,
- 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}`,
- getSendFromObject: (s) => `mockFrom:${s}`,
- getTokenBalance: (s) => `mockTokenBalance:${s}`,
- getQrCodeData: (s) => `mockQrCodeData:${s}`,
- },
- '../../actions': actionSpies,
- '../../ducks/send.duck': duckActionSpies,
- './send.utils.js': {
- calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
- },
-})
-
-describe('send container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- amount: 'mockAmount:mockState',
- amountConversionRate: 'mockAmountConversionRate:mockState',
- blockGasLimit: 'mockBlockGasLimit:mockState',
- conversionRate: 'mockConversionRate:mockState',
- editingTransactionId: 'mockEditingTransactionId:mockState',
- from: 'mockFrom:mockState',
- gasLimit: 'mockGasLimit:mockState',
- gasPrice: 'mockGasPrice:mockState',
- gasTotal: 'mockGasTotal:mockState',
- network: 'mockNetwork:mockState',
- primaryCurrency: 'mockPrimaryCurrency:mockState',
- recentBlocks: 'mockRecentBlocks:mockState',
- selectedAddress: 'mockSelectedAddress:mockState',
- selectedToken: 'mockSelectedToken:mockState',
- showHexData: 'mockSendHexDataFeatureFlagState:mockState',
- to: 'mockTo:mockState',
- tokenBalance: 'mockTokenBalance:mockState',
- tokenContract: 'mockTokenContract:mockState',
- tokenToFiatRate: 'mockTokenToFiatRate:mockState',
- qrCodeData: 'mockQrCodeData:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('updateAndSetGasLimit()', () => {
- const mockProps = {
- blockGasLimit: 'mockBlockGasLimit',
- editingTransactionId: '0x2',
- gasLimit: '0x3',
- gasPrice: '0x4',
- recentBlocks: ['mockBlock'],
- selectedAddress: '0x4',
- selectedToken: { address: '0x1' },
- to: 'mockTo',
- value: 'mockValue',
- data: undefined,
- }
-
- it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
- mapDispatchToPropsObject.updateAndSetGasLimit(mockProps)
- assert(dispatchSpy.calledOnce)
- assert.equal(
- actionSpies.setGasTotal.getCall(0).args[0],
- '0x30x4'
- )
- })
-
- it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
- const { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
- mapDispatchToPropsObject.updateAndSetGasLimit(
- Object.assign({}, mockProps, {editingTransactionId: false})
- )
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- actionSpies.updateGasData.getCall(0).args[0],
- { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data }
- )
- })
- })
-
- describe('updateSendTokenBalance()', () => {
- const mockProps = {
- address: '0x10',
- tokenContract: '0x00a',
- selectedToken: {address: '0x1'},
- }
-
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendTokenBalance(Object.assign({}, mockProps))
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- actionSpies.updateSendTokenBalance.getCall(0).args[0],
- mockProps
- )
- })
- })
-
- describe('updateSendErrors()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendErrors('mockError')
- assert(dispatchSpy.calledOnce)
- assert.equal(
- duckActionSpies.updateSendErrors.getCall(0).args[0],
- 'mockError'
- )
- })
- })
-
- describe('resetSendState()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.resetSendState()
- assert(dispatchSpy.calledOnce)
- assert.equal(
- duckActionSpies.resetSendState.getCall(0).args.length,
- 0
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js
deleted file mode 100644
index 48fa09392..000000000
--- a/ui/app/components/send/tests/send-utils.test.js
+++ /dev/null
@@ -1,527 +0,0 @@
-import assert from 'assert'
-import sinon from 'sinon'
-import proxyquire from 'proxyquire'
-import {
- BASE_TOKEN_GAS_COST,
- ONE_GWEI_IN_WEI_HEX,
- SIMPLE_GAS_COST,
-} from '../send.constants'
-const {
- addCurrencies,
- subtractCurrencies,
-} = require('../../../conversion-util')
-
-const {
- INSUFFICIENT_FUNDS_ERROR,
- INSUFFICIENT_TOKENS_ERROR,
-} = require('../send.constants')
-
-const stubs = {
- addCurrencies: sinon.stub().callsFake((a, b, obj) => {
- if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2))
- if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2))
- return a + b
- }),
- conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
- conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
- multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
- calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
- rawEncode: sinon.stub().returns([16, 1100]),
- conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
- conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value),
-}
-
-const sendUtils = proxyquire('../send.utils.js', {
- '../../conversion-util': {
- addCurrencies: stubs.addCurrencies,
- conversionUtil: stubs.conversionUtil,
- conversionGTE: stubs.conversionGTE,
- multiplyCurrencies: stubs.multiplyCurrencies,
- conversionGreaterThan: stubs.conversionGreaterThan,
- conversionLessThan: stubs.conversionLessThan,
- },
- '../../token-util': { calcTokenAmount: stubs.calcTokenAmount },
- 'ethereumjs-abi': {
- rawEncode: stubs.rawEncode,
- },
-})
-
-const {
- calcGasTotal,
- estimateGas,
- doesAmountErrorRequireUpdate,
- estimateGasPriceFromRecentBlocks,
- generateTokenTransferData,
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
- calcTokenBalance,
- isBalanceSufficient,
- isTokenBalanceSufficient,
- removeLeadingZeroes,
-} = sendUtils
-
-describe('send utils', () => {
-
- describe('calcGasTotal()', () => {
- it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => {
- const result = calcGasTotal(12, 15)
- assert.equal(result, '12x15')
- const call_ = stubs.multiplyCurrencies.getCall(0).args
- assert.deepEqual(
- call_,
- [12, 15, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- } ]
- )
- })
- })
-
- describe('doesAmountErrorRequireUpdate()', () => {
- const config = {
- 'should return true if balances are different': {
- balance: 0,
- prevBalance: 1,
- expectedResult: true,
- },
- 'should return true if gasTotals are different': {
- gasTotal: 0,
- prevGasTotal: 1,
- expectedResult: true,
- },
- 'should return true if token balances are different': {
- tokenBalance: 0,
- prevTokenBalance: 1,
- selectedToken: 'someToken',
- expectedResult: true,
- },
- 'should return false if they are all the same': {
- balance: 1,
- prevBalance: 1,
- gasTotal: 1,
- prevGasTotal: 1,
- tokenBalance: 1,
- prevTokenBalance: 1,
- selectedToken: 'someToken',
- expectedResult: false,
- },
- }
- Object.entries(config).map(([description, obj]) => {
- it(description, () => {
- assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult)
- })
- })
-
- })
-
- describe('generateTokenTransferData()', () => {
- it('should return undefined if not passed a selected token', () => {
- assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined)
- })
-
- it('should call abi.rawEncode with the correct params', () => {
- stubs.rawEncode.resetHistory()
- generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true})
- assert.deepEqual(
- stubs.rawEncode.getCall(0).args,
- [['address', 'uint256'], ['mockAddress', '0xab']]
- )
- })
-
- it('should return encoded token transfer data', () => {
- assert.equal(
- generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}),
- '0xa9059cbb104c'
- )
- })
- })
-
- describe('getAmountErrorObject()', () => {
- const config = {
- 'should return insufficientFunds error if isBalanceSufficient returns false': {
- amount: 15,
- amountConversionRate: 2,
- balance: 1,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
- },
- 'should not return insufficientFunds error if selectedToken is truthy': {
- amount: '0x0',
- amountConversionRate: 2,
- balance: 1,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- selectedToken: { symbole: 'DEF', decimals: 0 },
- decimals: 0,
- tokenBalance: 'sometokenbalance',
- expectedResult: { amount: null },
- },
- 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
- amount: '0x10',
- amountConversionRate: 2,
- balance: 100,
- conversionRate: 3,
- decimals: 10,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- selectedToken: 'someToken',
- tokenBalance: 123,
- expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR },
- },
- }
- Object.entries(config).map(([description, obj]) => {
- it(description, () => {
- assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult)
- })
- })
- })
-
- describe('getGasFeeErrorObject()', () => {
- const config = {
- 'should return insufficientFunds error if isBalanceSufficient returns false': {
- amountConversionRate: 2,
- balance: 16,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR },
- },
- 'should return null error if isBalanceSufficient returns true': {
- amountConversionRate: 2,
- balance: 16,
- conversionRate: 3,
- gasTotal: 15,
- primaryCurrency: 'ABC',
- expectedResult: { gasFee: null },
- },
- }
- Object.entries(config).map(([description, obj]) => {
- it(description, () => {
- assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult)
- })
- })
- })
-
- describe('calcTokenBalance()', () => {
- it('should return the calculated token blance', () => {
- assert.equal(calcTokenBalance({
- selectedToken: {
- decimals: 11,
- },
- usersToken: {
- balance: 20,
- },
- }), 'calc:2011')
- })
- })
-
- describe('isBalanceSufficient()', () => {
- it('should correctly call addCurrencies and return the result of calling conversionGTE', () => {
- stubs.conversionGTE.resetHistory()
- const result = isBalanceSufficient({
- amount: 15,
- amountConversionRate: 2,
- balance: 100,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- })
- assert.deepEqual(
- stubs.addCurrencies.getCall(0).args,
- [
- 15, 17, {
- aBase: 16,
- bBase: 16,
- toNumericBase: 'hex',
- },
- ]
- )
- assert.deepEqual(
- stubs.conversionGTE.getCall(0).args,
- [
- {
- value: 100,
- fromNumericBase: 'hex',
- fromCurrency: 'ABC',
- conversionRate: 3,
- },
- {
- value: 32,
- fromNumericBase: 'hex',
- conversionRate: 2,
- fromCurrency: 'ABC',
- },
- ]
- )
-
- assert.equal(result, true)
- })
- })
-
- describe('isTokenBalanceSufficient()', () => {
- it('should correctly call conversionUtil and return the result of calling conversionGTE', () => {
- stubs.conversionGTE.resetHistory()
- stubs.conversionUtil.resetHistory()
- const result = isTokenBalanceSufficient({
- amount: '0x10',
- tokenBalance: 123,
- decimals: 10,
- })
- assert.deepEqual(
- stubs.conversionUtil.getCall(0).args,
- [
- '0x10', {
- fromNumericBase: 'hex',
- },
- ]
- )
- assert.deepEqual(
- stubs.conversionGTE.getCall(0).args,
- [
- {
- value: 123,
- fromNumericBase: 'hex',
- },
- {
- value: 'calc:1610',
- },
- ]
- )
-
- assert.equal(result, false)
- })
- })
-
- describe('estimateGas', () => {
- const baseMockParams = {
- blockGasLimit: '0x64',
- selectedAddress: 'mockAddress',
- to: '0xisContract',
- estimateGasMethod: sinon.stub().callsFake(
- ({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 = {
- from: 'mockAddress',
- gas: '0x64x0.95',
- to: '0xisContract',
- value: '0xff',
- }
-
- beforeEach(() => {
- global.eth = {
- getCode: sinon.stub().callsFake(
- (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
- ),
- }
- })
-
- afterEach(() => {
- baseMockParams.estimateGasMethod.resetHistory()
- global.eth.getCode.resetHistory()
- })
-
- it('should call ethQuery.estimateGas with the expected params', async () => {
- const result = await sendUtils.estimateGas(baseMockParams)
- assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
- assert.deepEqual(
- baseMockParams.estimateGasMethod.getCall(0).args[0],
- Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
- )
- assert.equal(result, '0xabc16')
- })
-
- it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => {
- const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' }))
- assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
- assert.deepEqual(
- baseMockParams.estimateGasMethod.getCall(0).args[0],
- Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' })
- )
- assert.equal(result, '0xabc16x1.5')
- })
-
- it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
- const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams))
- assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
- assert.deepEqual(
- baseMockParams.estimateGasMethod.getCall(0).args[0],
- Object.assign({}, baseExpectedCall, {
- gasPrice: undefined,
- value: '0x0',
- data: '0xa9059cbb104c',
- to: 'mockAddress',
- })
- )
- 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: '0xff', 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' }))
- assert.equal(result, SIMPLE_GAS_COST)
- })
-
- it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => {
- assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
- const result = await estimateGas(Object.assign({}, baseMockParams, { to: null }))
- assert.equal(result, SIMPLE_GAS_COST)
- })
-
- it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => {
- assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
- const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
- assert.notEqual(result, SIMPLE_GAS_COST)
- })
-
- it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => {
- const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } }))
- assert.equal(result, BASE_TOKEN_GAS_COST)
- })
-
- it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
- const result = await estimateGas(Object.assign({}, baseMockParams, {
- to: 'isContract willFailBecauseOf:Transaction execution error.',
- }))
- assert.equal(result, '0x64x0.95')
- })
-
- it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => {
- const result = await estimateGas(Object.assign({}, baseMockParams, {
- to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.',
- }))
- assert.equal(result, '0x64x0.95')
- })
-
- it(`should reject other errors`, async () => {
- try {
- await estimateGas(Object.assign({}, baseMockParams, {
- to: 'isContract willFailBecauseOf:some other error',
- }))
- } catch (err) {
- assert.equal(err.message, 'some other error')
- }
- })
- })
-
- describe('estimateGasPriceFromRecentBlocks', () => {
- const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
- aBase: 16,
- bBase: 16,
- toNumericBase: 'hex',
- })
- const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', {
- aBase: 16,
- bBase: 16,
- toNumericBase: 'hex',
- })
- const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
- aBase: 16,
- bBase: 16,
- toNumericBase: 'hex',
- })
-
- it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => {
- assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX)
- })
-
- it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => {
- assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX)
- })
-
- it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => {
- const mockRecentBlocks = [
- { gasPrices: null },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
- ]
- assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
- })
-
- it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => {
- const mockRecentBlocks = [
- { gasPrices: [] },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
- ]
- assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
- })
-
- it(`should return the middle value of all blocks lowest prices`, () => {
- const mockRecentBlocks = [
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
- { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
- ]
- assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE)
- })
-
- it(`should work if a block has multiple gas prices`, () => {
- const mockRecentBlocks = [
- { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] },
- { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] },
- { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] },
- ]
- assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
- })
- })
-
- describe('getToAddressForGasUpdate()', () => {
- it('should return empty string if all params are undefined or null', () => {
- assert.equal(getToAddressForGasUpdate(undefined, null), '')
- })
-
- it('should return the first string that is not defined or null in lower case', () => {
- assert.equal(getToAddressForGasUpdate('A', null), 'a')
- assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
- })
- })
-
- describe('removeLeadingZeroes()', () => {
- it('should remove leading zeroes from int when user types', () => {
- assert.equal(removeLeadingZeroes('0'), '0')
- assert.equal(removeLeadingZeroes('1'), '1')
- assert.equal(removeLeadingZeroes('00'), '0')
- assert.equal(removeLeadingZeroes('01'), '1')
- })
-
- it('should remove leading zeroes from int when user copy/paste', () => {
- assert.equal(removeLeadingZeroes('001'), '1')
- })
-
- it('should remove leading zeroes from float when user types', () => {
- assert.equal(removeLeadingZeroes('0.'), '0.')
- assert.equal(removeLeadingZeroes('0.0'), '0.0')
- assert.equal(removeLeadingZeroes('0.00'), '0.00')
- assert.equal(removeLeadingZeroes('0.001'), '0.001')
- assert.equal(removeLeadingZeroes('0.10'), '0.10')
- })
-
- it('should remove leading zeroes from float when user copy/paste', () => {
- assert.equal(removeLeadingZeroes('00.1'), '0.1')
- })
- })
-})
diff --git a/ui/app/components/send/to-autocomplete.component.js b/ui/app/components/send/to-autocomplete.component.js
deleted file mode 100644
index 9e270db75..000000000
--- a/ui/app/components/send/to-autocomplete.component.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import React, {Component} from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import AccountListItem from '../send/account-list-item/account-list-item.component'
-
-
-export default class ToAutoComplete extends Component {
-
- static propTypes = {
- dropdownOpen: PropTypes.bool,
- openDropdown: PropTypes.func,
- closeDropdown: PropTypes.func,
- onChange: PropTypes.func,
- to: PropTypes.string,
- accounts: PropTypes.array,
- inError: PropTypes.bool,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- state = {
- accountsToRender: [],
- }
-
- getListItemIcon (listItemAddress, toAddress) {
- return toAddress && listItemAddress === toAddress
- ? <i className={'fa fa-check fa-lg'}
- style={{
- color: '#02c9b1',
- }}
- />
- : null
- }
-
- renderDropdown () {
- const {
- closeDropdown,
- onChange,
- to,
- } = this.props
- const {accountsToRender} = this.state
-
- if (!accountsToRender.length) {
- return null
- }
-
- return (
- <div>
- <div className={'send-v2__from-dropdown__close-area'} onClick={closeDropdown} />
- <div className={'send-v2__from-dropdown__list'}>
- {accountsToRender.map((account, i) => (
- <AccountListItem
- key={i}
- account={account}
- className={'account-list-item__dropdown'}
- handleClick={() => {
- onChange(account.address)
- closeDropdown()
- }}
- icon={this.getListItemIcon(account.address, to)}
- displayBalance={false}
- displayAddress={true}
- />
- ))}
- </div>
- </div>
- )
- }
-
- handleInputEvent (event = {}, cb) {
- const {
- to,
- accounts,
- closeDropdown,
- openDropdown,
- } = this.props
-
- const matchingAccounts = accounts.filter(({address}) => address.match(to || ''))
- const matches = matchingAccounts.length
-
- if (!matches || matchingAccounts[0].address === to) {
- this.setState({accountsToRender: []})
- event.target && event.target.select()
- closeDropdown()
- } else {
- this.setState({accountsToRender: matchingAccounts})
- openDropdown()
- }
- cb && cb(event.target.value)
- }
-
- componentDidUpdate (nextProps) {
- if (this.props.to !== nextProps.to) {
- this.handleInputEvent()
- }
- }
-
- render () {
- const {
- to,
- dropdownOpen,
- onChange,
- inError,
- } = this.props
-
- return (
- <div className={'send-v2__to-autocomplete'}>
- <input
- className={classnames('send-v2__to-autocomplete__input', {
- 'send-v2__error-border': inError,
- })}
- placeholder={this.context.t('recipientAddress')}
- value={to}
- onChange={event => onChange(event.target.value)}
- onFocus={event => this.handleInputEvent(event)}
- style={{
- borderColor: inError ? 'red' : null,
- }}
- />
- {
- to
- ? null
- : <i className={'fa fa-caret-down fa-lg send-v2__to-autocomplete__down-caret'}
- onClick={() => this.handleInputEvent()}
- style={{
- style: {color: '#dedede'},
- }}
- />
- }
- {
- dropdownOpen
- ? this.renderDropdown()
- : null
- }
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js
deleted file mode 100644
index 39d15dfa7..000000000
--- a/ui/app/components/send/to-autocomplete/to-autocomplete.js
+++ /dev/null
@@ -1,129 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-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,
-}
-
-module.exports = connect()(ToAutoComplete)
-
-
-inherits(ToAutoComplete, Component)
-function ToAutoComplete () {
- Component.call(this)
-
- this.state = { accountsToRender: [] }
-}
-
-ToAutoComplete.prototype.getListItemIcon = function (listItemAddress, toAddress) {
- const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
-
- return toAddress && listItemAddress === toAddress
- ? listItemIcon
- : null
-}
-
-ToAutoComplete.prototype.renderDropdown = function () {
- const {
- closeDropdown,
- onChange,
- to,
- } = this.props
- const { accountsToRender } = this.state
-
- return accountsToRender.length && h('div', {}, [
-
- h('div.send-v2__from-dropdown__close-area', {
- onClick: closeDropdown,
- }),
-
- h('div.send-v2__from-dropdown__list', {}, [
-
- ...accountsToRender.map(account => h(AccountListItem, {
- account,
- className: 'account-list-item__dropdown',
- handleClick: () => {
- onChange(checksumAddress(account.address))
- closeDropdown()
- },
- icon: this.getListItemIcon(account.address, to),
- displayBalance: false,
- displayAddress: true,
- })),
-
- ]),
-
- ])
-}
-
-ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) {
- const {
- to,
- accounts,
- closeDropdown,
- openDropdown,
- } = this.props
-
- const matchingAccounts = accounts.filter(({ address }) => address.match(to || ''))
- const matches = matchingAccounts.length
-
- if (!matches || matchingAccounts[0].address === to) {
- this.setState({ accountsToRender: [] })
- event.target && event.target.select()
- closeDropdown()
- } else {
- this.setState({ accountsToRender: matchingAccounts })
- openDropdown()
- }
- cb && cb(event.target.value)
-}
-
-ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) {
- if (this.props.to !== nextProps.to) {
- this.handleInputEvent()
- }
-}
-
-ToAutoComplete.prototype.render = function () {
- const {
- to,
- dropdownOpen,
- onChange,
- inError,
- qrScanner,
- } = this.props
-
- return h('div.send-v2__to-autocomplete', {}, [
-
- h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
- placeholder: this.context.t('recipientAddress'),
- className: inError ? `send-v2__error-border` : '',
- value: to,
- onChange: event => onChange(event.target.value),
- onFocus: event => this.handleInputEvent(event),
- style: {
- borderColor: inError ? 'red' : null,
- },
- }),
- qrScanner && h(Tooltip, {
- title: this.context.t('scanQrCode'),
- position: 'bottom',
- }, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, {
- style: { color: '#33333' },
- onClick: () => this.props.scanQrCode(),
- })),
- !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
- style: { color: '#dedede' },
- onClick: () => this.handleInputEvent(),
- }),
-
- dropdownOpen && this.renderDropdown(),
-
- ])
-}
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
deleted file mode 100644
index 7d3436dc3..000000000
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ /dev/null
@@ -1,186 +0,0 @@
-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, FLAT_VARIANT } from './sender-to-recipient.constants'
-import { checksumAddress } from '../../util'
-
-const variantHash = {
- [DEFAULT_VARIANT]: 'sender-to-recipient--default',
- [CARDS_VARIANT]: 'sender-to-recipient--cards',
- [FLAT_VARIANT]: 'sender-to-recipient--flat',
-}
-
-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, FLAT_VARIANT]),
- addressOnly: PropTypes.bool,
- assetImage: PropTypes.string,
- onRecipientClick: PropTypes.func,
- onSenderClick: PropTypes.func,
- }
-
- static defaultProps = {
- variant: DEFAULT_VARIANT,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- state = {
- senderAddressCopied: false,
- 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, addressOnly, onRecipientClick } = this.props
- const checksummedRecipientAddress = checksumAddress(recipientAddress)
-
- return (
- <div
- className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
- onClick={() => {
- this.setState({ recipientAddressCopied: true })
- copyToClipboard(checksummedRecipientAddress)
- if (onRecipientClick) {
- onRecipientClick()
- }
- }}
- >
- { this.renderRecipientIdenticon() }
- <Tooltip
- position="bottom"
- title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
- wrapperClassName="sender-to-recipient__tooltip-wrapper"
- containerClassName="sender-to-recipient__tooltip-container"
- onHidden={() => this.setState({ recipientAddressCopied: false })}
- >
- <div className="sender-to-recipient__name">
- {
- addressOnly
- ? `${t('to')}: ${checksummedRecipientAddress}`
- : (recipientName || this.context.t('newContract'))
- }
- </div>
- </Tooltip>
- </div>
- )
- }
-
- renderRecipientWithoutAddress () {
- return (
- <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>
- )
- }
-
- renderArrow () {
- return this.props.variant === DEFAULT_VARIANT
- ? (
- <div className="sender-to-recipient__arrow-container">
- <div className="sender-to-recipient__arrow-circle">
- <img
- height={15}
- width={15}
- src="./images/arrow-right.svg"
- />
- </div>
- </div>
- ) : (
- <div className="sender-to-recipient__arrow-container">
- <img
- height={20}
- src="./images/caret-right.svg"
- />
- </div>
- )
- }
-
- render () {
- const { senderAddress, recipientAddress, variant, onSenderClick } = this.props
- const checksummedSenderAddress = checksumAddress(senderAddress)
-
- return (
- <div className={classnames('sender-to-recipient', variantHash[variant])}>
- <div
- className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
- onClick={() => {
- this.setState({ senderAddressCopied: true })
- copyToClipboard(checksummedSenderAddress)
- if (onSenderClick) {
- onSenderClick()
- }
- }}
- >
- { this.renderSenderIdenticon() }
- { this.renderSenderAddress() }
- </div>
- { this.renderArrow() }
- {
- recipientAddress
- ? this.renderRecipientWithAddress()
- : this.renderRecipientWithoutAddress()
- }
- </div>
- )
- }
-}
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
deleted file mode 100644
index b22c01e8f..000000000
--- a/ui/app/components/shapeshift-form.js
+++ /dev/null
@@ -1,256 +0,0 @@
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const PropTypes = require('prop-types')
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const classnames = require('classnames')
-const qrcode = require('qrcode-generator')
-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,
- tokenExchangeRates,
- selectedAddress,
- } = state.metamask
- const { warning } = state.appState
-
- return {
- coinOptions,
- tokenExchangeRates,
- selectedAddress,
- warning,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- shapeShiftSubview: () => dispatch(shapeShiftSubview()),
- pairUpdate: coin => dispatch(pairUpdate(coin)),
- buyWithShapeShift: data => dispatch(buyWithShapeShift(data)),
- }
-}
-
-ShapeshiftForm.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftForm)
-
-
-inherits(ShapeshiftForm, Component)
-function ShapeshiftForm () {
- Component.call(this)
-
- this.state = {
- depositCoin: 'btc',
- refundAddress: '',
- showQrCode: false,
- depositAddress: '',
- errorMessage: '',
- isLoading: false,
- bought: false,
- }
-}
-
-ShapeshiftForm.prototype.getCoinPair = function () {
- return `${this.state.depositCoin.toUpperCase()}_ETH`
-}
-
-ShapeshiftForm.prototype.componentWillMount = function () {
- this.props.shapeShiftSubview()
-}
-
-ShapeshiftForm.prototype.onCoinChange = function (coin) {
- this.setState({
- depositCoin: coin,
- errorMessage: '',
- })
- this.props.pairUpdate(coin)
-}
-
-ShapeshiftForm.prototype.onBuyWithShapeShift = function () {
- this.setState({
- isLoading: true,
- showQrCode: true,
- })
-
- const {
- buyWithShapeShift,
- selectedAddress: withdrawal,
- } = this.props
- const {
- refundAddress: returnAddress,
- depositCoin,
- } = this.state
- const pair = `${depositCoin}_eth`
- const data = {
- withdrawal,
- pair,
- returnAddress,
- // Public api key
- 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6',
- }
-
- if (isValidAddress(withdrawal)) {
- buyWithShapeShift(data)
- .then(d => this.setState({
- showQrCode: true,
- depositAddress: d.deposit,
- isLoading: false,
- }))
- .catch(() => this.setState({
- showQrCode: false,
- errorMessage: this.context.t('invalidRequest'),
- isLoading: false,
- }))
- }
-}
-
-ShapeshiftForm.prototype.renderMetadata = function (label, value) {
- return h('div', {className: 'shapeshift-form__metadata-wrapper'}, [
-
- h('div.shapeshift-form__metadata-label', {}, [
- h('span', `${label}:`),
- ]),
-
- h('div.shapeshift-form__metadata-value', {}, [
- h('span', value),
- ]),
-
- ])
-}
-
-ShapeshiftForm.prototype.renderMarketInfo = function () {
- const { tokenExchangeRates } = this.props
- const {
- limit,
- rate,
- minimum,
- } = tokenExchangeRates[this.getCoinPair()] || {}
-
- return h('div.shapeshift-form__metadata', {}, [
-
- this.renderMetadata(this.context.t('status'), limit ? this.context.t('available') : this.context.t('unavailable')),
- this.renderMetadata(this.context.t('limit'), limit),
- this.renderMetadata(this.context.t('exchangeRate'), rate),
- this.renderMetadata(this.context.t('min'), minimum),
-
- ])
-}
-
-ShapeshiftForm.prototype.renderQrCode = function () {
- const { depositAddress, isLoading, depositCoin } = this.state
- const qrImage = qrcode(4, 'M')
- qrImage.addData(depositAddress)
- qrImage.make()
-
- return h('div.shapeshift-form', {}, [
-
- h('div.shapeshift-form__deposit-instruction', [
- this.context.t('depositCoin', [depositCoin.toUpperCase()]),
- ]),
-
- h('div', depositAddress),
-
- h('div.shapeshift-form__qr-code', [
- isLoading
- ? h('img', {
- src: 'images/loading.svg',
- style: { width: '60px'},
- })
- : h('div', {
- dangerouslySetInnerHTML: { __html: qrImage.createTableTag(4) },
- }),
- ]),
-
- this.renderMarketInfo(),
-
- ])
-}
-
-
-ShapeshiftForm.prototype.render = function () {
- const { coinOptions, btnClass, warning } = this.props
- const { errorMessage, showQrCode, depositAddress } = this.state
- const { tokenExchangeRates } = this.props
- const token = tokenExchangeRates[this.getCoinPair()]
-
- return h('div.shapeshift-form-wrapper', [
- showQrCode
- ? this.renderQrCode()
- : h('div.modal-shapeshift-form', [
- h('div.shapeshift-form__selectors', [
-
- h('div.shapeshift-form__selector', [
-
- h('div.shapeshift-form__selector-label', this.context.t('deposit')),
-
- h(SimpleDropdown, {
- selectedOption: this.state.depositCoin,
- onSelect: (coin) => this.onCoinChange(coin),
- options: Object.entries(coinOptions).map(([coin]) => ({
- value: coin.toLowerCase(),
- displayValue: coin,
- })),
- }),
-
- ]),
-
- h('div.icon.shapeshift-form__caret', {
- style: { backgroundImage: 'url(images/caret-right.svg)'},
- }),
-
- h('div.shapeshift-form__selector', [
-
- h('div.shapeshift-form__selector-label', [
- this.context.t('receive'),
- ]),
-
- h('div.shapeshift-form__selector-input', ['ETH']),
-
- ]),
-
- ]),
-
- warning && h('div.shapeshift-form__address-input-label', warning),
-
- !warning && h('div', {
- className: classnames('shapeshift-form__address-input-wrapper', {
- 'shapeshift-form__address-input-wrapper--error': errorMessage,
- }),
- }, [
-
- h('div.shapeshift-form__address-input-label', [
- this.context.t('refundAddress'),
- ]),
-
- h('input.shapeshift-form__address-input', {
- type: 'text',
- onChange: e => this.setState({
- refundAddress: e.target.value,
- errorMessage: '',
- }),
- }),
-
- h('divshapeshift-form__address-input-error-message', [errorMessage]),
- ]),
-
- !warning && this.renderMarketInfo(),
-
- ]),
-
- !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
deleted file mode 100644
index 2d08bbddc..000000000
--- a/ui/app/components/shift-list-item.js
+++ /dev/null
@@ -1,204 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const explorerLink = require('etherscan-link').createExplorerLink
-const actions = require('../actions')
-const { formatDate, addressSummary } = require('../util')
-
-const CopyButton = require('./copyButton')
-const EthBalance = require('./eth-balance')
-const Tooltip = require('./tooltip')
-
-
-ShiftListItem.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(ShiftListItem)
-
-
-function mapStateToProps (state) {
- return {
- selectedAddress: state.metamask.selectedAddress,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
- }
-}
-
-inherits(ShiftListItem, Component)
-
-function ShiftListItem () {
- Component.call(this)
-}
-
-ShiftListItem.prototype.render = function () {
- return h('div.transaction-list-item.tx-list-clickable', {
- style: {
- paddingTop: '20px',
- paddingBottom: '20px',
- justifyContent: 'space-around',
- alignItems: 'center',
- flexDirection: 'row',
- },
- }, [
- h('div', {
- style: {
- width: '0px',
- position: 'relative',
- bottom: '19px',
- },
- }, [
- h('img', {
- src: 'https://shapeshift.io/logo.png',
- style: {
- height: '35px',
- width: '132px',
- position: 'absolute',
- clip: 'rect(0px,30px,34px,0px)',
- },
- }),
- ]),
-
- this.renderInfo(),
- this.renderUtilComponents(),
- ])
-}
-
-ShiftListItem.prototype.renderUtilComponents = function () {
- var props = this.props
- const { conversionRate, currentCurrency } = props
-
- switch (props.response.status) {
- case 'no_deposits':
- return h('.flex-row', [
- h(CopyButton, {
- value: this.props.depositAddress,
- }),
- h(Tooltip, {
- title: this.context.t('qrCode'),
- }, [
- h('i.fa.fa-qrcode.pointer.pop-hover', {
- onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)),
- style: {
- margin: '5px',
- marginLeft: '23px',
- marginRight: '12px',
- fontSize: '20px',
- color: '#F7861C',
- },
- }),
- ]),
- ])
- case 'received':
- return h('.flex-row')
-
- case 'complete':
- return h('.flex-row', [
- h(CopyButton, {
- value: this.props.response.transaction,
- }),
- h(EthBalance, {
- value: `${props.response.outgoingCoin}`,
- conversionRate,
- currentCurrency,
- width: '55px',
- shorten: true,
- needsParse: false,
- incoming: true,
- style: {
- fontSize: '15px',
- color: '#01888C',
- },
- }),
- ])
-
- case 'failed':
- return ''
- default:
- return ''
- }
-}
-
-ShiftListItem.prototype.renderInfo = function () {
- var props = this.props
- switch (props.response.status) {
- case 'no_deposits':
- return h('.flex-column', {
- style: {
- overflow: 'hidden',
- },
- }, [
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, this.context.t('toETHviaShapeShift', [props.depositType])),
- h('div', this.context.t('noDeposits')),
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, formatDate(props.time)),
- ])
- case 'received':
- return h('.flex-column', {
- style: {
- width: '200px',
- overflow: 'hidden',
- },
- }, [
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, this.context.t('toETHviaShapeShift', [props.depositType])),
- h('div', this.context.t('conversionProgress')),
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, formatDate(props.time)),
- ])
- case 'complete':
- var url = explorerLink(props.response.transaction, parseInt('1'))
-
- return h('.flex-column.pointer', {
- style: {
- width: '200px',
- overflow: 'hidden',
- },
- onClick: () => global.platform.openWindow({ url }),
- }, [
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, this.context.t('fromShapeShift')),
- h('div', formatDate(props.time)),
- h('div', {
- style: {
- fontSize: 'x-small',
- color: '#ABA9AA',
- width: '100%',
- },
- }, addressSummary(props.response.transaction)),
- ])
-
- case 'failed':
- return h('span.error', '(' + this.context.t('failed') + ')')
- default:
- return ''
- }
-}
diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/sidebars/index.scss
deleted file mode 100644
index b9845d564..000000000
--- a/ui/app/components/sidebars/index.scss
+++ /dev/null
@@ -1,81 +0,0 @@
-@import './sidebar-content';
-
-.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);
- }
-
- @media screen and (max-width: $break-small) {
- width: 100%;
- left: 0%;
- }
-}
-
-.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/signature-request.js b/ui/app/components/signature-request.js
deleted file mode 100644
index 25bd9a7b1..000000000
--- a/ui/app/components/signature-request.js
+++ /dev/null
@@ -1,316 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-import Identicon from './identicon'
-const connect = require('react-redux').connect
-const ethUtil = require('ethereumjs-util')
-const classnames = require('classnames')
-const { compose } = require('recompose')
-const { withRouter } = require('react-router-dom')
-const { ObjectInspector } = require('react-inspector')
-
-import AccountDropdownMini from './account-dropdown-mini'
-
-const actions = require('../actions')
-const { conversionUtil } = require('../conversion-util')
-
-const {
- getSelectedAccount,
- getCurrentAccountWithSendEtherInfo,
- getSelectedAddress,
- accountsWithSendEtherInfoSelector,
- conversionRateSelector,
-} = require('../selectors.js')
-
-import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
-import Button from './button'
-
-const { DEFAULT_ROUTE } = require('../routes')
-
-function mapStateToProps (state) {
- return {
- balance: getSelectedAccount(state).balance,
- selectedAccount: getCurrentAccountWithSendEtherInfo(state),
- selectedAddress: getSelectedAddress(state),
- requester: null,
- requesterAddress: null,
- accounts: accountsWithSendEtherInfoSelector(state),
- conversionRate: conversionRateSelector(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- goHome: () => dispatch(actions.goHome()),
- clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
- }
-}
-
-SignatureRequest.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(SignatureRequest)
-
-
-inherits(SignatureRequest, Component)
-function SignatureRequest (props) {
- Component.call(this)
-
- this.state = {
- selectedAccount: props.selectedAccount,
- }
-}
-
-SignatureRequest.prototype.renderHeader = function () {
- return h('div.request-signature__header', [
-
- h('div.request-signature__header-background'),
-
- h('div.request-signature__header__text', this.context.t('sigRequest')),
-
- h('div.request-signature__header__tip-container', [
- h('div.request-signature__header__tip'),
- ]),
-
- ])
-}
-
-SignatureRequest.prototype.renderAccountDropdown = function () {
- const { selectedAccount } = this.state
-
- const {
- accounts,
- } = this.props
-
- return h('div.request-signature__account', [
-
- h('div.request-signature__account-text', [this.context.t('account') + ':']),
-
- h(AccountDropdownMini, {
- selectedAccount,
- accounts,
- disabled: true,
- }),
-
- ])
-}
-
-SignatureRequest.prototype.renderBalance = function () {
- const { balance, conversionRate } = this.props
-
- const balanceInEther = conversionUtil(balance, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- numberOfDecimals: 6,
- conversionRate,
- })
-
- return h('div.request-signature__balance', [
-
- h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
-
- h('div.request-signature__balance-value', `${balanceInEther} ETH`),
-
- ])
-}
-
-SignatureRequest.prototype.renderAccountInfo = function () {
- return h('div.request-signature__account-info', [
-
- this.renderAccountDropdown(),
-
- this.renderRequestIcon(),
-
- this.renderBalance(),
-
- ])
-}
-
-SignatureRequest.prototype.renderRequestIcon = function () {
- const { requesterAddress } = this.props
-
- return h('div.request-signature__request-icon', [
- h(Identicon, {
- diameter: 40,
- address: requesterAddress,
- }),
- ])
-}
-
-SignatureRequest.prototype.renderRequestInfo = function () {
- return h('div.request-signature__request-info', [
-
- h('div.request-signature__headline', [
- this.context.t('yourSigRequested'),
- ]),
-
- ])
-}
-
-SignatureRequest.prototype.msgHexToText = function (hex) {
- try {
- const stripped = ethUtil.stripHexPrefix(hex)
- const buff = Buffer.from(stripped, 'hex')
- return buff.length === 32 ? hex : buff.toString('utf8')
- } catch (e) {
- return 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, version } } = txData
-
- if (type === 'personal_sign') {
- rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }]
- } else if (type === 'eth_signTypedData') {
- rows = data
- } else if (type === 'eth_sign') {
- rows = [{ name: this.context.t('message'), value: data }]
- notice = [this.context.t('signNotice'),
- h('span.request-signature__help-link', {
- onClick: () => {
- global.platform.openWindow({
- url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751',
- })
- },
- }, this.context.t('learnMore'))]
- }
-
- return h('div.request-signature__body', {}, [
-
- this.renderAccountInfo(),
-
- this.renderRequestInfo(),
-
- h('div.request-signature__notice', {
- className: classnames({
- 'request-signature__notice': type === 'personal_sign' || type === 'eth_signTypedData',
- 'request-signature__warning': type === 'eth_sign',
- }),
- }, [notice]),
-
- h('div.request-signature__rows', type === 'eth_signTypedData' && version === 'V3' ?
- this.renderTypedDataV3(data) :
- rows.map(({ name, value }) => {
- if (typeof value === 'boolean') {
- value = value.toString()
- }
- return h('div.request-signature__row', [
- h('div.request-signature__row-title', [`${name}:`]),
- h('div.request-signature__row-value', value),
- ])
- }),
- ),
- ])
-}
-
-SignatureRequest.prototype.renderFooter = function () {
- const {
- signPersonalMessage,
- signTypedMessage,
- cancelPersonalMessage,
- cancelTypedMessage,
- signMessage,
- cancelMessage,
- } = this.props
-
- const { txData } = this.props
- const { type } = txData
-
- let cancel
- let sign
- if (type === 'personal_sign') {
- cancel = cancelPersonalMessage
- sign = signPersonalMessage
- } else if (type === 'eth_signTypedData') {
- cancel = cancelTypedMessage
- sign = signTypedMessage
- } else if (type === 'eth_sign') {
- cancel = cancelMessage
- sign = signMessage
- }
-
- return h('div.request-signature__footer', [
- h(Button, {
- type: 'default',
- large: true,
- className: 'request-signature__footer__cancel-button',
- onClick: event => {
- cancel(event).then(() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Sign Request',
- name: 'Cancel',
- },
- })
- this.props.clearConfirmTransaction()
- this.props.history.push(DEFAULT_ROUTE)
- })
- },
- }, this.context.t('cancel')),
- h(Button, {
- type: 'primary',
- large: true,
- className: 'request-signature__footer__sign-button',
- onClick: event => {
- sign(event).then(() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Sign Request',
- name: 'Confirm',
- },
- })
- this.props.clearConfirmTransaction()
- this.props.history.push(DEFAULT_ROUTE)
- })
- },
- }, this.context.t('sign')),
- ])
-}
-
-SignatureRequest.prototype.render = function () {
- return (
-
- h('div.request-signature__container', [
-
- this.renderHeader(),
-
- this.renderBody(),
-
- this.renderFooter(),
-
- ])
-
- )
-
-}
diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js
deleted file mode 100644
index 0016a09c1..000000000
--- a/ui/app/components/tab-bar.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const classnames = require('classnames')
-
-class TabBar extends Component {
- render () {
- const { tabs = [], onSelect, isActive } = this.props
-
- return (
- h('.tab-bar', {}, [
- tabs.map(({ key, content }) => {
- return h('div', {
- className: classnames('tab-bar__tab pointer', {
- 'tab-bar__tab--active': isActive(key, content),
- }),
- onClick: () => onSelect(key),
- key,
- }, content)
- }),
- h('div.tab-bar__tab.tab-bar__grow-tab'),
- ])
- )
- }
-}
-
-TabBar.propTypes = {
- isActive: PropTypes.func.isRequired,
- tabs: PropTypes.array,
- onSelect: PropTypes.func,
-}
-
-module.exports = TabBar
diff --git a/ui/app/components/tabs/index.scss b/ui/app/components/tabs/index.scss
deleted file mode 100644
index a3b42f8e3..000000000
--- a/ui/app/components/tabs/index.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import './tab/index';
-
-.tabs {
- &__list {
- display: flex;
- justify-content: flex-start;
- background-color: #f9fafa;
- border-bottom: 1px solid $geyser;
- padding: 0 16px;
- }
-}
diff --git a/ui/app/components/text-field/text-field.stories.js b/ui/app/components/text-field/text-field.stories.js
deleted file mode 100644
index c00873b8a..000000000
--- a/ui/app/components/text-field/text-field.stories.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import TextField from './'
-
-storiesOf('TextField', module)
- .add('text', () =>
- <TextField
- label="Text"
- type="text"
- />
- )
- .add('password', () =>
- <TextField
- label="Password"
- type="password"
- />
- )
- .add('error', () =>
- <TextField
- type="text"
- label="Name"
- error="Invalid value"
- />
- )
- .add('Mascara text', () =>
- <TextField
- label="Text"
- type="text"
- largeLabel
- />
- )
- .add('Material text', () =>
- <TextField
- label="Text"
- type="text"
- material
- />
- )
- .add('Material password', () =>
- <TextField
- label="Password"
- type="password"
- material
- />
- )
- .add('Material error', () =>
- <TextField
- type="text"
- label="Name"
- error="Invalid value"
- material
- />
- )
diff --git a/ui/app/components/token-balance/token-balance.container.js b/ui/app/components/token-balance/token-balance.container.js
deleted file mode 100644
index adc001f83..000000000
--- a/ui/app/components/token-balance/token-balance.container.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { connect } from 'react-redux'
-import { compose } from 'recompose'
-import withTokenTracker from '../../higher-order-components/with-token-tracker'
-import TokenBalance from './token-balance.component'
-import selectors from '../../selectors'
-
-const mapStateToProps = state => {
- return {
- userAddress: selectors.getSelectedAddress(state),
- }
-}
-
-export default compose(
- connect(mapStateToProps),
- withTokenTracker
-)(TokenBalance)
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
deleted file mode 100644
index d9c80b4f4..000000000
--- a/ui/app/components/token-cell.js
+++ /dev/null
@@ -1,177 +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
-import Identicon from './identicon'
-const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
-const selectors = require('../selectors')
-const actions = require('../actions')
-const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
-
-const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js')
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- currentCurrency: state.metamask.currentCurrency,
- selectedTokenAddress: state.metamask.selectedTokenAddress,
- userAddress: selectors.getSelectedAddress(state),
- contractExchangeRates: state.metamask.contractExchangeRates,
- conversionRate: state.metamask.conversionRate,
- sidebarOpen: state.appState.sidebar.isOpen,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setSelectedToken: address => dispatch(actions.setSelectedToken(address)),
- hideSidebar: () => dispatch(actions.hideSidebar()),
- }
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell)
-
-inherits(TokenCell, Component)
-function TokenCell () {
- Component.call(this)
-
- this.state = {
- tokenMenuOpen: false,
- }
-}
-
-TokenCell.contextTypes = {
- metricsEvent: PropTypes.func,
-}
-
-TokenCell.prototype.render = function () {
- const { tokenMenuOpen } = this.state
- const props = this.props
- const {
- address,
- symbol,
- string,
- network,
- setSelectedToken,
- selectedTokenAddress,
- contractExchangeRates,
- conversionRate,
- hideSidebar,
- sidebarOpen,
- currentCurrency,
- // userAddress,
- image,
- } = props
- let currentTokenToFiatRate
- let currentTokenInFiat
- let formattedFiat = ''
-
- if (contractExchangeRates[address]) {
- currentTokenToFiatRate = multiplyCurrencies(
- contractExchangeRates[address],
- conversionRate
- )
- currentTokenInFiat = conversionUtil(string, {
- fromNumericBase: 'dec',
- fromCurrency: symbol,
- toCurrency: currentCurrency.toUpperCase(),
- numberOfDecimals: 2,
- conversionRate: currentTokenToFiatRate,
- })
- formattedFiat = currentTokenInFiat.toString() === '0'
- ? ''
- : `${currentTokenInFiat} ${currentCurrency.toUpperCase()}`
- }
-
- const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
-
- return (
- h('div.token-list-item', {
- className: `token-list-item ${selectedTokenAddress === address ? 'token-list-item--active' : ''}`,
- // style: { cursor: network === '1' ? 'pointer' : 'default' },
- // onClick: this.view.bind(this, address, userAddress, network),
- onClick: () => {
- setSelectedToken(address)
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Token Menu',
- name: 'Clicked Token',
- },
- })
- selectedTokenAddress !== address && sidebarOpen && hideSidebar()
- },
- }, [
-
- h(Identicon, {
- className: 'token-list-item__identicon',
- diameter: 50,
- address,
- network,
- image,
- }),
-
- h('div.token-list-item__balance-ellipsis', null, [
- h('div.token-list-item__balance-wrapper', null, [
- h('div.token-list-item__token-balance', `${string || 0}`),
- h('div.token-list-item__token-symbol', symbol),
- showFiat && h('div.token-list-item__fiat-amount', {
- style: {},
- }, formattedFiat),
- ]),
-
- h('i.fa.fa-ellipsis-h.fa-lg.token-list-item__ellipsis.cursor-pointer', {
- onClick: (e) => {
- e.stopPropagation()
- this.setState({ tokenMenuOpen: true })
- },
- }),
-
- ]),
-
-
- tokenMenuOpen && h(TokenMenuDropdown, {
- onClose: () => this.setState({ tokenMenuOpen: false }),
- token: { symbol, address },
- }),
-
- /*
- h('button', {
- onClick: this.send.bind(this, address),
- }, 'SEND'),
- */
-
- ])
- )
-}
-
-TokenCell.prototype.send = function (address, event) {
- event.preventDefault()
- event.stopPropagation()
- const url = tokenFactoryFor(address)
- if (url) {
- navigateTo(url)
- }
-}
-
-TokenCell.prototype.view = function (address, userAddress, network, event) {
- const url = etherscanLinkFor(address, userAddress, network)
- if (url) {
- navigateTo(url)
- }
-}
-
-function navigateTo (url) {
- global.platform.openWindow({ url })
-}
-
-function etherscanLinkFor (tokenAddress, address, network) {
- const prefix = prefixForNetwork(network)
- return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
-}
-
-function tokenFactoryFor (tokenAddress) {
- return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
-}
-
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
deleted file mode 100644
index f49846449..000000000
--- a/ui/app/components/token-currency-display/token-currency-display.component.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import CurrencyDisplay from '../currency-display'
-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: '',
- suffix: '',
- }
-
- 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: suffix = '' } = token
- const tokenData = getTokenData(data)
-
- let displayValue
-
- if (tokenData && tokenData.params && tokenData.params.length) {
- const tokenValue = getTokenValue(tokenData.params)
- displayValue = calcTokenAmount(tokenValue, decimals).toString()
- }
-
- this.setState({ displayValue, suffix })
- }
-
- render () {
- const { displayValue, suffix } = this.state
-
- return (
- <CurrencyDisplay
- {...this.props}
- displayValue={displayValue}
- suffix={suffix}
- />
- )
- }
-}
diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js
deleted file mode 100644
index 398b762ec..000000000
--- a/ui/app/components/token-input/token-input.component.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import UnitInput from '../unit-input'
-import CurrencyDisplay from '../currency-display'
-import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
-import ethUtil from 'ethereumjs-util'
-import { conversionUtil, multiplyCurrencies } from '../../conversion-util'
-import { ETH } from '../../constants/common'
-
-/**
- * Component that allows user to enter token values as a number, and props receive a converted
- * hex value. props.value, used as a default or forced value, should be a hex value, which
- * gets converted into a decimal value.
- */
-export default class TokenInput extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- currentCurrency: PropTypes.string,
- onChange: PropTypes.func,
- onBlur: PropTypes.func,
- value: PropTypes.string,
- suffix: PropTypes.string,
- showFiat: PropTypes.bool,
- hideConversion: PropTypes.bool,
- selectedToken: PropTypes.object,
- selectedTokenExchangeRate: PropTypes.number,
- }
-
- constructor (props) {
- super(props)
-
- const { value: hexValue } = props
- const decimalValue = hexValue ? this.getValue(props) : 0
-
- this.state = {
- decimalValue,
- hexValue,
- }
- }
-
- componentDidUpdate (prevProps) {
- const { value: prevPropsHexValue } = prevProps
- const { value: propsHexValue } = this.props
- const { hexValue: stateHexValue } = this.state
-
- if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
- const decimalValue = this.getValue(this.props)
- this.setState({ hexValue: propsHexValue, decimalValue })
- }
- }
-
- getValue (props) {
- const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props
-
- const multiplier = Math.pow(10, Number(decimals || 0))
- const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- toCurrency: symbol,
- conversionRate: multiplier,
- invertConversionRate: true,
- })
-
- return Number(decimalValueString) ? decimalValueString : ''
- }
-
- handleChange = decimalValue => {
- const { selectedToken: { decimals } = {}, onChange } = this.props
-
- const multiplier = Math.pow(10, Number(decimals || 0))
- const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' })
-
- this.setState({ hexValue, decimalValue })
- onChange(hexValue)
- }
-
- handleBlur = () => {
- this.props.onBlur(this.state.hexValue)
- }
-
- renderConversionComponent () {
- const { selectedTokenExchangeRate, showFiat, currentCurrency, hideConversion } = this.props
- const { decimalValue } = this.state
- let currency, numberOfDecimals
-
- if (hideConversion) {
- return (
- <div className="currency-input__conversion-component">
- { this.context.t('noConversionRateAvailable') }
- </div>
- )
- }
-
- if (showFiat) {
- // Display Fiat
- currency = currentCurrency
- numberOfDecimals = 2
- } else {
- // Display ETH
- currency = ETH
- numberOfDecimals = 6
- }
-
- const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0
- const hexWeiValue = getWeiHexFromDecimalValue({
- value: decimalEthValue,
- fromCurrency: ETH,
- fromDenomination: ETH,
- })
-
- return selectedTokenExchangeRate
- ? (
- <CurrencyDisplay
- className="currency-input__conversion-component"
- currency={currency}
- value={hexWeiValue}
- numberOfDecimals={numberOfDecimals}
- />
- ) : (
- <div className="currency-input__conversion-component">
- { this.context.t('noConversionRateAvailable') }
- </div>
- )
- }
-
- render () {
- const { suffix, ...restProps } = this.props
- const { decimalValue } = this.state
-
- return (
- <UnitInput
- {...restProps}
- suffix={suffix}
- onChange={this.handleChange}
- onBlur={this.handleBlur}
- value={decimalValue}
- >
- { this.renderConversionComponent() }
- </UnitInput>
- )
- }
-}
diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js
deleted file mode 100644
index a00d200f7..000000000
--- a/ui/app/components/token-input/token-input.container.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { connect } from 'react-redux'
-import TokenInput from './token-input.component'
-import {getIsMainnet, getSelectedToken, getSelectedTokenExchangeRate, preferencesSelector} from '../../selectors'
-
-const mapStateToProps = state => {
- const { metamask: { currentCurrency } } = state
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
-
- return {
- currentCurrency,
- selectedToken: getSelectedToken(state),
- selectedTokenExchangeRate: getSelectedTokenExchangeRate(state),
- hideConversion: (!isMainnet && !showFiatInTestnets),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { selectedToken } = stateProps
- const suffix = selectedToken && selectedToken.symbol
-
- return {
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- suffix,
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(TokenInput)
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
deleted file mode 100644
index 258abde72..000000000
--- a/ui/app/components/token-list.js
+++ /dev/null
@@ -1,188 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const TokenTracker = require('eth-token-tracker')
-const TokenCell = require('./token-cell.js')
-const connect = require('react-redux').connect
-const selectors = require('../selectors')
-const log = require('loglevel')
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- tokens: state.metamask.tokens,
- userAddress: selectors.getSelectedAddress(state),
- assetImages: state.metamask.assetImages,
- }
-}
-
-const defaultTokens = []
-const contracts = require('eth-contract-metadata')
-for (const address in contracts) {
- const contract = contracts[address]
- if (contract.erc20) {
- contract.address = address
- defaultTokens.push(contract)
- }
-}
-
-TokenList.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(TokenList)
-
-
-inherits(TokenList, Component)
-function TokenList () {
- this.state = {
- tokens: [],
- isLoading: true,
- network: null,
- }
- Component.call(this)
-}
-
-TokenList.prototype.render = function () {
- const { userAddress, assetImages } = this.props
- const state = this.state
- const { tokens, isLoading, error } = state
- if (isLoading) {
- return this.message(this.context.t('loadingTokens'))
- }
-
- if (error) {
- log.error(error)
- return h('.hotFix', {
- style: {
- padding: '80px',
- },
- }, [
- this.context.t('troubleTokenBalances'),
- h('span.hotFix', {
- style: {
- color: 'rgba(247, 134, 28, 1)',
- cursor: 'pointer',
- },
- onClick: () => {
- global.platform.openWindow({
- url: `https://ethplorer.io/address/${userAddress}`,
- })
- },
- }, this.context.t('here')),
- ])
- }
-
- return h('div', tokens.map((tokenData) => {
- tokenData.image = assetImages[tokenData.address]
- return h(TokenCell, tokenData)
- }))
-
-}
-
-TokenList.prototype.message = function (body) {
- return h('div', {
- style: {
- display: 'flex',
- height: '250px',
- alignItems: 'center',
- justifyContent: 'center',
- padding: '30px',
- },
- }, body)
-}
-
-TokenList.prototype.componentDidMount = function () {
- this.createFreshTokenTracker()
-}
-
-TokenList.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 } = this.props
-
- this.tracker = new TokenTracker({
- userAddress,
- provider: global.ethereumProvider,
- tokens: this.props.tokens,
- pollingInterval: 8000,
- })
-
-
- // Set up listener instances for cleaning up
- this.balanceUpdater = this.updateBalances.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.updateBalances(this.tracker.serialize())
- })
- .catch((reason) => {
- log.error(`Problem updating balances`, reason)
- this.setState({ isLoading: false })
- })
-}
-
-TokenList.prototype.componentDidUpdate = function (prevProps) {
- const {
- network: oldNet,
- userAddress: oldAddress,
- tokens,
- } = prevProps
- const {
- network: newNet,
- userAddress: newAddress,
- tokens: newTokens,
- } = this.props
-
- const isLoading = newNet === 'loading'
- const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress
- const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet
- const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork
-
- const oldTokensLength = tokens ? tokens.length : 0
- const tokensLengthUnchanged = oldTokensLength === newTokens.length
-
- if (tokensLengthUnchanged && shouldUpdateTokens) return
-
- this.setState({ isLoading: true })
- this.createFreshTokenTracker()
-}
-
-TokenList.prototype.updateBalances = function (tokens) {
- if (!this.tracker.running) {
- return
- }
- this.setState({ tokens, isLoading: false })
-}
-
-TokenList.prototype.componentWillUnmount = function () {
- if (!this.tracker) return
- this.tracker.stop()
- this.tracker.removeListener('update', this.balanceUpdater)
- this.tracker.removeListener('error', this.showError)
-}
-
-// function uniqueMergeTokens (tokensA, tokensB = []) {
-// const uniqueAddresses = []
-// const result = []
-// tokensA.concat(tokensB).forEach((token) => {
-// const normal = normalizeAddress(token.address)
-// if (!uniqueAddresses.includes(normal)) {
-// uniqueAddresses.push(normal)
-// result.push(token)
-// }
-// })
-// return result
-// }
diff --git a/ui/app/components/transaction-action/transaction-action.component.js b/ui/app/components/transaction-action/transaction-action.component.js
deleted file mode 100644
index 1de91cb71..000000000
--- a/ui/app/components/transaction-action/transaction-action.component.js
+++ /dev/null
@@ -1,58 +0,0 @@
-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/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
deleted file mode 100644
index ca46d7830..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
-import { formatDate } from '../../util'
-import TransactionActivityLogIcon from './transaction-activity-log-icon'
-import { CONFIRMED_STATUS } from './transaction-activity-log.constants'
-import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
-
-export default class TransactionActivityLog extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricEvent: PropTypes.func,
- }
-
- static propTypes = {
- activities: PropTypes.array,
- className: PropTypes.string,
- conversionRate: PropTypes.number,
- inlineRetryIndex: PropTypes.number,
- inlineCancelIndex: PropTypes.number,
- nativeCurrency: PropTypes.string,
- onCancel: PropTypes.func,
- onRetry: PropTypes.func,
- primaryTransaction: PropTypes.object,
- }
-
- handleActivityClick = hash => {
- const { primaryTransaction } = this.props
- const { metamaskNetworkId } = primaryTransaction
-
- const prefix = prefixForNetwork(metamaskNetworkId)
- const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
-
- global.platform.openWindow({ url: etherscanUrl })
- }
-
- renderInlineRetry (index, activity) {
- const { t } = this.context
- const { inlineRetryIndex, primaryTransaction = {}, onRetry } = this.props
- const { status } = primaryTransaction
- const { id } = activity
-
- return status !== CONFIRMED_STATUS && index === inlineRetryIndex
- ? (
- <div
- className="transaction-activity-log__action-link"
- onClick={() => onRetry(id)}
- >
- { t('speedUpTransaction') }
- </div>
- ) : null
- }
-
- renderInlineCancel (index, activity) {
- const { t } = this.context
- const { inlineCancelIndex, primaryTransaction = {}, onCancel } = this.props
- const { status } = primaryTransaction
- const { id } = activity
-
- return status !== CONFIRMED_STATUS && index === inlineCancelIndex
- ? (
- <div
- className="transaction-activity-log__action-link"
- onClick={() => onCancel(id)}
- >
- { t('speedUpCancellation') }
- </div>
- ) : null
- }
-
- renderActivity (activity, index) {
- const { conversionRate, nativeCurrency } = this.props
- const { eventKey, value, timestamp, hash } = activity
- const ethValue = index === 0
- ? `${getValueFromWeiHex({
- value,
- fromCurrency: nativeCurrency,
- toCurrency: nativeCurrency,
- conversionRate,
- numberOfDecimals: 6,
- })} ${nativeCurrency}`
- : getEthConversionFromWeiHex({
- value,
- fromCurrency: nativeCurrency,
- conversionRate,
- numberOfDecimals: 3,
- })
- const formattedTimestamp = formatDate(timestamp, 'T \'on\' M/d/y')
- const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
-
- return (
- <div
- key={index}
- className="transaction-activity-log__activity"
- >
- <TransactionActivityLogIcon
- className="transaction-activity-log__activity-icon"
- eventKey={eventKey}
- />
- <div className="transaction-activity-log__entry-container">
- <div
- className="transaction-activity-log__activity-text"
- title={activityText}
- onClick={() => this.handleActivityClick(hash)}
- >
- { activityText }
- </div>
- { this.renderInlineRetry(index, activity) }
- { this.renderInlineCancel(index, activity) }
- </div>
- </div>
- )
- }
-
- render () {
- const { t } = this.context
- const { className, activities } = this.props
-
- return (
- <div className={classnames('transaction-activity-log', className)}>
- <div className="transaction-activity-log__title">
- { t('activityLog') }
- </div>
- <div className="transaction-activity-log__activities-container">
- { activities.map((activity, index) => this.renderActivity(activity, index)) }
- </div>
- </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
deleted file mode 100644
index e43229708..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { connect } from 'react-redux'
-import R from 'ramda'
-import TransactionActivityLog from './transaction-activity-log.component'
-import { conversionRateSelector, getNativeCurrency } from '../../selectors'
-import { combineTransactionHistories } from './transaction-activity-log.util'
-import {
- TRANSACTION_RESUBMITTED_EVENT,
- TRANSACTION_CANCEL_ATTEMPTED_EVENT,
-} from './transaction-activity-log.constants'
-
-const matchesEventKey = matchEventKey => ({ eventKey }) => eventKey === matchEventKey
-
-const mapStateToProps = state => {
- return {
- conversionRate: conversionRateSelector(state),
- nativeCurrency: getNativeCurrency(state),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const {
- transactionGroup: {
- transactions = [],
- primaryTransaction,
- } = {},
- ...restOwnProps
- } = ownProps
-
- const activities = combineTransactionHistories(transactions)
- const inlineRetryIndex = R.findLastIndex(matchesEventKey(TRANSACTION_RESUBMITTED_EVENT))(activities)
- const inlineCancelIndex = R.findLastIndex(matchesEventKey(TRANSACTION_CANCEL_ATTEMPTED_EVENT))(activities)
-
- return {
- ...stateProps,
- ...dispatchProps,
- ...restOwnProps,
- activities,
- inlineRetryIndex,
- inlineCancelIndex,
- primaryTransaction,
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(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
deleted file mode 100644
index 6206a4678..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
+++ /dev/null
@@ -1,224 +0,0 @@
-import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
-
-// path constants
-const STATUS_PATH = '/status'
-const GAS_PRICE_PATH = '/txParams/gasPrice'
-const GAS_LIMIT_PATH = '/txParams/gas'
-
-// op constants
-const REPLACE_OP = 'replace'
-
-import {
- // event constants
- TRANSACTION_CREATED_EVENT,
- TRANSACTION_SUBMITTED_EVENT,
- TRANSACTION_RESUBMITTED_EVENT,
- TRANSACTION_CONFIRMED_EVENT,
- TRANSACTION_DROPPED_EVENT,
- TRANSACTION_UPDATED_EVENT,
- TRANSACTION_ERRORED_EVENT,
- TRANSACTION_CANCEL_ATTEMPTED_EVENT,
- TRANSACTION_CANCEL_SUCCESS_EVENT,
- // status constants
- SUBMITTED_STATUS,
- CONFIRMED_STATUS,
- DROPPED_STATUS,
-} from './transaction-activity-log.constants'
-
-import {
- TRANSACTION_TYPE_CANCEL,
- TRANSACTION_TYPE_RETRY,
-} from '../../../../app/scripts/controllers/transactions/enums'
-
-const eventPathsHash = {
- [STATUS_PATH]: true,
- [GAS_PRICE_PATH]: true,
- [GAS_LIMIT_PATH]: true,
-}
-
-const statusHash = {
- [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
- [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
- [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
-}
-
-/**
- * @name getActivities
- * @param {Object} transaction - txMeta object
- * @param {boolean} isFirstTransaction - True if the transaction is the first created transaction
- * in the list of transactions with the same nonce. If so, we use this transaction to create the
- * transactionCreated activity.
- * @returns {Array}
- */
-export function getActivities (transaction, isFirstTransaction = false) {
- const { id, hash, history = [], txReceipt: { status } = {}, type } = transaction
-
- let cachedGasLimit = '0x0'
- let cachedGasPrice = '0x0'
-
- const historyActivities = history.reduce((acc, base, index) => {
- // First history item should be transaction creation
- if (index === 0 && !Array.isArray(base) && base.txParams) {
- const { time: timestamp, txParams: { value, gas = '0x0', gasPrice = '0x0' } = {} } = base
- // The cached gas limit and gas price are used to display the gas fee in the activity log. We
- // need to cache these values because the status update history events don't provide us with
- // the latest gas limit and gas price.
- cachedGasLimit = gas
- cachedGasPrice = gasPrice
-
- if (isFirstTransaction) {
- return acc.concat({
- id,
- hash,
- eventKey: TRANSACTION_CREATED_EVENT,
- timestamp,
- value,
- })
- }
- // An entry in the history may be an array of more sub-entries.
- } else if (Array.isArray(base)) {
- const events = []
-
- base.forEach(entry => {
- const { op, path, value, timestamp: entryTimestamp } = entry
- // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a
- // timestamp, the first sub-entry in a history entry should.
- const timestamp = entryTimestamp || base[0] && base[0].timestamp
-
- if (path in eventPathsHash && op === REPLACE_OP) {
- switch (path) {
- case STATUS_PATH: {
- const gasFee = getHexGasTotal({ gasLimit: cachedGasLimit, gasPrice: cachedGasPrice })
-
- if (value in statusHash) {
- let eventKey = statusHash[value]
-
- // If the status is 'submitted', we need to determine whether the event is a
- // transaction retry or a cancellation attempt.
- if (value === SUBMITTED_STATUS) {
- if (type === TRANSACTION_TYPE_RETRY) {
- eventKey = TRANSACTION_RESUBMITTED_EVENT
- } else if (type === TRANSACTION_TYPE_CANCEL) {
- eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT
- }
- } else if (value === CONFIRMED_STATUS) {
- if (type === TRANSACTION_TYPE_CANCEL) {
- eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT
- }
- }
-
- events.push({
- id,
- hash,
- eventKey,
- timestamp,
- value: gasFee,
- })
- }
-
- break
- }
-
- // If the gas price or gas limit has been changed, we update the gasFee of the
- // previously submitted event. These events happen when the gas limit and gas price is
- // changed at the confirm screen.
- case GAS_PRICE_PATH:
- case GAS_LIMIT_PATH: {
- const lastEvent = events[events.length - 1] || {}
- const { lastEventKey } = lastEvent
-
- if (path === GAS_LIMIT_PATH) {
- cachedGasLimit = value
- } else if (path === GAS_PRICE_PATH) {
- cachedGasPrice = value
- }
-
- if (lastEventKey === TRANSACTION_SUBMITTED_EVENT ||
- lastEventKey === TRANSACTION_RESUBMITTED_EVENT) {
- lastEvent.value = getHexGasTotal({
- gasLimit: cachedGasLimit,
- gasPrice: cachedGasPrice,
- })
- }
-
- break
- }
-
- default: {
- events.push({
- id,
- hash,
- eventKey: TRANSACTION_UPDATED_EVENT,
- timestamp,
- })
- }
- }
- }
- })
-
- return acc.concat(events)
- }
-
- return acc
- }, [])
-
- // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction,
- // so we add an error entry to the Activity Log.
- return status === '0x0'
- ? historyActivities.concat({ id, hash, eventKey: TRANSACTION_ERRORED_EVENT })
- : historyActivities
-}
-
-/**
- * @description Removes "Transaction dropped" activities from a list of sorted activities if one of
- * the transactions has been confirmed. Typically, if multiple transactions have the same nonce,
- * once one transaction is confirmed, the rest are dropped. In this case, we don't want to show
- * multiple "Transaction dropped" activities, and instead want to show a single "Transaction
- * confirmed".
- * @param {Array} activities - List of sorted activities generated from the getActivities function.
- * @returns {Array}
- */
-function filterSortedActivities (activities) {
- const filteredActivities = []
- const hasConfirmedActivity = Boolean(activities.find(({ eventKey }) => (
- eventKey === TRANSACTION_CONFIRMED_EVENT || eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT
- )))
- let addedDroppedActivity = false
-
- activities.forEach(activity => {
- if (activity.eventKey === TRANSACTION_DROPPED_EVENT) {
- if (!hasConfirmedActivity && !addedDroppedActivity) {
- filteredActivities.push(activity)
- addedDroppedActivity = true
- }
- } else {
- filteredActivities.push(activity)
- }
- })
-
- return filteredActivities
-}
-
-/**
- * Combines the histories of an array of transactions into a single array.
- * @param {Array} transactions - Array of txMeta transaction objects.
- * @returns {Array}
- */
-export function combineTransactionHistories (transactions = []) {
- if (!transactions.length) {
- return []
- }
-
- const activities = []
-
- transactions.forEach((transaction, index) => {
- // The first transaction should be the transaction with the earliest submittedTime. We show the
- // 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted'
- // instead.
- const transactionActivities = getActivities(transaction, index === 0)
- activities.push(...transactionActivities)
- })
-
- const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp)
- return filterSortedActivities(sortedActivities)
-}
diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/transaction-breakdown/index.scss
deleted file mode 100644
index b56cbdd7f..000000000
--- a/ui/app/components/transaction-breakdown/index.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-@import './transaction-breakdown-row/index';
-
-.transaction-breakdown {
- &__title {
- border-bottom: 1px solid #d8d8d8;
- padding-bottom: 4px;
- text-transform: capitalize;
- }
-
- &__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/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
deleted file mode 100644
index c19399dbb..000000000
--- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
deleted file mode 100644
index 26dc4c153..000000000
--- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import TransactionBreakdownRow from './transaction-breakdown-row'
-import CurrencyDisplay from '../currency-display'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import HexToDecimal from '../hex-to-decimal'
-import { GWEI, PRIMARY, SECONDARY } from '../../constants/common'
-
-export default class TransactionBreakdown extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- transaction: PropTypes.object,
- className: PropTypes.string,
- nativeCurrency: PropTypes.string.isRequired,
- showFiat: PropTypes.bool,
- gas: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- gasPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- gasUsed: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- totalInHex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- }
-
- static defaultProps = {
- transaction: {},
- showFiat: true,
- }
-
- render () {
- const { t } = this.context
- const { gas, gasPrice, value, className, nativeCurrency, showFiat, totalInHex, gasUsed } = this.props
-
- return (
- <div className={classnames('transaction-breakdown', className)}>
- <div className="transaction-breakdown__title">
- { t('transaction') }
- </div>
- <TransactionBreakdownRow title={t('amount')}>
- <UserPreferencedCurrencyDisplay
- className="transaction-breakdown__value"
- type={PRIMARY}
- value={value}
- />
- </TransactionBreakdownRow>
- <TransactionBreakdownRow
- title={`${t('gasLimit')} (${t('units')})`}
- className="transaction-breakdown__row-title"
- >
- {typeof gas !== 'undefined'
- ? <HexToDecimal
- className="transaction-breakdown__value"
- value={gas}
- />
- : '?'
- }
- </TransactionBreakdownRow>
- {
- typeof gasUsed === 'string' && (
- <TransactionBreakdownRow
- title={`${t('gasUsed')} (${t('units')})`}
- className="transaction-breakdown__row-title"
- >
- <HexToDecimal
- className="transaction-breakdown__value"
- value={gasUsed}
- />
- </TransactionBreakdownRow>
- )
- }
- <TransactionBreakdownRow title={t('gasPrice')}>
- {typeof gasPrice !== 'undefined'
- ? <CurrencyDisplay
- className="transaction-breakdown__value"
- currency={nativeCurrency}
- denomination={GWEI}
- value={gasPrice}
- hideLabel
- />
- : '?'
- }
- </TransactionBreakdownRow>
- <TransactionBreakdownRow title={t('total')}>
- <div>
- <UserPreferencedCurrencyDisplay
- className="transaction-breakdown__value transaction-breakdown__value--eth-total"
- type={PRIMARY}
- value={totalInHex}
- />
- {
- showFiat && (
- <UserPreferencedCurrencyDisplay
- className="transaction-breakdown__value"
- type={SECONDARY}
- value={totalInHex}
- />
- )
- }
- </div>
- </TransactionBreakdownRow>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/transaction-breakdown/transaction-breakdown.container.js
deleted file mode 100644
index 3e85b9e23..000000000
--- a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { connect } from 'react-redux'
-import TransactionBreakdown from './transaction-breakdown.component'
-import {getIsMainnet, getNativeCurrency, preferencesSelector} from '../../selectors'
-import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
-import { sumHexes } from '../../helpers/transactions.util'
-
-const mapStateToProps = (state, ownProps) => {
- const { transaction } = ownProps
- const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
-
- const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas
-
- const hexGasTotal = gasLimit && gasPrice && getHexGasTotal({ gasLimit, gasPrice }) || '0x0'
- const totalInHex = sumHexes(hexGasTotal, value)
-
- return {
- nativeCurrency: getNativeCurrency(state),
- showFiat: (isMainnet || !!showFiatInTestnets),
- totalInHex,
- gas,
- gasPrice,
- value,
- gasUsed,
- }
-}
-
-export default connect(mapStateToProps)(TransactionBreakdown)
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
deleted file mode 100644
index 5b55beeb4..000000000
--- a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
+++ /dev/null
@@ -1,81 +0,0 @@
-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 transactionGroup = {
- transactions: [transaction],
- primaryTransaction: transaction,
- initialTransaction: transaction,
- }
-
- const wrapper = shallow(
- <TransactionListItemDetails
- transactionGroup={transactionGroup}
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-list-item-details'))
- assert.equal(wrapper.find(Button).length, 2)
- 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 transactionGroup = {
- transactions: [transaction],
- primaryTransaction: transaction,
- initialTransaction: transaction,
- nonce: '0xa4',
- hasRetried: false,
- hasCancelled: false,
- }
-
- const wrapper = shallow(
- <TransactionListItemDetails
- transactionGroup={transactionGroup}
- showRetry={true}
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-list-item-details'))
- assert.equal(wrapper.find(Button).length, 3)
- })
-})
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
deleted file mode 100644
index 3e39212d3..000000000
--- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import copyToClipboard from 'copy-to-clipboard'
-import SenderToRecipient from '../sender-to-recipient'
-import { FLAT_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 Tooltip from '../tooltip'
-import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
-
-export default class TransactionListItemDetails extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- onCancel: PropTypes.func,
- onRetry: PropTypes.func,
- showCancel: PropTypes.bool,
- showRetry: PropTypes.bool,
- transactionGroup: PropTypes.object,
- }
-
- state = {
- justCopied: false,
- }
-
- handleEtherscanClick = () => {
- const { transactionGroup: { primaryTransaction } } = this.props
- const { hash, metamaskNetworkId } = primaryTransaction
-
- const prefix = prefixForNetwork(metamaskNetworkId)
- const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Activity Log',
- name: 'Clicked "View on Etherscan"',
- },
- })
-
- global.platform.openWindow({ url: etherscanUrl })
- }
-
- handleCancel = event => {
- const { transactionGroup: { initialTransaction: { id } = {} } = {}, onCancel } = this.props
-
- event.stopPropagation()
- onCancel(id)
- }
-
- handleRetry = event => {
- const { transactionGroup: { initialTransaction: { id } = {} } = {}, onRetry } = this.props
-
- event.stopPropagation()
- onRetry(id)
- }
-
- handleCopyTxId = () => {
- const { transactionGroup} = this.props
- const { primaryTransaction: transaction } = transactionGroup
- const { hash } = transaction
-
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Activity Log',
- name: 'Copied Transaction ID',
- },
- })
-
- this.setState({ justCopied: true }, () => {
- copyToClipboard(hash)
- setTimeout(() => this.setState({ justCopied: false }), 1000)
- })
- }
-
- render () {
- const { t } = this.context
- const { justCopied } = this.state
- const { transactionGroup, showCancel, showRetry, onCancel, onRetry } = this.props
- const { primaryTransaction: transaction } = transactionGroup
- const { txParams: { to, from } = {} } = transaction
-
- return (
- <div className="transaction-list-item-details">
- <div className="transaction-list-item-details__header">
- <div>{ t('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>
- )
- }
- <Tooltip title={justCopied ? t('copiedTransactionId') : t('copyTransactionId')}>
- <Button
- type="raised"
- onClick={this.handleCopyTxId}
- className="transaction-list-item-details__header-button"
- >
- <img
- className="transaction-list-item-details__header-button__copy-icon"
- src="/images/copy-to-clipboard.svg"
- />
- </Button>
- </Tooltip>
- <Tooltip title={t('viewOnEtherscan')}>
- <Button
- type="raised"
- onClick={this.handleEtherscanClick}
- className="transaction-list-item-details__header-button"
- >
- <img src="/images/arrow-popout.svg" />
- </Button>
- </Tooltip>
- </div>
- </div>
- <div className="transaction-list-item-details__body">
- <div className="transaction-list-item-details__sender-to-recipient-container">
- <SenderToRecipient
- variant={FLAT_VARIANT}
- addressOnly
- recipientAddress={to}
- senderAddress={from}
- onRecipientClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Activity Log',
- name: 'Copied "To" Address',
- },
- })
- }}
- onSenderClick={() => {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Activity Log',
- name: 'Copied "From" Address',
- },
- })
- }}
- />
- </div>
- <div className="transaction-list-item-details__cards-container">
- <TransactionBreakdown
- transaction={transaction}
- className="transaction-list-item-details__transaction-breakdown"
- />
- <TransactionActivityLog
- transactionGroup={transactionGroup}
- className="transaction-list-item-details__transaction-activity-log"
- onCancel={onCancel}
- onRetry={onRetry}
- />
- </div>
- </div>
- </div>
- )
- }
-}
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
deleted file mode 100644
index e843fe1a0..000000000
--- a/ui/app/components/transaction-list-item/transaction-list-item.component.js
+++ /dev/null
@@ -1,224 +0,0 @@
-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 UserPreferencedCurrencyDisplay from '../user-preferenced-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 { PRIMARY, SECONDARY } from '../../constants/common'
-import { getStatusKey } from '../../helpers/transactions.util'
-
-export default class TransactionListItem extends PureComponent {
- static propTypes = {
- assetImages: PropTypes.object,
- history: PropTypes.object,
- methodData: PropTypes.object,
- nonceAndDate: PropTypes.string,
- primaryTransaction: PropTypes.object,
- retryTransaction: PropTypes.func,
- setSelectedToken: PropTypes.func,
- showCancelModal: PropTypes.func,
- showCancel: PropTypes.bool,
- showRetry: PropTypes.bool,
- showFiat: PropTypes.bool,
- token: PropTypes.object,
- tokenData: PropTypes.object,
- transaction: PropTypes.object,
- transactionGroup: PropTypes.object,
- value: PropTypes.string,
- fetchBasicGasAndTimeEstimates: PropTypes.func,
- fetchGasEstimates: PropTypes.func,
- }
-
- static defaultProps = {
- showFiat: true,
- }
-
- static contextTypes = {
- metricsEvent: PropTypes.func,
- }
-
- state = {
- showTransactionDetails: false,
- }
-
- handleClick = () => {
- const {
- transaction,
- history,
- } = this.props
- const { id, status } = transaction
- const { showTransactionDetails } = this.state
-
- if (status === UNAPPROVED_STATUS) {
- history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
- return
- }
-
- if (!showTransactionDetails) {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Expand Transaction',
- },
- })
- }
-
- this.setState({ showTransactionDetails: !showTransactionDetails })
- }
-
- handleCancel = id => {
- const {
- primaryTransaction: { txParams: { gasPrice } } = {},
- transaction: { id: initialTransactionId },
- showCancelModal,
- } = this.props
-
- const cancelId = id || initialTransactionId
- showCancelModal(cancelId, gasPrice)
- }
-
- /**
- * @name handleRetry
- * @description Resubmits a transaction. Retrying a transaction within a list of transactions with
- * the same nonce requires keeping the original value while increasing the gas price of the latest
- * transaction.
- * @param {number} id - Transaction id
- */
- handleRetry = id => {
- const {
- primaryTransaction: { txParams: { gasPrice } } = {},
- transaction: { txParams: { to } = {}, id: initialTransactionId },
- methodData: { name } = {},
- setSelectedToken,
- retryTransaction,
- fetchBasicGasAndTimeEstimates,
- fetchGasEstimates,
- } = this.props
-
- if (name === TOKEN_METHOD_TRANSFER) {
- setSelectedToken(to)
- }
-
- const retryId = id || initialTransactionId
-
- return fetchBasicGasAndTimeEstimates()
- .then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime))
- .then(retryTransaction(retryId, gasPrice))
- }
-
- renderPrimaryCurrency () {
- const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props
-
- return token
- ? (
- <TokenCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--primary"
- token={token}
- transactionData={data}
- prefix="-"
- />
- ) : (
- <UserPreferencedCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--primary"
- value={value}
- type={PRIMARY}
- prefix="-"
- />
- )
- }
-
- renderSecondaryCurrency () {
- const { token, value, showFiat } = this.props
-
- return token || !showFiat
- ? null
- : (
- <UserPreferencedCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--secondary"
- value={value}
- prefix="-"
- type={SECONDARY}
- />
- )
- }
-
- render () {
- const {
- assetImages,
- transaction,
- methodData,
- nonceAndDate,
- primaryTransaction,
- showCancel,
- showRetry,
- tokenData,
- transactionGroup,
- } = 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={getStatusKey(primaryTransaction)}
- title={(
- (primaryTransaction.err && primaryTransaction.err.rpc)
- ? primaryTransaction.err.rpc.message
- : primaryTransaction.err && primaryTransaction.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
- transactionGroup={transactionGroup}
- 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
deleted file mode 100644
index 93a82849e..000000000
--- a/ui/app/components/transaction-list-item/transaction-list-item.container.js
+++ /dev/null
@@ -1,82 +0,0 @@
-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, showModal, showSidebar, addKnownMethodData } from '../../actions'
-import { hexToDecimal } from '../../helpers/conversions.util'
-import { getTokenData } from '../../helpers/transactions.util'
-import { increaseLastGasPrice } from '../../helpers/confirm-transaction/util'
-import { formatDate } from '../../util'
-import {
- fetchBasicGasAndTimeEstimates,
- fetchGasEstimates,
- setCustomGasPriceForRetry,
- setCustomGasLimit,
-} from '../../ducks/gas.duck'
-import {getIsMainnet, preferencesSelector} from '../../selectors'
-
-const mapStateToProps = state => {
- const { metamask: { knownMethodData } } = state
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
-
- return {
- knownMethodData,
- showFiat: (isMainnet || !!showFiatInTestnets),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
- fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
- setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
- addKnownMethodData: (fourBytePrefix, methodData) => dispatch(addKnownMethodData(fourBytePrefix, methodData)),
- retryTransaction: (transaction, gasPrice) => {
- dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice))
- dispatch(setCustomGasLimit(transaction.txParams.gas))
- dispatch(showSidebar({
- transitionName: 'sidebar-left',
- type: 'customize-gas',
- props: { transaction },
- }))
- },
- showCancelModal: (transactionId, originalGasPrice) => {
- return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
- },
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps
- const { retryTransaction, ...restDispatchProps } = dispatchProps
- const { txParams: { nonce, data } = {}, time } = initialTransaction
- const { txParams: { value } = {} } = primaryTransaction
-
- const tokenData = data && getTokenData(data)
- const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
-
- return {
- ...stateProps,
- ...restDispatchProps,
- ...ownProps,
- value,
- nonceAndDate,
- tokenData,
- transaction: initialTransaction,
- primaryTransaction,
- retryTransaction: (transactionId, gasPrice) => {
- const { transactionGroup: { transactions = [] } } = ownProps
- const transaction = transactions.find(tx => tx.id === transactionId) || {}
- const increasedGasPrice = increaseLastGasPrice(gasPrice)
- retryTransaction(transaction, increasedGasPrice)
- },
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps, mergeProps),
- withMethodData,
-)(TransactionListItem)
diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js
deleted file mode 100644
index ddab3b290..000000000
--- a/ui/app/components/transaction-list/transaction-list.component.js
+++ /dev/null
@@ -1,126 +0,0 @@
-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: [],
- }
-
- static propTypes = {
- pendingTransactions: PropTypes.array,
- completedTransactions: PropTypes.array,
- 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 = (transactionGroup, isEarliestNonce) => {
- const { transactions = [], hasRetried } = transactionGroup
- const [earliestTransaction = {}] = transactions
- const { submittedTime } = earliestTransaction
- return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried
- }
-
- shouldShowCancel (transactionGroup) {
- const { hasCancelled } = transactionGroup
- return !hasCancelled
- }
-
- renderTransactions () {
- const { t } = this.context
- const { pendingTransactions = [], completedTransactions = [] } = this.props
- const pendingLength = pendingTransactions.length
-
- return (
- <div className="transaction-list__transactions">
- {
- pendingLength > 0 && (
- <div className="transaction-list__pending-transactions">
- <div className="transaction-list__header">
- { `${t('queue')} (${pendingTransactions.length})` }
- </div>
- {
- pendingTransactions.map((transactionGroup, index) => (
- this.renderTransaction(transactionGroup, index, true)
- ))
- }
- </div>
- )
- }
- <div className="transaction-list__completed-transactions">
- <div className="transaction-list__header">
- { t('history') }
- </div>
- {
- completedTransactions.length > 0
- ? completedTransactions.map((transactionGroup, index) => (
- this.renderTransaction(transactionGroup, index)
- ))
- : this.renderEmpty()
- }
- </div>
- </div>
- )
- }
-
- renderTransaction (transactionGroup, index, isPendingTx = false) {
- const { selectedToken, assetImages } = this.props
- const { transactions = [] } = transactionGroup
-
- return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT
- ? (
- <ShapeShiftTransactionListItem
- { ...transactions[0] }
- key={`shapeshift${index}`}
- />
- ) : (
- <TransactionListItem
- transactionGroup={transactionGroup}
- key={`${transactionGroup.nonce}:${index}`}
- showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, index === 0)}
- showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)}
- 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
deleted file mode 100644
index e70ca15c5..000000000
--- a/ui/app/components/transaction-list/transaction-list.container.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import TransactionList from './transaction-list.component'
-import {
- nonceSortedCompletedTransactionsSelector,
- nonceSortedPendingTransactionsSelector,
-} from '../../selectors/transactions'
-import { getSelectedAddress, getAssetImages } from '../../selectors'
-import { selectedTokenSelector } from '../../selectors/tokens'
-import { updateNetworkNonce } from '../../actions'
-
-const mapStateToProps = state => {
- return {
- completedTransactions: nonceSortedCompletedTransactionsSelector(state),
- pendingTransactions: nonceSortedPendingTransactionsSelector(state),
- 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/tests/transaction-status.component.test.js b/ui/app/components/transaction-status/tests/transaction-status.component.test.js
deleted file mode 100644
index f4ddc9206..000000000
--- a/ui/app/components/transaction-status/tests/transaction-status.component.test.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { mount } from 'enzyme'
-import TransactionStatus from '../transaction-status.component'
-import Tooltip from '../../tooltip-v2'
-
-describe('TransactionStatus Component', () => {
- it('should render APPROVED properly', () => {
- const wrapper = mount(
- <TransactionStatus
- statusKey="approved"
- title="test-title"
- />,
- { context: { t: str => str.toUpperCase() } }
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.text(), 'APPROVED')
- assert.equal(wrapper.find(Tooltip).props().title, 'test-title')
- })
-
- it('should render SUBMITTED properly', () => {
- const wrapper = mount(
- <TransactionStatus
- statusKey="submitted"
- />,
- { context: { t: str => str.toUpperCase() } }
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.text(), 'PENDING')
- })
-})
diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/transaction-status/transaction-status.component.js
deleted file mode 100644
index 28544d2cd..000000000
--- a/ui/app/components/transaction-status/transaction-status.component.js
+++ /dev/null
@@ -1,63 +0,0 @@
-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,
- CANCELLED_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',
- [CANCELLED_STATUS]: 'transaction-status--failed',
-}
-
-const statusToTextHash = {
- [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/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
deleted file mode 100644
index efc987371..000000000
--- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TokenBalance from '../../token-balance'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-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
-const metricsEvent = () => ({})
-
-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, metricsEvent } })
-
- assert.equal(wrapper.find('.transaction-view-balance').length, 1)
- assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
- assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).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
deleted file mode 100644
index a18e959b5..000000000
--- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import Button from '../button'
-import Identicon from '../identicon'
-import TokenBalance from '../token-balance'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import { SEND_ROUTE } from '../../routes'
-import { PRIMARY, SECONDARY } from '../../constants/common'
-import Tooltip from '../tooltip-v2'
-
-export default class TransactionViewBalance extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- }
-
- static propTypes = {
- showDepositModal: PropTypes.func,
- selectedToken: PropTypes.object,
- history: PropTypes.object,
- network: PropTypes.string,
- balance: PropTypes.string,
- assetImage: PropTypes.string,
- balanceIsCached: PropTypes.bool,
- showFiat: PropTypes.bool,
- }
-
- static defaultProps = {
- showFiat: true,
- }
-
- renderBalance () {
- const { selectedToken, balance, balanceIsCached, showFiat } = this.props
-
- return selectedToken
- ? (
- <div className="transaction-view-balance__balance">
- <TokenBalance
- token={selectedToken}
- withSymbol
- className="transaction-view-balance__primary-balance"
- />
- </div>
- ) : (
- <Tooltip position="top" title={this.context.t('balanceOutdated')} disabled={!balanceIsCached}>
- <div className="transaction-view-balance__balance">
- <div className="transaction-view-balance__primary-container">
- <UserPreferencedCurrencyDisplay
- className={classnames('transaction-view-balance__primary-balance', {
- 'transaction-view-balance__cached-balance': balanceIsCached,
- })}
- value={balance}
- type={PRIMARY}
- ethNumberOfDecimals={4}
- hideTitle={true}
- />
- {
- balanceIsCached ? <span className="transaction-view-balance__cached-star">*</span> : null
- }
- </div>
- {
- showFiat && (
- <UserPreferencedCurrencyDisplay
- className={classnames({
- 'transaction-view-balance__cached-secondary-balance': balanceIsCached,
- 'transaction-view-balance__secondary-balance': !balanceIsCached,
- })}
- value={balance}
- type={SECONDARY}
- ethNumberOfDecimals={4}
- hideTitle={true}
- />
- )
- }
- </div>
- </Tooltip>
- )
- }
-
- renderButtons () {
- const { t, metricsEvent } = 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={() => {
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Clicked Deposit',
- },
- })
- showDepositModal()
- }}
- >
- { t('deposit') }
- </Button>
- )
- }
- <Button
- type="primary"
- className="transaction-view-balance__button"
- onClick={() => {
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Clicked Send',
- },
- })
- 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
deleted file mode 100644
index df6d287eb..000000000
--- a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
+++ /dev/null
@@ -1,46 +0,0 @@
-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,
- getNativeCurrency,
- getSelectedTokenAssetImage,
- getMetaMaskAccounts,
- isBalanceCached,
- preferencesSelector,
- getIsMainnet,
-} from '../../selectors'
-import { showModal } from '../../actions'
-
-const mapStateToProps = state => {
- const { showFiatInTestnets } = preferencesSelector(state)
- const isMainnet = getIsMainnet(state)
- const selectedAddress = getSelectedAddress(state)
- const { metamask: { network } } = state
- const accounts = getMetaMaskAccounts(state)
- const account = accounts[selectedAddress]
- const { balance } = account
-
- return {
- selectedToken: getSelectedToken(state),
- network,
- balance,
- nativeCurrency: getNativeCurrency(state),
- assetImage: getSelectedTokenAssetImage(state),
- balanceIsCached: isBalanceCached(state),
- showFiat: (isMainnet || !!showFiatInTestnets),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TransactionViewBalance)
diff --git a/ui/app/components/ui-migration-annoucement/ui-migration-announcement.container.js b/ui/app/components/ui-migration-annoucement/ui-migration-announcement.container.js
deleted file mode 100644
index 6dc993b87..000000000
--- a/ui/app/components/ui-migration-annoucement/ui-migration-announcement.container.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { connect } from 'react-redux'
-import UiMigrationAnnouncement from './ui-migration-annoucement.component'
-import { setCompletedUiMigration } from '../../actions'
-
-const mapStateToProps = (state) => {
- const shouldShowAnnouncement = !state.metamask.completedUiMigration
-
- return {
- shouldShowAnnouncement,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- onClose () {
- dispatch(setCompletedUiMigration())
- },
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(UiMigrationAnnouncement)
diff --git a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js
new file mode 100644
index 000000000..8abe1ab18
--- /dev/null
+++ b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js
@@ -0,0 +1,84 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import AccountListItem from '../../app/send/account-list-item/account-list-item.component'
+
+export default class AccountDropdownMini extends PureComponent {
+ static propTypes = {
+ accounts: PropTypes.array.isRequired,
+ closeDropdown: PropTypes.func,
+ disabled: PropTypes.bool,
+ dropdownOpen: PropTypes.bool,
+ onSelect: PropTypes.func,
+ openDropdown: PropTypes.func,
+ selectedAccount: PropTypes.object.isRequired,
+ }
+
+ static defaultProps = {
+ closeDropdown: () => {},
+ disabled: false,
+ dropdownOpen: false,
+ onSelect: () => {},
+ openDropdown: () => {},
+ }
+
+ getListItemIcon (currentAccount, selectedAccount) {
+ return currentAccount.address === selectedAccount.address && (
+ <i
+ className="fa fa-check fa-lg"
+ style={{ color: '#02c9b1' }}
+ />
+ )
+ }
+
+ renderDropdown () {
+ const { accounts, selectedAccount, closeDropdown, onSelect } = this.props
+
+ return (
+ <div>
+ <div
+ className="account-dropdown-mini__close-area"
+ onClick={closeDropdown}
+ />
+ <div className="account-dropdown-mini__list">
+ {
+ accounts.map(account => (
+ <AccountListItem
+ key={account.address}
+ account={account}
+ displayBalance={false}
+ displayAddress={false}
+ handleClick={() => {
+ onSelect(account)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account, selectedAccount)}
+ />
+ ))
+ }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { disabled, selectedAccount, openDropdown, dropdownOpen } = this.props
+
+ return (
+ <div className="account-dropdown-mini">
+ <AccountListItem
+ account={selectedAccount}
+ handleClick={() => !disabled && openDropdown()}
+ displayBalance={false}
+ displayAddress={false}
+ icon={
+ !disabled && <i
+ className="fa fa-caret-down fa-lg"
+ style={{ color: '#dedede' }}
+ />
+ }
+ />
+ { !disabled && dropdownOpen && this.renderDropdown() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/account-dropdown-mini/index.js b/ui/app/components/ui/account-dropdown-mini/index.js
index cb0839e72..cb0839e72 100644
--- a/ui/app/components/account-dropdown-mini/index.js
+++ b/ui/app/components/ui/account-dropdown-mini/index.js
diff --git a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js
new file mode 100644
index 000000000..bc74ceb3c
--- /dev/null
+++ b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js
@@ -0,0 +1,107 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import AccountDropdownMini from '../account-dropdown-mini.component'
+import AccountListItem from '../../../app/send/account-list-item/account-list-item.component'
+
+describe('AccountDropdownMini', () => {
+ it('should render an account with an icon', () => {
+ const accounts = [
+ {
+ address: '0x1',
+ name: 'account1',
+ balance: '0x1',
+ },
+ {
+ address: '0x2',
+ name: 'account2',
+ balance: '0x2',
+ },
+ {
+ address: '0x3',
+ name: 'account3',
+ balance: '0x3',
+ },
+ ]
+
+ const wrapper = shallow(
+ <AccountDropdownMini
+ selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
+ accounts={accounts}
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(AccountListItem).length, 1)
+ const accountListItemProps = wrapper.find(AccountListItem).at(0).props()
+ assert.equal(accountListItemProps.account.address, '0x1')
+ const iconProps = accountListItemProps.icon.props
+ assert.equal(iconProps.className, 'fa fa-caret-down fa-lg')
+ })
+
+ it('should render a list of accounts', () => {
+ const accounts = [
+ {
+ address: '0x1',
+ name: 'account1',
+ balance: '0x1',
+ },
+ {
+ address: '0x2',
+ name: 'account2',
+ balance: '0x2',
+ },
+ {
+ address: '0x3',
+ name: 'account3',
+ balance: '0x3',
+ },
+ ]
+
+ const wrapper = shallow(
+ <AccountDropdownMini
+ selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
+ accounts={accounts}
+ dropdownOpen={true}
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(AccountListItem).length, 4)
+ })
+
+ it('should render a single account when disabled', () => {
+ const accounts = [
+ {
+ address: '0x1',
+ name: 'account1',
+ balance: '0x1',
+ },
+ {
+ address: '0x2',
+ name: 'account2',
+ balance: '0x2',
+ },
+ {
+ address: '0x3',
+ name: 'account3',
+ balance: '0x3',
+ },
+ ]
+
+ const wrapper = shallow(
+ <AccountDropdownMini
+ selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }}
+ accounts={accounts}
+ dropdownOpen={false}
+ disabled={true}
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(AccountListItem).length, 1)
+ const accountListItemProps = wrapper.find(AccountListItem).at(0).props()
+ assert.equal(accountListItemProps.account.address, '0x1')
+ assert.equal(accountListItemProps.icon, false)
+ })
+})
diff --git a/ui/app/components/alert/index.js b/ui/app/components/ui/alert/index.js
index 5620d847a..5620d847a 100644
--- a/ui/app/components/alert/index.js
+++ b/ui/app/components/ui/alert/index.js
diff --git a/ui/app/components/ui/balance/balance.component.js b/ui/app/components/ui/balance/balance.component.js
new file mode 100644
index 000000000..9a6f71ce5
--- /dev/null
+++ b/ui/app/components/ui/balance/balance.component.js
@@ -0,0 +1,92 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TokenBalance from '../token-balance'
+import Identicon from '../identicon'
+import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
+import { formatBalance } from '../../../helpers/utils/util'
+
+export default class Balance extends PureComponent {
+ static propTypes = {
+ account: PropTypes.object,
+ assetImages: PropTypes.object,
+ nativeCurrency: PropTypes.string,
+ needsParse: PropTypes.bool,
+ network: PropTypes.string,
+ showFiat: PropTypes.bool,
+ token: PropTypes.object,
+ }
+
+ static defaultProps = {
+ needsParse: true,
+ showFiat: true,
+ }
+
+ renderBalance () {
+ const { account, nativeCurrency, needsParse, showFiat } = this.props
+ const balanceValue = account && account.balance
+ const formattedBalance = balanceValue
+ ? formatBalance(balanceValue, 6, needsParse, nativeCurrency)
+ : '...'
+
+ if (formattedBalance === 'None' || formattedBalance === '...') {
+ return (
+ <div className="flex-column balance-display">
+ <div className="token-amount">
+ { formattedBalance }
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="flex-column balance-display">
+ <UserPreferencedCurrencyDisplay
+ className="token-amount"
+ value={balanceValue}
+ type={PRIMARY}
+ ethNumberOfDecimals={4}
+ />
+ {
+ showFiat && (
+ <UserPreferencedCurrencyDisplay
+ value={balanceValue}
+ type={SECONDARY}
+ ethNumberOfDecimals={4}
+ />
+ )
+ }
+ </div>
+ )
+ }
+
+ renderTokenBalance () {
+ const { token } = this.props
+
+ return (
+ <div className="flex-column balance-display">
+ <div className="token-amount">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { token, network, assetImages } = this.props
+ const address = token && token.address
+ const image = assetImages && address ? assetImages[token.address] : undefined
+
+ return (
+ <div className="balance-container">
+ <Identicon
+ diameter={50}
+ address={address}
+ network={network}
+ image={image}
+ />
+ { token ? this.renderTokenBalance() : this.renderBalance() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/ui/balance/balance.container.js b/ui/app/components/ui/balance/balance.container.js
new file mode 100644
index 000000000..2ad5c5ad8
--- /dev/null
+++ b/ui/app/components/ui/balance/balance.container.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux'
+import Balance from './balance.component'
+import {
+ getNativeCurrency,
+ getAssetImages,
+ conversionRateSelector,
+ getCurrentCurrency,
+ getMetaMaskAccounts,
+ getIsMainnet,
+ preferencesSelector,
+} from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const accounts = getMetaMaskAccounts(state)
+ const network = state.metamask.network
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ const account = accounts[selectedAddress]
+
+ return {
+ account,
+ network,
+ nativeCurrency: getNativeCurrency(state),
+ conversionRate: conversionRateSelector(state),
+ currentCurrency: getCurrentCurrency(state),
+ assetImages: getAssetImages(state),
+ showFiat: (isMainnet || !!showFiatInTestnets),
+ }
+}
+
+export default connect(mapStateToProps)(Balance)
diff --git a/ui/app/components/balance/index.js b/ui/app/components/ui/balance/index.js
index f8fb9ea19..f8fb9ea19 100644
--- a/ui/app/components/balance/index.js
+++ b/ui/app/components/ui/balance/index.js
diff --git a/ui/app/components/breadcrumbs/breadcrumbs.component.js b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js
index 6644836db..6644836db 100644
--- a/ui/app/components/breadcrumbs/breadcrumbs.component.js
+++ b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js
diff --git a/ui/app/components/breadcrumbs/index.js b/ui/app/components/ui/breadcrumbs/index.js
index 07a11574f..07a11574f 100644
--- a/ui/app/components/breadcrumbs/index.js
+++ b/ui/app/components/ui/breadcrumbs/index.js
diff --git a/ui/app/components/breadcrumbs/index.scss b/ui/app/components/ui/breadcrumbs/index.scss
index e23aa7970..e23aa7970 100644
--- a/ui/app/components/breadcrumbs/index.scss
+++ b/ui/app/components/ui/breadcrumbs/index.scss
diff --git a/ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js
index 5013c5b60..5013c5b60 100644
--- a/ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js
+++ b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js
diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/ui/button-group/button-group.component.js
index 17a281030..17a281030 100644
--- a/ui/app/components/button-group/button-group.component.js
+++ b/ui/app/components/ui/button-group/button-group.component.js
diff --git a/ui/app/components/ui/button-group/button-group.stories.js b/ui/app/components/ui/button-group/button-group.stories.js
new file mode 100644
index 000000000..c58c628b3
--- /dev/null
+++ b/ui/app/components/ui/button-group/button-group.stories.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import ButtonGroup from '.'
+import Button from '../button'
+import { text, boolean } from '@storybook/addon-knobs/react'
+
+storiesOf('ButtonGroup', module)
+ .add('with Buttons', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ defaultActiveButtonIndex={1}
+ >
+ <Button
+ onClick={action('cheap')}
+ >
+ {text('Button1', 'Cheap')}
+ </Button>
+ <Button
+ onClick={action('average')}
+ >
+ {text('Button2', 'Average')}
+ </Button>
+ <Button
+ onClick={action('fast')}
+ >
+ {text('Button3', 'Fast')}
+ </Button>
+ </ButtonGroup>
+ )
+ .add('with a disabled Button', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ >
+ <Button
+ onClick={action('enabled')}
+ >
+ {text('Button1', 'Enabled')}
+ </Button>
+ <Button
+ onClick={action('disabled')}
+ disabled
+ >
+ {text('Button2', 'Disabled')}
+ </Button>
+ </ButtonGroup>
+ )
diff --git a/ui/app/components/button-group/index.js b/ui/app/components/ui/button-group/index.js
index df470bd57..df470bd57 100644
--- a/ui/app/components/button-group/index.js
+++ b/ui/app/components/ui/button-group/index.js
diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/ui/button-group/index.scss
index 29713c75b..29713c75b 100644
--- a/ui/app/components/button-group/index.scss
+++ b/ui/app/components/ui/button-group/index.scss
diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/tests/button-group-component.test.js
index 0bece90d6..0bece90d6 100644
--- a/ui/app/components/button-group/tests/button-group-component.test.js
+++ b/ui/app/components/ui/button-group/tests/button-group-component.test.js
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/ui/button/button.component.js
index 5d19219b4..5d19219b4 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/ui/button/button.component.js
diff --git a/ui/app/components/ui/button/button.stories.js b/ui/app/components/ui/button/button.stories.js
new file mode 100644
index 000000000..667824a47
--- /dev/null
+++ b/ui/app/components/ui/button/button.stories.js
@@ -0,0 +1,58 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import Button from '.'
+import { text } from '@storybook/addon-knobs/react'
+
+storiesOf('Button', module)
+ .add('primary', () =>
+ <Button
+ onClick={action('clicked')}
+ type="primary"
+ >
+ {text('text', 'Click me')}
+ </Button>
+ )
+ .add('secondary', () =>
+ <Button
+ onClick={action('clicked')}
+ type="secondary"
+ >
+ {text('text', 'Click me')}
+ </Button>
+ )
+ .add('default', () => (
+ <Button
+ onClick={action('clicked')}
+ type="default"
+ >
+ {text('text', 'Click me')}
+ </Button>
+ ))
+ .add('large primary', () => (
+ <Button
+ onClick={action('clicked')}
+ type="primary"
+ large
+ >
+ {text('text', 'Click me')}
+ </Button>
+ ))
+ .add('large secondary', () => (
+ <Button
+ onClick={action('clicked')}
+ type="secondary"
+ large
+ >
+ {text('text', 'Click me')}
+ </Button>
+ ))
+ .add('large default', () => (
+ <Button
+ onClick={action('clicked')}
+ type="default"
+ large
+ >
+ {text('text', 'Click me')}
+ </Button>
+ ))
diff --git a/ui/app/components/button/index.js b/ui/app/components/ui/button/index.js
index 33ae95ae2..33ae95ae2 100644
--- a/ui/app/components/button/index.js
+++ b/ui/app/components/ui/button/index.js
diff --git a/ui/app/components/card/card.component.js b/ui/app/components/ui/card/card.component.js
index bb7241da1..bb7241da1 100644
--- a/ui/app/components/card/card.component.js
+++ b/ui/app/components/ui/card/card.component.js
diff --git a/ui/app/components/card/index.js b/ui/app/components/ui/card/index.js
index c3ca6e3f4..c3ca6e3f4 100644
--- a/ui/app/components/card/index.js
+++ b/ui/app/components/ui/card/index.js
diff --git a/ui/app/components/card/index.scss b/ui/app/components/ui/card/index.scss
index bde54a15e..bde54a15e 100644
--- a/ui/app/components/card/index.scss
+++ b/ui/app/components/ui/card/index.scss
diff --git a/ui/app/components/card/tests/card.component.test.js b/ui/app/components/ui/card/tests/card.component.test.js
index cea05033f..cea05033f 100644
--- a/ui/app/components/card/tests/card.component.test.js
+++ b/ui/app/components/ui/card/tests/card.component.test.js
diff --git a/ui/app/components/copyButton.js b/ui/app/components/ui/copyButton.js
index a60d33523..a60d33523 100644
--- a/ui/app/components/copyButton.js
+++ b/ui/app/components/ui/copyButton.js
diff --git a/ui/app/components/ui/currency-display/currency-display.component.js b/ui/app/components/ui/currency-display/currency-display.component.js
new file mode 100644
index 000000000..04dd89892
--- /dev/null
+++ b/ui/app/components/ui/currency-display/currency-display.component.js
@@ -0,0 +1,46 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { GWEI } from '../../../helpers/constants/common'
+
+export default class CurrencyDisplay extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ displayValue: PropTypes.string,
+ prefix: PropTypes.string,
+ prefixComponent: PropTypes.node,
+ style: PropTypes.object,
+ suffix: PropTypes.string,
+ // Used in container
+ currency: PropTypes.string,
+ denomination: PropTypes.oneOf([GWEI]),
+ value: PropTypes.string,
+ numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ hideLabel: PropTypes.bool,
+ hideTitle: PropTypes.bool,
+ }
+
+ render () {
+ const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props
+ const text = `${prefix || ''}${displayValue}`
+ const title = `${text} ${suffix}`
+
+ return (
+ <div
+ className={classnames('currency-display-component', className)}
+ style={style}
+ title={!hideTitle && title || null}
+ >
+ { prefixComponent }
+ <span className="currency-display-component__text">{ text }</span>
+ {
+ suffix && (
+ <span className="currency-display-component__suffix">
+ { suffix }
+ </span>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/ui/currency-display/currency-display.container.js b/ui/app/components/ui/currency-display/currency-display.container.js
new file mode 100644
index 000000000..093d99c8e
--- /dev/null
+++ b/ui/app/components/ui/currency-display/currency-display.container.js
@@ -0,0 +1,51 @@
+import { connect } from 'react-redux'
+import CurrencyDisplay from './currency-display.component'
+import { getValueFromWeiHex, formatCurrency } from '../../../helpers/utils/confirm-tx.util'
+
+const mapStateToProps = state => {
+ const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
+
+ return {
+ currentCurrency,
+ conversionRate,
+ nativeCurrency,
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { nativeCurrency, currentCurrency, conversionRate, ...restStateProps } = stateProps
+ const {
+ value,
+ numberOfDecimals = 2,
+ currency,
+ denomination,
+ hideLabel,
+ displayValue: propsDisplayValue,
+ suffix: propsSuffix,
+ ...restOwnProps
+ } = ownProps
+
+ const toCurrency = currency || currentCurrency
+
+ const displayValue = propsDisplayValue || formatCurrency(
+ getValueFromWeiHex({
+ value,
+ fromCurrency: nativeCurrency,
+ toCurrency, conversionRate,
+ numberOfDecimals,
+ toDenomination: denomination,
+ }),
+ toCurrency
+ )
+ const suffix = propsSuffix || (hideLabel ? undefined : toCurrency.toUpperCase())
+
+ return {
+ ...restStateProps,
+ ...dispatchProps,
+ ...restOwnProps,
+ displayValue,
+ suffix,
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay)
diff --git a/ui/app/components/currency-display/index.js b/ui/app/components/ui/currency-display/index.js
index 38f08765f..38f08765f 100644
--- a/ui/app/components/currency-display/index.js
+++ b/ui/app/components/ui/currency-display/index.js
diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/ui/currency-display/index.scss
index 313c932b8..313c932b8 100644
--- a/ui/app/components/currency-display/index.scss
+++ b/ui/app/components/ui/currency-display/index.scss
diff --git a/ui/app/components/currency-display/tests/currency-display.component.test.js b/ui/app/components/ui/currency-display/tests/currency-display.component.test.js
index d9ef052f1..d9ef052f1 100644
--- a/ui/app/components/currency-display/tests/currency-display.component.test.js
+++ b/ui/app/components/ui/currency-display/tests/currency-display.component.test.js
diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js
index 9888c366e..9888c366e 100644
--- a/ui/app/components/currency-display/tests/currency-display.container.test.js
+++ b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js
diff --git a/ui/app/components/ui/currency-input/currency-input.component.js b/ui/app/components/ui/currency-input/currency-input.component.js
new file mode 100644
index 000000000..b5be0972b
--- /dev/null
+++ b/ui/app/components/ui/currency-input/currency-input.component.js
@@ -0,0 +1,160 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import UnitInput from '../unit-input'
+import CurrencyDisplay from '../currency-display'
+import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../../helpers/utils/conversions.util'
+import { ETH } from '../../../helpers/constants/common'
+
+/**
+ * Component that allows user to enter currency values as a number, and props receive a converted
+ * hex value in WEI. props.value, used as a default or forced value, should be a hex value, which
+ * gets converted into a decimal value depending on the currency (ETH or Fiat).
+ */
+export default class CurrencyInput extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ nativeCurrency: PropTypes.string,
+ onChange: PropTypes.func,
+ onBlur: PropTypes.func,
+ useFiat: PropTypes.bool,
+ hideFiat: PropTypes.bool,
+ value: PropTypes.string,
+ fiatSuffix: PropTypes.string,
+ nativeSuffix: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ const { value: hexValue } = props
+ const decimalValue = hexValue ? this.getDecimalValue(props) : 0
+
+ this.state = {
+ decimalValue,
+ hexValue,
+ isSwapped: false,
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { value: prevPropsHexValue } = prevProps
+ const { value: propsHexValue } = this.props
+ const { hexValue: stateHexValue } = this.state
+
+ if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
+ const decimalValue = this.getDecimalValue(this.props)
+ this.setState({ hexValue: propsHexValue, decimalValue })
+ }
+ }
+
+ getDecimalValue (props) {
+ const { value: hexValue, currentCurrency, conversionRate } = props
+ const decimalValueString = this.shouldUseFiat()
+ ? getValueFromWeiHex({
+ value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
+ })
+ : getValueFromWeiHex({
+ value: hexValue, toCurrency: ETH, numberOfDecimals: 6,
+ })
+
+ return Number(decimalValueString) || 0
+ }
+
+ shouldUseFiat = () => {
+ const { useFiat, hideFiat } = this.props
+ const { isSwapped } = this.state || {}
+
+ if (hideFiat) {
+ return false
+ }
+
+ return isSwapped ? !useFiat : useFiat
+ }
+
+ swap = () => {
+ const { isSwapped, decimalValue } = this.state
+ this.setState({isSwapped: !isSwapped}, () => {
+ this.handleChange(decimalValue)
+ })
+ }
+
+ handleChange = decimalValue => {
+ const { currentCurrency: fromCurrency, conversionRate, onChange } = this.props
+
+ const hexValue = this.shouldUseFiat()
+ ? getWeiHexFromDecimalValue({
+ value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true,
+ })
+ : getWeiHexFromDecimalValue({
+ value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate,
+ })
+
+ this.setState({ hexValue, decimalValue })
+ onChange(hexValue)
+ }
+
+ handleBlur = () => {
+ this.props.onBlur(this.state.hexValue)
+ }
+
+ renderConversionComponent () {
+ const { currentCurrency, nativeCurrency, hideFiat } = this.props
+ const { hexValue } = this.state
+ let currency, numberOfDecimals
+
+ if (hideFiat) {
+ return (
+ <div className="currency-input__conversion-component">
+ { this.context.t('noConversionRateAvailable') }
+ </div>
+ )
+ }
+
+ if (this.shouldUseFiat()) {
+ // Display ETH
+ currency = nativeCurrency || ETH
+ numberOfDecimals = 6
+ } else {
+ // Display Fiat
+ currency = currentCurrency
+ numberOfDecimals = 2
+ }
+
+ return (
+ <CurrencyDisplay
+ className="currency-input__conversion-component"
+ currency={currency}
+ value={hexValue}
+ numberOfDecimals={numberOfDecimals}
+ />
+ )
+ }
+
+ render () {
+ const { fiatSuffix, nativeSuffix, ...restProps } = this.props
+ const { decimalValue } = this.state
+
+ return (
+ <UnitInput
+ {...restProps}
+ suffix={this.shouldUseFiat() ? fiatSuffix : nativeSuffix}
+ onChange={this.handleChange}
+ onBlur={this.handleBlur}
+ value={decimalValue}
+ actionComponent={(
+ <div
+ className="currency-input__swap-component"
+ onClick={this.swap}
+ />
+ )}
+ >
+ { this.renderConversionComponent() }
+ </UnitInput>
+ )
+ }
+}
diff --git a/ui/app/components/ui/currency-input/currency-input.container.js b/ui/app/components/ui/currency-input/currency-input.container.js
new file mode 100644
index 000000000..b5d7dfe6d
--- /dev/null
+++ b/ui/app/components/ui/currency-input/currency-input.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import CurrencyInput from './currency-input.component'
+import { ETH } from '../../../helpers/constants/common'
+import {getIsMainnet, preferencesSelector} from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+
+ return {
+ nativeCurrency,
+ currentCurrency,
+ conversionRate,
+ hideFiat: (!isMainnet && !showFiatInTestnets),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { nativeCurrency, currentCurrency } = stateProps
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ nativeSuffix: nativeCurrency || ETH,
+ fiatSuffix: currentCurrency.toUpperCase(),
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(CurrencyInput)
diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/ui/currency-input/index.js
index d8069fb67..d8069fb67 100644
--- a/ui/app/components/currency-input/index.js
+++ b/ui/app/components/ui/currency-input/index.js
diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/ui/currency-input/index.scss
index f659f5b35..f659f5b35 100644
--- a/ui/app/components/currency-input/index.scss
+++ b/ui/app/components/ui/currency-input/index.scss
diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js
index 6d4612e3c..6d4612e3c 100644
--- a/ui/app/components/currency-input/tests/currency-input.component.test.js
+++ b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js
diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js
index 6109d29b6..6109d29b6 100644
--- a/ui/app/components/currency-input/tests/currency-input.container.test.js
+++ b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js
diff --git a/ui/app/components/editable-label.js b/ui/app/components/ui/editable-label.js
index eb41ec50c..eb41ec50c 100644
--- a/ui/app/components/editable-label.js
+++ b/ui/app/components/ui/editable-label.js
diff --git a/ui/app/components/error-message/error-message.component.js b/ui/app/components/ui/error-message/error-message.component.js
index b4464c33b..b4464c33b 100644
--- a/ui/app/components/error-message/error-message.component.js
+++ b/ui/app/components/ui/error-message/error-message.component.js
diff --git a/ui/app/components/error-message/index.js b/ui/app/components/ui/error-message/index.js
index 1c97a9955..1c97a9955 100644
--- a/ui/app/components/error-message/index.js
+++ b/ui/app/components/ui/error-message/index.js
diff --git a/ui/app/components/error-message/index.scss b/ui/app/components/ui/error-message/index.scss
index 5915e21cf..5915e21cf 100644
--- a/ui/app/components/error-message/index.scss
+++ b/ui/app/components/ui/error-message/index.scss
diff --git a/ui/app/components/error-message/tests/error-message.component.test.js b/ui/app/components/ui/error-message/tests/error-message.component.test.js
index 8c5347173..8c5347173 100644
--- a/ui/app/components/error-message/tests/error-message.component.test.js
+++ b/ui/app/components/ui/error-message/tests/error-message.component.test.js
diff --git a/ui/app/components/ui/eth-balance.js b/ui/app/components/ui/eth-balance.js
new file mode 100644
index 000000000..7d577b716
--- /dev/null
+++ b/ui/app/components/ui/eth-balance.js
@@ -0,0 +1,102 @@
+const { Component } = require('react')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const { inherits } = require('util')
+const {
+ formatBalance,
+ generateBalanceObject,
+} = require('../../helpers/utils/util')
+const Tooltip = require('./tooltip.js')
+const FiatValue = require('./fiat-value.js')
+
+module.exports = connect(mapStateToProps)(EthBalanceComponent)
+function mapStateToProps (state) {
+ return {
+ ticker: state.metamask.ticker,
+ }
+}
+
+inherits(EthBalanceComponent, Component)
+function EthBalanceComponent () {
+ Component.call(this)
+}
+
+EthBalanceComponent.prototype.render = function () {
+ const props = this.props
+ const { ticker, value, style, width, needsParse = true } = props
+
+ const formattedValue = value ? formatBalance(value, 6, needsParse, ticker) : '...'
+
+ return (
+
+ h('.ether-balance.ether-balance-amount', {
+ style,
+ }, [
+ h('div', {
+ style: {
+ display: 'inline',
+ width,
+ },
+ }, this.renderBalance(formattedValue)),
+ ])
+
+ )
+}
+EthBalanceComponent.prototype.renderBalance = function (value) {
+ if (value === 'None') return value
+ if (value === '...') return value
+
+ const {
+ conversionRate,
+ shorten,
+ incoming,
+ currentCurrency,
+ hideTooltip,
+ styleOveride = {},
+ showFiat = true,
+ } = this.props
+ const { fontSize, color, fontFamily, lineHeight } = styleOveride
+
+ const { shortBalance, balance, label } = generateBalanceObject(value, shorten ? 1 : 3)
+ const balanceToRender = shorten ? shortBalance : balance
+
+ const [ethNumber, ethSuffix] = value.split(' ')
+ const containerProps = hideTooltip ? {} : {
+ position: 'bottom',
+ title: `${ethNumber} ${ethSuffix}`,
+ }
+
+ return (
+ h(hideTooltip ? 'div' : Tooltip,
+ containerProps,
+ h('div.flex-column', [
+ h('.flex-row', {
+ style: {
+ alignItems: 'flex-end',
+ lineHeight: lineHeight || '13px',
+ fontFamily: fontFamily || 'Montserrat Light',
+ textRendering: 'geometricPrecision',
+ },
+ }, [
+ h('div', {
+ style: {
+ width: '100%',
+ textAlign: 'right',
+ fontSize: fontSize || 'inherit',
+ color: color || 'inherit',
+ },
+ }, incoming ? `+${balanceToRender}` : balanceToRender),
+ h('div', {
+ style: {
+ color: color || '#AEAEAE',
+ fontSize: fontSize || '12px',
+ marginLeft: '5px',
+ },
+ }, label),
+ ]),
+
+ showFiat ? h(FiatValue, { value: this.props.value, conversionRate, currentCurrency }) : null,
+ ])
+ )
+ )
+}
diff --git a/ui/app/components/ui/export-text-container/export-text-container.component.js b/ui/app/components/ui/export-text-container/export-text-container.component.js
new file mode 100644
index 000000000..c632e8f26
--- /dev/null
+++ b/ui/app/components/ui/export-text-container/export-text-container.component.js
@@ -0,0 +1,45 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const copyToClipboard = require('copy-to-clipboard')
+const { exportAsFile } = require('../../../helpers/utils/util')
+
+class ExportTextContainer extends Component {
+ render () {
+ const { text = '', filename = '' } = this.props
+ const { t } = this.context
+
+ return (
+ h('.export-text-container', [
+ h('.export-text-container__text-container', [
+ h('.export-text-container__text', text),
+ ]),
+ h('.export-text-container__buttons-container', [
+ h('.export-text-container__button.export-text-container__button--copy', {
+ onClick: () => copyToClipboard(text),
+ }, [
+ h('img', { src: 'images/copy-to-clipboard.svg' }),
+ h('.export-text-container__button-text', t('copyToClipboard')),
+ ]),
+ h('.export-text-container__button', {
+ onClick: () => exportAsFile(filename, text),
+ }, [
+ h('img', { src: 'images/download.svg' }),
+ h('.export-text-container__button-text', t('saveAsCsvFile')),
+ ]),
+ ]),
+ ])
+ )
+ }
+}
+
+ExportTextContainer.propTypes = {
+ text: PropTypes.string,
+ filename: PropTypes.string,
+}
+
+ExportTextContainer.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ExportTextContainer
diff --git a/ui/app/components/export-text-container/index.js b/ui/app/components/ui/export-text-container/index.js
index b2864a717..b2864a717 100644
--- a/ui/app/components/export-text-container/index.js
+++ b/ui/app/components/ui/export-text-container/index.js
diff --git a/ui/app/components/export-text-container/index.scss b/ui/app/components/ui/export-text-container/index.scss
index 975d62f70..975d62f70 100644
--- a/ui/app/components/export-text-container/index.scss
+++ b/ui/app/components/ui/export-text-container/index.scss
diff --git a/ui/app/components/ui/fiat-value.js b/ui/app/components/ui/fiat-value.js
new file mode 100644
index 000000000..02111ba49
--- /dev/null
+++ b/ui/app/components/ui/fiat-value.js
@@ -0,0 +1,66 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const formatBalance = require('../../helpers/utils/util').formatBalance
+
+module.exports = FiatValue
+
+inherits(FiatValue, Component)
+function FiatValue () {
+ Component.call(this)
+}
+
+FiatValue.prototype.render = function () {
+ const props = this.props
+ const { conversionRate, currentCurrency, style } = props
+ const renderedCurrency = currentCurrency || ''
+
+ const value = formatBalance(props.value, 6)
+
+ if (value === 'None') return value
+ var fiatDisplayNumber, fiatTooltipNumber
+ var splitBalance = value.split(' ')
+
+ if (conversionRate !== 0) {
+ fiatTooltipNumber = Number(splitBalance[0]) * conversionRate
+ fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
+ } else {
+ fiatDisplayNumber = 'N/A'
+ fiatTooltipNumber = 'Unknown'
+ }
+
+ return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase(), style)
+}
+
+function fiatDisplay (fiatDisplayNumber, fiatSuffix, styleOveride = {}) {
+ const { fontSize, color, fontFamily, lineHeight } = styleOveride
+
+ if (fiatDisplayNumber !== 'N/A') {
+ return h('.flex-row', {
+ style: {
+ alignItems: 'flex-end',
+ lineHeight: lineHeight || '13px',
+ fontFamily: fontFamily || 'Montserrat Light',
+ textRendering: 'geometricPrecision',
+ },
+ }, [
+ h('div', {
+ style: {
+ width: '100%',
+ textAlign: 'right',
+ fontSize: fontSize || '12px',
+ color: color || '#333333',
+ },
+ }, fiatDisplayNumber),
+ h('div', {
+ style: {
+ color: color || '#AEAEAE',
+ marginLeft: '5px',
+ fontSize: fontSize || '12px',
+ },
+ }, fiatSuffix),
+ ])
+ } else {
+ return h('div')
+ }
+}
diff --git a/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js b/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js
new file mode 100644
index 000000000..f03aaf255
--- /dev/null
+++ b/ui/app/components/ui/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/utils/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/ui/hex-to-decimal/index.js
index 6e8567ca9..6e8567ca9 100644
--- a/ui/app/components/hex-to-decimal/index.js
+++ b/ui/app/components/ui/hex-to-decimal/index.js
diff --git a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js b/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js
index c98da9ad4..c98da9ad4 100644
--- a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
+++ b/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js
diff --git a/ui/app/components/ui/identicon/identicon.component.js b/ui/app/components/ui/identicon/identicon.component.js
new file mode 100644
index 000000000..88521247c
--- /dev/null
+++ b/ui/app/components/ui/identicon/identicon.component.js
@@ -0,0 +1,99 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { toDataUrl } from '../../../../lib/blockies'
+import contractMap from 'eth-contract-metadata'
+import { checksumAddress } from '../../../helpers/utils/util'
+import Jazzicon from '../jazzicon'
+
+const getStyles = diameter => (
+ {
+ height: diameter,
+ width: diameter,
+ borderRadius: diameter / 2,
+ }
+)
+
+export default class Identicon extends PureComponent {
+ static propTypes = {
+ address: PropTypes.string,
+ className: PropTypes.string,
+ diameter: PropTypes.number,
+ image: PropTypes.string,
+ useBlockie: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ diameter: 46,
+ }
+
+ renderImage () {
+ const { className, diameter, image } = this.props
+
+ return (
+ <img
+ className={classnames('identicon', className)}
+ src={image}
+ style={getStyles(diameter)}
+ />
+ )
+ }
+
+ renderJazzicon () {
+ const { address, className, diameter } = this.props
+
+ return (
+ <Jazzicon
+ address={address}
+ diameter={diameter}
+ className={classnames('identicon', className)}
+ style={getStyles(diameter)}
+ />
+ )
+ }
+
+ renderBlockie () {
+ const { address, className, diameter } = this.props
+
+ return (
+ <div
+ className={classnames('identicon', className)}
+ style={getStyles(diameter)}
+ >
+ <img
+ src={toDataUrl(address)}
+ height={diameter}
+ width={diameter}
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const { className, address, image, diameter, useBlockie } = this.props
+
+ if (image) {
+ return this.renderImage()
+ }
+
+ if (address) {
+ const checksummedAddress = checksumAddress(address)
+
+ if (contractMap[checksummedAddress] && contractMap[checksummedAddress].logo) {
+ return this.renderJazzicon()
+ }
+
+ return useBlockie
+ ? this.renderBlockie()
+ : this.renderJazzicon()
+ }
+
+ return (
+ <img
+ className={classnames('balance-icon', className)}
+ src="./images/eth_logo.svg"
+ style={getStyles(diameter)}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/identicon/identicon.container.js b/ui/app/components/ui/identicon/identicon.container.js
index bc49bc18e..bc49bc18e 100644
--- a/ui/app/components/identicon/identicon.container.js
+++ b/ui/app/components/ui/identicon/identicon.container.js
diff --git a/ui/app/components/identicon/index.js b/ui/app/components/ui/identicon/index.js
index 799c886f2..799c886f2 100644
--- a/ui/app/components/identicon/index.js
+++ b/ui/app/components/ui/identicon/index.js
diff --git a/ui/app/components/identicon/index.scss b/ui/app/components/ui/identicon/index.scss
index 657afc48f..657afc48f 100644
--- a/ui/app/components/identicon/index.scss
+++ b/ui/app/components/ui/identicon/index.scss
diff --git a/ui/app/components/identicon/tests/identicon.component.test.js b/ui/app/components/ui/identicon/tests/identicon.component.test.js
index 2944818f5..2944818f5 100644
--- a/ui/app/components/identicon/tests/identicon.component.test.js
+++ b/ui/app/components/ui/identicon/tests/identicon.component.test.js
diff --git a/ui/app/components/jazzicon/index.js b/ui/app/components/ui/jazzicon/index.js
index bea900ab9..bea900ab9 100644
--- a/ui/app/components/jazzicon/index.js
+++ b/ui/app/components/ui/jazzicon/index.js
diff --git a/ui/app/components/ui/jazzicon/jazzicon.component.js b/ui/app/components/ui/jazzicon/jazzicon.component.js
new file mode 100644
index 000000000..3a17e446f
--- /dev/null
+++ b/ui/app/components/ui/jazzicon/jazzicon.component.js
@@ -0,0 +1,69 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import isNode from 'detect-node'
+import { findDOMNode } from 'react-dom'
+import jazzicon from 'jazzicon'
+import iconFactoryGenerator from '../../../../lib/icon-factory'
+const iconFactory = iconFactoryGenerator(jazzicon)
+
+/**
+ * Wrapper around the jazzicon library to return a React component, as the library returns an
+ * HTMLDivElement which needs to be appended.
+ */
+export default class Jazzicon extends PureComponent {
+ static propTypes = {
+ address: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ diameter: PropTypes.number,
+ style: PropTypes.object,
+ }
+
+ static defaultProps = {
+ diameter: 46,
+ }
+
+ componentDidMount () {
+ if (!isNode) {
+ this.appendJazzicon()
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { address: prevAddress } = prevProps
+ const { address } = this.props
+
+ if (!isNode && address !== prevAddress) {
+ this.removeExistingChildren()
+ this.appendJazzicon()
+ }
+ }
+
+ removeExistingChildren () {
+ // eslint-disable-next-line react/no-find-dom-node
+ const container = findDOMNode(this)
+ const { children } = container
+
+ for (let i = 0; i < children.length; i++) {
+ container.removeChild(children[i])
+ }
+ }
+
+ appendJazzicon () {
+ // eslint-disable-next-line react/no-find-dom-node
+ const container = findDOMNode(this)
+ const { address, diameter } = this.props
+ const image = iconFactory.iconForAddress(address, diameter)
+ container.appendChild(image)
+ }
+
+ render () {
+ const { className, style } = this.props
+
+ return (
+ <div
+ className={className}
+ style={style}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/loading-screen/index.js b/ui/app/components/ui/loading-screen/index.js
index 191d953f7..191d953f7 100644
--- a/ui/app/components/loading-screen/index.js
+++ b/ui/app/components/ui/loading-screen/index.js
diff --git a/ui/app/components/loading-screen/loading-screen.component.js b/ui/app/components/ui/loading-screen/loading-screen.component.js
index 6b843cfee..6b843cfee 100644
--- a/ui/app/components/loading-screen/loading-screen.component.js
+++ b/ui/app/components/ui/loading-screen/loading-screen.component.js
diff --git a/ui/app/components/lock-icon/index.js b/ui/app/components/ui/lock-icon/index.js
index 6b4df0e58..6b4df0e58 100644
--- a/ui/app/components/lock-icon/index.js
+++ b/ui/app/components/ui/lock-icon/index.js
diff --git a/ui/app/components/lock-icon/lock-icon.component.js b/ui/app/components/ui/lock-icon/lock-icon.component.js
index d010cb6b2..d010cb6b2 100644
--- a/ui/app/components/lock-icon/lock-icon.component.js
+++ b/ui/app/components/ui/lock-icon/lock-icon.component.js
diff --git a/ui/app/components/mascot.js b/ui/app/components/ui/mascot.js
index 3b0d3e31b..3b0d3e31b 100644
--- a/ui/app/components/mascot.js
+++ b/ui/app/components/ui/mascot.js
diff --git a/ui/app/components/page-container/index.js b/ui/app/components/ui/page-container/index.js
index 913b8c9c6..913b8c9c6 100644
--- a/ui/app/components/page-container/index.js
+++ b/ui/app/components/ui/page-container/index.js
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/ui/page-container/index.scss
index b71a3cb9d..b71a3cb9d 100644
--- a/ui/app/components/page-container/index.scss
+++ b/ui/app/components/ui/page-container/index.scss
diff --git a/ui/app/components/page-container/page-container-content.component.js b/ui/app/components/ui/page-container/page-container-content.component.js
index a1d6988cc..a1d6988cc 100644
--- a/ui/app/components/page-container/page-container-content.component.js
+++ b/ui/app/components/ui/page-container/page-container-content.component.js
diff --git a/ui/app/components/page-container/page-container-footer/index.js b/ui/app/components/ui/page-container/page-container-footer/index.js
index 7825c4520..7825c4520 100644
--- a/ui/app/components/page-container/page-container-footer/index.js
+++ b/ui/app/components/ui/page-container/page-container-footer/index.js
diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
index 85b16cefe..85b16cefe 100644
--- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
+++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
diff --git a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js
index 64efabab0..64efabab0 100644
--- a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
+++ b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js
diff --git a/ui/app/components/page-container/page-container-header/index.js b/ui/app/components/ui/page-container/page-container-header/index.js
index b194af057..b194af057 100644
--- a/ui/app/components/page-container/page-container-header/index.js
+++ b/ui/app/components/ui/page-container/page-container-header/index.js
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js
index 08f9c7544..08f9c7544 100644
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js
diff --git a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js
index 59304b2bd..59304b2bd 100644
--- a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
+++ b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js
diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/ui/page-container/page-container.component.js
index 45dfff517..45dfff517 100644
--- a/ui/app/components/page-container/page-container.component.js
+++ b/ui/app/components/ui/page-container/page-container.component.js
diff --git a/ui/app/components/page-container/tests/page-container.component.test.js b/ui/app/components/ui/page-container/tests/page-container.component.test.js
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/page-container/tests/page-container.component.test.js
+++ b/ui/app/components/ui/page-container/tests/page-container.component.test.js
diff --git a/ui/app/components/ui/qr-code.js b/ui/app/components/ui/qr-code.js
new file mode 100644
index 000000000..351e072e2
--- /dev/null
+++ b/ui/app/components/ui/qr-code.js
@@ -0,0 +1,63 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const qrCode = require('qrcode-generator')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const { isHexPrefixed } = require('ethereumjs-util')
+const ReadOnlyInput = require('./readonly-input')
+const { checksumAddress } = require('../../helpers/utils/util')
+
+module.exports = connect(mapStateToProps)(QrCodeView)
+
+function mapStateToProps (state) {
+ return {
+ // Qr code is not fetched from state. 'message' and 'data' props are passed instead.
+ buyView: state.appState.buyView,
+ warning: state.appState.warning,
+ }
+}
+
+inherits(QrCodeView, Component)
+
+function QrCodeView () {
+ Component.call(this)
+}
+
+QrCodeView.prototype.render = function () {
+ const props = this.props
+ const { message, data, network } = props.Qr
+ const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data, network)}`
+ const qrImage = qrCode(4, 'M')
+ qrImage.addData(address)
+ qrImage.make()
+
+ return h('.div.flex-column.flex-center', [
+ Array.isArray(message)
+ ? h('.message-container', this.renderMultiMessage())
+ : message && h('.qr-header', message),
+
+ this.props.warning ? this.props.warning && h('span.error.flex-center', {
+ style: {
+ },
+ },
+ this.props.warning) : null,
+
+ h('.div.qr-wrapper', {
+ style: {},
+ dangerouslySetInnerHTML: {
+ __html: qrImage.createTableTag(4),
+ },
+ }),
+ h(ReadOnlyInput, {
+ wrapperClass: 'ellip-address-wrapper',
+ inputClass: 'qr-ellip-address',
+ value: checksumAddress(data, network),
+ }),
+ ])
+}
+
+QrCodeView.prototype.renderMultiMessage = function () {
+ var Qr = this.props.Qr
+ var multiMessage = Qr.message.map((message) => h('.qr-message', message))
+ return multiMessage
+}
diff --git a/ui/app/components/readonly-input.js b/ui/app/components/ui/readonly-input.js
index fcf05fb9e..fcf05fb9e 100644
--- a/ui/app/components/readonly-input.js
+++ b/ui/app/components/ui/readonly-input.js
diff --git a/ui/app/components/sender-to-recipient/index.js b/ui/app/components/ui/sender-to-recipient/index.js
index f515c4ac4..f515c4ac4 100644
--- a/ui/app/components/sender-to-recipient/index.js
+++ b/ui/app/components/ui/sender-to-recipient/index.js
diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/ui/sender-to-recipient/index.scss
index b21e4e1bb..b21e4e1bb 100644
--- a/ui/app/components/sender-to-recipient/index.scss
+++ b/ui/app/components/ui/sender-to-recipient/index.scss
diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js
new file mode 100644
index 000000000..57b595d48
--- /dev/null
+++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js
@@ -0,0 +1,186 @@
+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, FLAT_VARIANT } from './sender-to-recipient.constants'
+import { checksumAddress } from '../../../helpers/utils/util'
+
+const variantHash = {
+ [DEFAULT_VARIANT]: 'sender-to-recipient--default',
+ [CARDS_VARIANT]: 'sender-to-recipient--cards',
+ [FLAT_VARIANT]: 'sender-to-recipient--flat',
+}
+
+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, FLAT_VARIANT]),
+ addressOnly: PropTypes.bool,
+ assetImage: PropTypes.string,
+ onRecipientClick: PropTypes.func,
+ onSenderClick: PropTypes.func,
+ }
+
+ static defaultProps = {
+ variant: DEFAULT_VARIANT,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ senderAddressCopied: false,
+ 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, addressOnly, onRecipientClick } = this.props
+ const checksummedRecipientAddress = checksumAddress(recipientAddress)
+
+ return (
+ <div
+ className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
+ onClick={() => {
+ this.setState({ recipientAddressCopied: true })
+ copyToClipboard(checksummedRecipientAddress)
+ if (onRecipientClick) {
+ onRecipientClick()
+ }
+ }}
+ >
+ { this.renderRecipientIdenticon() }
+ <Tooltip
+ position="bottom"
+ title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ recipientAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name">
+ {
+ addressOnly
+ ? `${t('to')}: ${checksummedRecipientAddress}`
+ : (recipientName || this.context.t('newContract'))
+ }
+ </div>
+ </Tooltip>
+ </div>
+ )
+ }
+
+ renderRecipientWithoutAddress () {
+ return (
+ <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>
+ )
+ }
+
+ renderArrow () {
+ return this.props.variant === DEFAULT_VARIANT
+ ? (
+ <div className="sender-to-recipient__arrow-container">
+ <div className="sender-to-recipient__arrow-circle">
+ <img
+ height={15}
+ width={15}
+ src="./images/arrow-right.svg"
+ />
+ </div>
+ </div>
+ ) : (
+ <div className="sender-to-recipient__arrow-container">
+ <img
+ height={20}
+ src="./images/caret-right.svg"
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const { senderAddress, recipientAddress, variant, onSenderClick } = this.props
+ const checksummedSenderAddress = checksumAddress(senderAddress)
+
+ return (
+ <div className={classnames('sender-to-recipient', variantHash[variant])}>
+ <div
+ className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
+ onClick={() => {
+ this.setState({ senderAddressCopied: true })
+ copyToClipboard(checksummedSenderAddress)
+ if (onSenderClick) {
+ onSenderClick()
+ }
+ }}
+ >
+ { this.renderSenderIdenticon() }
+ { this.renderSenderAddress() }
+ </div>
+ { this.renderArrow() }
+ {
+ recipientAddress
+ ? this.renderRecipientWithAddress()
+ : this.renderRecipientWithoutAddress()
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js
index f53a5115d..f53a5115d 100644
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
+++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js
diff --git a/ui/app/components/spinner/index.js b/ui/app/components/ui/spinner/index.js
index 9589efcf0..9589efcf0 100644
--- a/ui/app/components/spinner/index.js
+++ b/ui/app/components/ui/spinner/index.js
diff --git a/ui/app/components/spinner/spinner.component.js b/ui/app/components/ui/spinner/spinner.component.js
index b9a2eb52a..b9a2eb52a 100644
--- a/ui/app/components/spinner/spinner.component.js
+++ b/ui/app/components/ui/spinner/spinner.component.js
diff --git a/ui/app/components/tabs/index.js b/ui/app/components/ui/tabs/index.js
index 3a8d18248..3a8d18248 100644
--- a/ui/app/components/tabs/index.js
+++ b/ui/app/components/ui/tabs/index.js
diff --git a/ui/app/components/ui/tabs/index.scss b/ui/app/components/ui/tabs/index.scss
new file mode 100644
index 000000000..25143ff35
--- /dev/null
+++ b/ui/app/components/ui/tabs/index.scss
@@ -0,0 +1,11 @@
+@import 'tab/index';
+
+.tabs {
+ &__list {
+ display: flex;
+ justify-content: flex-start;
+ background-color: #f9fafa;
+ border-bottom: 1px solid $geyser;
+ padding: 0 16px;
+ }
+}
diff --git a/ui/app/components/tabs/tab/index.js b/ui/app/components/ui/tabs/tab/index.js
index fbc309e8e..fbc309e8e 100644
--- a/ui/app/components/tabs/tab/index.js
+++ b/ui/app/components/ui/tabs/tab/index.js
diff --git a/ui/app/components/tabs/tab/index.scss b/ui/app/components/ui/tabs/tab/index.scss
index 1de6ffa0e..1de6ffa0e 100644
--- a/ui/app/components/tabs/tab/index.scss
+++ b/ui/app/components/ui/tabs/tab/index.scss
diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/ui/tabs/tab/tab.component.js
index 9e590391c..9e590391c 100644
--- a/ui/app/components/tabs/tab/tab.component.js
+++ b/ui/app/components/ui/tabs/tab/tab.component.js
diff --git a/ui/app/components/tabs/tabs.component.js b/ui/app/components/ui/tabs/tabs.component.js
index d26dcff2f..d26dcff2f 100644
--- a/ui/app/components/tabs/tabs.component.js
+++ b/ui/app/components/ui/tabs/tabs.component.js
diff --git a/ui/app/components/text-field/index.js b/ui/app/components/ui/text-field/index.js
index 171caf7a4..171caf7a4 100644
--- a/ui/app/components/text-field/index.js
+++ b/ui/app/components/ui/text-field/index.js
diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js
index 2c72d8124..2c72d8124 100644
--- a/ui/app/components/text-field/text-field.component.js
+++ b/ui/app/components/ui/text-field/text-field.component.js
diff --git a/ui/app/components/ui/text-field/text-field.stories.js b/ui/app/components/ui/text-field/text-field.stories.js
new file mode 100644
index 000000000..337f78ecf
--- /dev/null
+++ b/ui/app/components/ui/text-field/text-field.stories.js
@@ -0,0 +1,53 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import TextField from '.'
+
+storiesOf('TextField', module)
+ .add('text', () =>
+ <TextField
+ label="Text"
+ type="text"
+ />
+ )
+ .add('password', () =>
+ <TextField
+ label="Password"
+ type="password"
+ />
+ )
+ .add('error', () =>
+ <TextField
+ type="text"
+ label="Name"
+ error="Invalid value"
+ />
+ )
+ .add('Mascara text', () =>
+ <TextField
+ label="Text"
+ type="text"
+ largeLabel
+ />
+ )
+ .add('Material text', () =>
+ <TextField
+ label="Text"
+ type="text"
+ material
+ />
+ )
+ .add('Material password', () =>
+ <TextField
+ label="Password"
+ type="password"
+ material
+ />
+ )
+ .add('Material error', () =>
+ <TextField
+ type="text"
+ label="Name"
+ error="Invalid value"
+ material
+ />
+ )
diff --git a/ui/app/components/token-balance/index.js b/ui/app/components/ui/token-balance/index.js
index f7da15cf8..f7da15cf8 100644
--- a/ui/app/components/token-balance/index.js
+++ b/ui/app/components/ui/token-balance/index.js
diff --git a/ui/app/components/token-balance/index.scss b/ui/app/components/ui/token-balance/index.scss
index 2ff6dfbc8..2ff6dfbc8 100644
--- a/ui/app/components/token-balance/index.scss
+++ b/ui/app/components/ui/token-balance/index.scss
diff --git a/ui/app/components/token-balance/token-balance.component.js b/ui/app/components/ui/token-balance/token-balance.component.js
index af1a32578..af1a32578 100644
--- a/ui/app/components/token-balance/token-balance.component.js
+++ b/ui/app/components/ui/token-balance/token-balance.component.js
diff --git a/ui/app/components/ui/token-balance/token-balance.container.js b/ui/app/components/ui/token-balance/token-balance.container.js
new file mode 100644
index 000000000..a0f1efc20
--- /dev/null
+++ b/ui/app/components/ui/token-balance/token-balance.container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker'
+import TokenBalance from './token-balance.component'
+import selectors from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ return {
+ userAddress: selectors.getSelectedAddress(state),
+ }
+}
+
+export default compose(
+ connect(mapStateToProps),
+ withTokenTracker
+)(TokenBalance)
diff --git a/ui/app/components/token-currency-display/index.js b/ui/app/components/ui/token-currency-display/index.js
index 6065cae1f..6065cae1f 100644
--- a/ui/app/components/token-currency-display/index.js
+++ b/ui/app/components/ui/token-currency-display/index.js
diff --git a/ui/app/components/ui/token-currency-display/token-currency-display.component.js b/ui/app/components/ui/token-currency-display/token-currency-display.component.js
new file mode 100644
index 000000000..3c2722b36
--- /dev/null
+++ b/ui/app/components/ui/token-currency-display/token-currency-display.component.js
@@ -0,0 +1,57 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../currency-display'
+import { getTokenData } from '../../../helpers/utils/transactions.util'
+import { getTokenValue, calcTokenAmount } from '../../../helpers/utils/token-util'
+
+export default class TokenCurrencyDisplay extends PureComponent {
+ static propTypes = {
+ transactionData: PropTypes.string,
+ token: PropTypes.object,
+ }
+
+ state = {
+ displayValue: '',
+ suffix: '',
+ }
+
+ 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: suffix = '' } = token
+ const tokenData = getTokenData(data)
+
+ let displayValue
+
+ if (tokenData && tokenData.params && tokenData.params.length) {
+ const tokenValue = getTokenValue(tokenData.params)
+ displayValue = calcTokenAmount(tokenValue, decimals).toString()
+ }
+
+ this.setState({ displayValue, suffix })
+ }
+
+ render () {
+ const { displayValue, suffix } = this.state
+
+ return (
+ <CurrencyDisplay
+ {...this.props}
+ displayValue={displayValue}
+ suffix={suffix}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/token-input/index.js b/ui/app/components/ui/token-input/index.js
index 22c06111e..22c06111e 100644
--- a/ui/app/components/token-input/index.js
+++ b/ui/app/components/ui/token-input/index.js
diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/ui/token-input/tests/token-input.component.test.js
index 881101880..881101880 100644
--- a/ui/app/components/token-input/tests/token-input.component.test.js
+++ b/ui/app/components/ui/token-input/tests/token-input.component.test.js
diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/ui/token-input/tests/token-input.container.test.js
index 2b1c102c8..2b1c102c8 100644
--- a/ui/app/components/token-input/tests/token-input.container.test.js
+++ b/ui/app/components/ui/token-input/tests/token-input.container.test.js
diff --git a/ui/app/components/ui/token-input/token-input.component.js b/ui/app/components/ui/token-input/token-input.component.js
new file mode 100644
index 000000000..c28af5fde
--- /dev/null
+++ b/ui/app/components/ui/token-input/token-input.component.js
@@ -0,0 +1,145 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import UnitInput from '../unit-input'
+import CurrencyDisplay from '../currency-display'
+import { getWeiHexFromDecimalValue } from '../../../helpers/utils/conversions.util'
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util'
+import { ETH } from '../../../helpers/constants/common'
+
+/**
+ * Component that allows user to enter token values as a number, and props receive a converted
+ * hex value. props.value, used as a default or forced value, should be a hex value, which
+ * gets converted into a decimal value.
+ */
+export default class TokenInput extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ currentCurrency: PropTypes.string,
+ onChange: PropTypes.func,
+ onBlur: PropTypes.func,
+ value: PropTypes.string,
+ suffix: PropTypes.string,
+ showFiat: PropTypes.bool,
+ hideConversion: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ selectedTokenExchangeRate: PropTypes.number,
+ }
+
+ constructor (props) {
+ super(props)
+
+ const { value: hexValue } = props
+ const decimalValue = hexValue ? this.getValue(props) : 0
+
+ this.state = {
+ decimalValue,
+ hexValue,
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { value: prevPropsHexValue } = prevProps
+ const { value: propsHexValue } = this.props
+ const { hexValue: stateHexValue } = this.state
+
+ if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
+ const decimalValue = this.getValue(this.props)
+ this.setState({ hexValue: propsHexValue, decimalValue })
+ }
+ }
+
+ getValue (props) {
+ const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props
+
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ toCurrency: symbol,
+ conversionRate: multiplier,
+ invertConversionRate: true,
+ })
+
+ return Number(decimalValueString) ? decimalValueString : ''
+ }
+
+ handleChange = decimalValue => {
+ const { selectedToken: { decimals } = {}, onChange } = this.props
+
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' })
+
+ this.setState({ hexValue, decimalValue })
+ onChange(hexValue)
+ }
+
+ handleBlur = () => {
+ this.props.onBlur(this.state.hexValue)
+ }
+
+ renderConversionComponent () {
+ const { selectedTokenExchangeRate, showFiat, currentCurrency, hideConversion } = this.props
+ const { decimalValue } = this.state
+ let currency, numberOfDecimals
+
+ if (hideConversion) {
+ return (
+ <div className="currency-input__conversion-component">
+ { this.context.t('noConversionRateAvailable') }
+ </div>
+ )
+ }
+
+ if (showFiat) {
+ // Display Fiat
+ currency = currentCurrency
+ numberOfDecimals = 2
+ } else {
+ // Display ETH
+ currency = ETH
+ numberOfDecimals = 6
+ }
+
+ const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0
+ const hexWeiValue = getWeiHexFromDecimalValue({
+ value: decimalEthValue,
+ fromCurrency: ETH,
+ fromDenomination: ETH,
+ })
+
+ return selectedTokenExchangeRate
+ ? (
+ <CurrencyDisplay
+ className="currency-input__conversion-component"
+ currency={currency}
+ value={hexWeiValue}
+ numberOfDecimals={numberOfDecimals}
+ />
+ ) : (
+ <div className="currency-input__conversion-component">
+ { this.context.t('noConversionRateAvailable') }
+ </div>
+ )
+ }
+
+ render () {
+ const { suffix, ...restProps } = this.props
+ const { decimalValue } = this.state
+
+ return (
+ <UnitInput
+ {...restProps}
+ suffix={suffix}
+ onChange={this.handleChange}
+ onBlur={this.handleBlur}
+ value={decimalValue}
+ >
+ { this.renderConversionComponent() }
+ </UnitInput>
+ )
+ }
+}
diff --git a/ui/app/components/ui/token-input/token-input.container.js b/ui/app/components/ui/token-input/token-input.container.js
new file mode 100644
index 000000000..981cb3598
--- /dev/null
+++ b/ui/app/components/ui/token-input/token-input.container.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux'
+import TokenInput from './token-input.component'
+import {getIsMainnet, getSelectedToken, getSelectedTokenExchangeRate, preferencesSelector} from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { metamask: { currentCurrency } } = state
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+
+ return {
+ currentCurrency,
+ selectedToken: getSelectedToken(state),
+ selectedTokenExchangeRate: getSelectedTokenExchangeRate(state),
+ hideConversion: (!isMainnet && !showFiatInTestnets),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { selectedToken } = stateProps
+ const suffix = selectedToken && selectedToken.symbol
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ suffix,
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(TokenInput)
diff --git a/ui/app/components/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js
index b54026794..b54026794 100644
--- a/ui/app/components/tooltip-v2.js
+++ b/ui/app/components/ui/tooltip-v2.js
diff --git a/ui/app/components/tooltip.js b/ui/app/components/ui/tooltip.js
index efab2c497..efab2c497 100644
--- a/ui/app/components/tooltip.js
+++ b/ui/app/components/ui/tooltip.js
diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/ui/unit-input/index.js
index 7c33c9e5c..7c33c9e5c 100644
--- a/ui/app/components/unit-input/index.js
+++ b/ui/app/components/ui/unit-input/index.js
diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss
index e4075d225..e4075d225 100644
--- a/ui/app/components/unit-input/index.scss
+++ b/ui/app/components/ui/unit-input/index.scss
diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/ui/unit-input/tests/unit-input.component.test.js
index 97d987bc7..97d987bc7 100644
--- a/ui/app/components/unit-input/tests/unit-input.component.test.js
+++ b/ui/app/components/ui/unit-input/tests/unit-input.component.test.js
diff --git a/ui/app/components/ui/unit-input/unit-input.component.js b/ui/app/components/ui/unit-input/unit-input.component.js
new file mode 100644
index 000000000..7b414f177
--- /dev/null
+++ b/ui/app/components/ui/unit-input/unit-input.component.js
@@ -0,0 +1,108 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { removeLeadingZeroes } from '../../app/send/send.utils'
+
+/**
+ * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also
+ * allows rendering a child component underneath the input to, for example, display conversions of
+ * the shown suffix.
+ */
+export default class UnitInput extends PureComponent {
+ static propTypes = {
+ children: PropTypes.node,
+ actionComponent: PropTypes.node,
+ error: PropTypes.bool,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ suffix: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }
+
+ static defaultProps = {
+ placeholder: '0',
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ value: props.value || '',
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { value: prevPropsValue } = prevProps
+ const { value: propsValue } = this.props
+ const { value: stateValue } = this.state
+
+ if (prevPropsValue !== propsValue && propsValue !== stateValue) {
+ this.setState({ value: propsValue })
+ }
+ }
+
+ handleFocus = () => {
+ this.unitInput.focus()
+ }
+
+ handleChange = event => {
+ const { value: userInput } = event.target
+ let value = userInput
+
+ if (userInput.length && userInput.length > 1) {
+ value = removeLeadingZeroes(userInput)
+ }
+
+ this.setState({ value })
+ this.props.onChange(value)
+ }
+
+ handleBlur = event => {
+ const { onBlur } = this.props
+ typeof onBlur === 'function' && onBlur(this.state.value)
+ }
+
+ getInputWidth (value) {
+ const valueString = String(value)
+ const valueLength = valueString.length || 1
+ const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
+ return (valueLength + decimalPointDeficit + 0.5) + 'ch'
+ }
+
+ render () {
+ const { error, placeholder, suffix, actionComponent, children } = this.props
+ const { value } = this.state
+
+ return (
+ <div
+ className={classnames('unit-input', { 'unit-input--error': error })}
+ onClick={this.handleFocus}
+ >
+ <div className="unit-input__inputs">
+ <div className="unit-input__input-container">
+ <input
+ type="number"
+ className="unit-input__input"
+ value={value}
+ placeholder={placeholder}
+ onChange={this.handleChange}
+ onBlur={this.handleBlur}
+ style={{ width: this.getInputWidth(value) }}
+ ref={ref => { this.unitInput = ref }}
+ />
+ {
+ suffix && (
+ <div className="unit-input__suffix">
+ { suffix }
+ </div>
+ )
+ }
+ </div>
+ { children }
+ </div>
+ {actionComponent}
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/unit-input/unit-input.component.js
deleted file mode 100644
index 230eecfe6..000000000
--- a/ui/app/components/unit-input/unit-input.component.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { removeLeadingZeroes } from '../send/send.utils'
-
-/**
- * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also
- * allows rendering a child component underneath the input to, for example, display conversions of
- * the shown suffix.
- */
-export default class UnitInput extends PureComponent {
- static propTypes = {
- children: PropTypes.node,
- actionComponent: PropTypes.node,
- error: PropTypes.bool,
- onBlur: PropTypes.func,
- onChange: PropTypes.func,
- placeholder: PropTypes.string,
- suffix: PropTypes.string,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- }
-
- static defaultProps = {
- placeholder: '0',
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- value: props.value || '',
- }
- }
-
- componentDidUpdate (prevProps) {
- const { value: prevPropsValue } = prevProps
- const { value: propsValue } = this.props
- const { value: stateValue } = this.state
-
- if (prevPropsValue !== propsValue && propsValue !== stateValue) {
- this.setState({ value: propsValue })
- }
- }
-
- handleFocus = () => {
- this.unitInput.focus()
- }
-
- handleChange = event => {
- const { value: userInput } = event.target
- let value = userInput
-
- if (userInput.length && userInput.length > 1) {
- value = removeLeadingZeroes(userInput)
- }
-
- this.setState({ value })
- this.props.onChange(value)
- }
-
- handleBlur = event => {
- const { onBlur } = this.props
- typeof onBlur === 'function' && onBlur(this.state.value)
- }
-
- getInputWidth (value) {
- const valueString = String(value)
- const valueLength = valueString.length || 1
- const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
- return (valueLength + decimalPointDeficit + 0.5) + 'ch'
- }
-
- render () {
- const { error, placeholder, suffix, actionComponent, children } = this.props
- const { value } = this.state
-
- return (
- <div
- className={classnames('unit-input', { 'unit-input--error': error })}
- onClick={this.handleFocus}
- >
- <div className="unit-input__inputs">
- <div className="unit-input__input-container">
- <input
- type="number"
- className="unit-input__input"
- value={value}
- placeholder={placeholder}
- onChange={this.handleChange}
- onBlur={this.handleBlur}
- style={{ width: this.getInputWidth(value) }}
- ref={ref => { this.unitInput = ref }}
- />
- {
- suffix && (
- <div className="unit-input__suffix">
- { suffix }
- </div>
- )
- }
- </div>
- { children }
- </div>
- {actionComponent}
- </div>
- )
- }
-}
diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js
deleted file mode 100644
index ead584c26..000000000
--- a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component'
-import CurrencyDisplay from '../../currency-display'
-
-describe('UserPreferencedCurrencyDisplay Component', () => {
- describe('rendering', () => {
- it('should render properly', () => {
- const wrapper = shallow(
- <UserPreferencedCurrencyDisplay />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(CurrencyDisplay).length, 1)
- })
-
- it('should pass all props to the CurrencyDisplay child component', () => {
- const wrapper = shallow(
- <UserPreferencedCurrencyDisplay
- prop1={true}
- prop2="test"
- prop3={1}
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(CurrencyDisplay).length, 1)
- assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true)
- assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test')
- assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1)
- })
- })
-})
diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js
deleted file mode 100644
index d9f29327d..000000000
--- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { PRIMARY, SECONDARY, ETH } from '../../constants/common'
-import CurrencyDisplay from '../currency-display'
-
-export default class UserPreferencedCurrencyDisplay extends PureComponent {
- static propTypes = {
- className: PropTypes.string,
- prefix: PropTypes.string,
- value: PropTypes.string,
- numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- hideLabel: PropTypes.bool,
- hideTitle: PropTypes.bool,
- style: PropTypes.object,
- showEthLogo: PropTypes.bool,
- ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- // Used in container
- type: PropTypes.oneOf([PRIMARY, SECONDARY]),
- ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- ethPrefix: PropTypes.string,
- fiatPrefix: PropTypes.string,
- // From container
- currency: PropTypes.string,
- nativeCurrency: PropTypes.string,
- }
-
- renderEthLogo () {
- const { currency, showEthLogo, ethLogoHeight = 12 } = this.props
-
- return currency === ETH && showEthLogo && (
- <img
- src="/images/eth.svg"
- height={ethLogoHeight}
- />
- )
- }
-
- render () {
- return (
- <CurrencyDisplay
- {...this.props}
- prefixComponent={this.renderEthLogo()}
- />
- )
- }
-}
diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js
deleted file mode 100644
index 3c5bd0f21..000000000
--- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { connect } from 'react-redux'
-import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'
-import { preferencesSelector, getIsMainnet } from '../../selectors'
-import { ETH, PRIMARY, SECONDARY } from '../../constants/common'
-
-const mapStateToProps = (state, ownProps) => {
- const {
- useNativeCurrencyAsPrimaryCurrency,
- showFiatInTestnets,
- } = preferencesSelector(state)
-
- const isMainnet = getIsMainnet(state)
-
- return {
- useNativeCurrencyAsPrimaryCurrency,
- showFiatInTestnets,
- isMainnet,
- nativeCurrency: state.metamask.nativeCurrency,
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, isMainnet, nativeCurrency, ...restStateProps } = stateProps
- const {
- type,
- numberOfDecimals: propsNumberOfDecimals,
- ethNumberOfDecimals,
- fiatNumberOfDecimals,
- ethPrefix,
- fiatPrefix,
- prefix: propsPrefix,
- ...restOwnProps
- } = ownProps
-
- let currency, numberOfDecimals, prefix
-
- if (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency ||
- type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency) {
- // Display ETH
- currency = nativeCurrency || ETH
- numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
- prefix = propsPrefix || ethPrefix
- } else if (type === SECONDARY && useNativeCurrencyAsPrimaryCurrency ||
- type === PRIMARY && !useNativeCurrencyAsPrimaryCurrency) {
- // Display Fiat
- numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2
- prefix = propsPrefix || fiatPrefix
- }
-
- if (!isMainnet && !showFiatInTestnets) {
- currency = nativeCurrency || ETH
- numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
- prefix = propsPrefix || ethPrefix
- }
-
- return {
- ...restStateProps,
- ...dispatchProps,
- ...restOwnProps,
- nativeCurrency,
- currency,
- numberOfDecimals,
- prefix,
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay)
diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js
deleted file mode 100644
index 710b5d519..000000000
--- a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component'
-import CurrencyInput from '../../currency-input'
-
-describe('UserPreferencedCurrencyInput Component', () => {
- describe('rendering', () => {
- it('should render properly', () => {
- const wrapper = shallow(
- <UserPreferencedCurrencyInput />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(CurrencyInput).length, 1)
- })
-
- it('should render useFiat for CurrencyInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => {
- const wrapper = shallow(
- <UserPreferencedCurrencyInput
- useNativeCurrencyAsPrimaryCurrency
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(CurrencyInput).length, 1)
- assert.equal(wrapper.find(CurrencyInput).props().useFiat, false)
- wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false })
- assert.equal(wrapper.find(CurrencyInput).props().useFiat, true)
- })
- })
-})
diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js
deleted file mode 100644
index 463e66b80..000000000
--- a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import CurrencyInput from '../currency-input'
-
-export default class UserPreferencedCurrencyInput extends PureComponent {
- static propTypes = {
- useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
- }
-
- render () {
- const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props
-
- return (
- <CurrencyInput
- {...restProps}
- useFiat={!useNativeCurrencyAsPrimaryCurrency}
- />
- )
- }
-}
diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js
deleted file mode 100644
index 0b88eb5a7..000000000
--- a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { connect } from 'react-redux'
-import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'
-import { preferencesSelector } from '../../selectors'
-
-const mapStateToProps = state => {
- const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
-
- return {
- useNativeCurrencyAsPrimaryCurrency,
- }
-}
-
-export default connect(mapStateToProps)(UserPreferencedCurrencyInput)
diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js
deleted file mode 100644
index d85bddeeb..000000000
--- a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import UserPreferencedTokenInput from '../user-preferenced-token-input.component'
-import TokenInput from '../../token-input'
-
-describe('UserPreferencedCurrencyInput Component', () => {
- describe('rendering', () => {
- it('should render properly', () => {
- const wrapper = shallow(
- <UserPreferencedTokenInput />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(TokenInput).length, 1)
- })
-
- it('should render showFiat for TokenInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => {
- const wrapper = shallow(
- <UserPreferencedTokenInput
- useNativeCurrencyAsPrimaryCurrency
- />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(TokenInput).length, 1)
- assert.equal(wrapper.find(TokenInput).props().showFiat, false)
- wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false })
- assert.equal(wrapper.find(TokenInput).props().showFiat, true)
- })
- })
-})
diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js
deleted file mode 100644
index 8f14231ac..000000000
--- a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import TokenInput from '../token-input'
-
-export default class UserPreferencedTokenInput extends PureComponent {
- static propTypes = {
- useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
- }
-
- render () {
- const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props
-
- return (
- <TokenInput
- {...restProps}
- showFiat={!useNativeCurrencyAsPrimaryCurrency}
- />
- )
- }
-}
diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js
deleted file mode 100644
index 3305d4e29..000000000
--- a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { connect } from 'react-redux'
-import UserPreferencedTokenInput from './user-preferenced-token-input.component'
-import { preferencesSelector } from '../../selectors'
-
-const mapStateToProps = state => {
- const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
-
- return {
- useNativeCurrencyAsPrimaryCurrency,
- }
-}
-
-export default connect(mapStateToProps)(UserPreferencedTokenInput)
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
deleted file mode 100644
index db572e5cb..000000000
--- a/ui/app/components/wallet-view.js
+++ /dev/null
@@ -1,246 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const inherits = require('util').inherits
-const classnames = require('classnames')
-const { checksumAddress } = require('../util')
-import Identicon from './identicon'
-// const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns
-const Tooltip = require('./tooltip-v2.js').default
-const copyToClipboard = require('copy-to-clipboard')
-const actions = require('../actions')
-import BalanceComponent from './balance'
-const TokenList = require('./token-list')
-const selectors = require('../selectors')
-const { ADD_TOKEN_ROUTE } = require('../routes')
-
-import AddTokenButton from './add-token-button'
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(WalletView)
-
-WalletView.contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
-}
-
-WalletView.defaultProps = {
- responsiveDisplayClassname: '',
-}
-
-function mapStateToProps (state) {
-
- return {
- network: state.metamask.network,
- sidebarOpen: state.appState.sidebar.isOpen,
- identities: state.metamask.identities,
- accounts: selectors.getMetaMaskAccounts(state),
- keyrings: state.metamask.keyrings,
- selectedAddress: selectors.getSelectedAddress(state),
- selectedAccount: selectors.getSelectedAccount(state),
- selectedTokenAddress: state.metamask.selectedTokenAddress,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showSendPage: () => dispatch(actions.showSendPage()),
- hideSidebar: () => dispatch(actions.hideSidebar()),
- unsetSelectedToken: () => dispatch(actions.setSelectedToken()),
- showAccountDetailModal: () => {
- dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
- },
- showAddTokenPage: () => dispatch(actions.showAddTokenPage()),
- }
-}
-
-inherits(WalletView, Component)
-function WalletView () {
- Component.call(this)
- this.state = {
- hasCopied: false,
- copyToClipboardPressed: false,
- }
-}
-
-WalletView.prototype.renderWalletBalance = function () {
- const {
- selectedTokenAddress,
- selectedAccount,
- unsetSelectedToken,
- hideSidebar,
- sidebarOpen,
- } = this.props
-
- const selectedClass = selectedTokenAddress
- ? ''
- : 'wallet-balance-wrapper--active'
- const className = `flex-column wallet-balance-wrapper ${selectedClass}`
-
- return h('div', { className }, [
- h('div.wallet-balance',
- {
- onClick: () => {
- unsetSelectedToken()
- selectedTokenAddress && sidebarOpen && hideSidebar()
- },
- },
- [
- h(BalanceComponent, {
- balanceValue: selectedAccount ? selectedAccount.balance : '',
- style: {},
- }),
- ]
- ),
- ])
-}
-
-WalletView.prototype.renderAddToken = function () {
- const {
- sidebarOpen,
- hideSidebar,
- history,
- } = this.props
- const { metricsEvent } = this.context
-
- return h(AddTokenButton, {
- onClick () {
- history.push(ADD_TOKEN_ROUTE)
- metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Token Menu',
- name: 'Clicked "Add Token"',
- },
- })
- if (sidebarOpen) {
- hideSidebar()
- }
- },
- })
-}
-
-WalletView.prototype.render = function () {
- const {
- responsiveDisplayClassname,
- selectedAddress,
- keyrings,
- showAccountDetailModal,
- hideSidebar,
- identities,
- network,
- } = this.props
- // temporary logs + fake extra wallets
-
- const checksummedAddress = checksumAddress(selectedAddress, network)
-
- if (!selectedAddress) {
- throw new Error('selectedAddress should not be ' + String(selectedAddress))
- }
-
- const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(selectedAddress)
- })
-
- let label = ''
- let type
- if (keyring) {
- type = keyring.type
- if (type !== 'HD Key Tree') {
- if (type.toLowerCase().search('hardware') !== -1) {
- label = this.context.t('hardware')
- } else {
- label = this.context.t('imported')
- }
- }
- }
-
- return h('div.wallet-view.flex-column', {
- style: {},
- className: responsiveDisplayClassname,
- }, [
-
- // TODO: Separate component: wallet account details
- h('div.flex-column.wallet-view-account-details', {
- style: {},
- }, [
- h('div.wallet-view__sidebar-close', {
- onClick: hideSidebar,
- }),
-
- h('div.wallet-view__keyring-label.allcaps', label),
-
- h('div.flex-column.flex-center.wallet-view__name-container', {
- style: { margin: '0 auto' },
- onClick: showAccountDetailModal,
- }, [
- h(Identicon, {
- diameter: 54,
- address: checksummedAddress,
- }),
-
- h('span.account-name', {
- style: {},
- }, [
- identities[selectedAddress].name,
- ]),
-
- h('button.btn-clear.wallet-view__details-button.allcaps', this.context.t('details')),
- ]),
- ]),
-
- h(Tooltip, {
- position: 'bottom',
- title: this.state.hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard'),
- wrapperClassName: 'wallet-view__tooltip',
- }, [
- h('button.wallet-view__address', {
- className: classnames({
- 'wallet-view__address__pressed': this.state.copyToClipboardPressed,
- }),
- onClick: () => {
- copyToClipboard(checksummedAddress)
- this.context.metricsEvent({
- eventOpts: {
- category: 'Navigation',
- action: 'Home',
- name: 'Copied Address',
- },
- })
- this.setState({ hasCopied: true })
- setTimeout(() => this.setState({ hasCopied: false }), 3000)
- },
- onMouseDown: () => {
- this.setState({ copyToClipboardPressed: true })
- },
- onMouseUp: () => {
- this.setState({ copyToClipboardPressed: false })
- },
- }, [
- `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`,
- h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }),
- ]),
- ]),
-
- this.renderWalletBalance(),
-
- h(TokenList),
-
- this.renderAddToken(),
- ])
-}
-
-// TODO: Extra wallets, for dev testing. Remove when PRing to master.
-// const extraWallet = h('div.flex-column.wallet-balance-wrapper', {}, [
-// h('div.wallet-balance', {}, [
-// h(BalanceComponent, {
-// balanceValue: selectedAccount.balance,
-// style: {},
-// }),
-// ]),
-// ])
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
deleted file mode 100644
index 34f5466e2..000000000
--- a/ui/app/conf-tx.js
+++ /dev/null
@@ -1,225 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const actions = require('./actions')
-const txHelper = require('../lib/tx-helper')
-const log = require('loglevel')
-const R = require('ramda')
-
-const SignatureRequest = require('./components/signature-request')
-const Loading = require('./components/loading-screen')
-const { DEFAULT_ROUTE } = require('./routes')
-const { getMetaMaskAccounts } = require('./selectors')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(ConfirmTxScreen)
-
-function mapStateToProps (state) {
- const { metamask } = state
- const {
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = metamask
-
- return {
- identities: state.metamask.identities,
- accounts: getMetaMaskAccounts(state),
- selectedAddress: state.metamask.selectedAddress,
- unapprovedTxs: state.metamask.unapprovedTxs,
- unapprovedMsgs: state.metamask.unapprovedMsgs,
- unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
- unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
- index: state.appState.currentView.context,
- warning: state.appState.warning,
- network: state.metamask.network,
- provider: state.metamask.provider,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
- blockGasLimit: state.metamask.currentBlockGasLimit,
- computedBalances: state.metamask.computedBalances,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- send: state.metamask.send,
- selectedAddressTxList: state.metamask.selectedAddressTxList,
- }
-}
-
-inherits(ConfirmTxScreen, Component)
-function ConfirmTxScreen () {
- Component.call(this)
-}
-
-ConfirmTxScreen.prototype.getUnapprovedMessagesTotal = function () {
- const {
- unapprovedMsgCount = 0,
- unapprovedPersonalMsgCount = 0,
- unapprovedTypedMessagesCount = 0,
- } = this.props
-
- return unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount
-}
-
-ConfirmTxScreen.prototype.componentDidMount = function () {
- const {
- unapprovedTxs = {},
- network,
- send,
- } = this.props
- const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
-
- if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
- this.props.history.push(DEFAULT_ROUTE)
- }
-}
-
-ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
- const {
- unapprovedTxs = {},
- network,
- selectedAddressTxList,
- send,
- history,
- match: { params: { id: transactionId } = {} },
- } = this.props
-
- let prevTx
-
- if (transactionId) {
- prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList)
- } else {
- const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
- const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network)
- const prevTxData = prevUnconfTxList[prevIndex] || {}
- prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {}
- }
-
- const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
-
- if (prevTx && prevTx.status === 'dropped') {
- this.props.dispatch(actions.showModal({
- name: 'TRANSACTION_CONFIRMED',
- onSubmit: () => history.push(DEFAULT_ROUTE),
- }))
-
- return
- }
-
- if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
- this.props.history.push(DEFAULT_ROUTE)
- }
-}
-
-ConfirmTxScreen.prototype.getTxData = function () {
- const {
- network,
- index,
- unapprovedTxs,
- unapprovedMsgs,
- unapprovedPersonalMsgs,
- unapprovedTypedMessages,
- match: { params: { id: transactionId } = {} },
- } = this.props
-
- const unconfTxList = txHelper(
- unapprovedTxs,
- unapprovedMsgs,
- unapprovedPersonalMsgs,
- unapprovedTypedMessages,
- network
- )
-
- log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`)
-
- return transactionId
- ? R.find(({ id }) => id + '' === transactionId)(unconfTxList)
- : unconfTxList[index]
-}
-
-ConfirmTxScreen.prototype.render = function () {
- const props = this.props
- const {
- currentCurrency,
- conversionRate,
- blockGasLimit,
- } = props
-
- var txData = this.getTxData() || {}
- const { msgParams } = txData
- log.debug('msgParams detected, rendering pending msg')
-
- return msgParams
- ? h(SignatureRequest, {
- // Properties
- txData: txData,
- key: txData.id,
- selectedAddress: props.selectedAddress,
- accounts: props.accounts,
- identities: props.identities,
- conversionRate,
- currentCurrency,
- blockGasLimit,
- // Actions
- signMessage: this.signMessage.bind(this, txData),
- signPersonalMessage: this.signPersonalMessage.bind(this, txData),
- signTypedMessage: this.signTypedMessage.bind(this, txData),
- cancelMessage: this.cancelMessage.bind(this, txData),
- cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
- cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
- })
- : h(Loading)
-}
-
-ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
- log.info('conf-tx.js: signing message')
- var params = msgData.msgParams
- params.metamaskId = msgData.id
- this.stopPropagation(event)
- return this.props.dispatch(actions.signMsg(params))
-}
-
-ConfirmTxScreen.prototype.stopPropagation = function (event) {
- if (event.stopPropagation) {
- event.stopPropagation()
- }
-}
-
-ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
- log.info('conf-tx.js: signing personal message')
- var params = msgData.msgParams
- params.metamaskId = msgData.id
- this.stopPropagation(event)
- return this.props.dispatch(actions.signPersonalMsg(params))
-}
-
-ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
- log.info('conf-tx.js: signing typed message')
- var params = msgData.msgParams
- params.metamaskId = msgData.id
- this.stopPropagation(event)
- return this.props.dispatch(actions.signTypedMsg(params))
-}
-
-ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
- log.info('canceling message')
- this.stopPropagation(event)
- return this.props.dispatch(actions.cancelMsg(msgData))
-}
-
-ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
- log.info('canceling personal message')
- this.stopPropagation(event)
- return this.props.dispatch(actions.cancelPersonalMsg(msgData))
-}
-
-ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
- log.info('canceling typed message')
- this.stopPropagation(event)
- return this.props.dispatch(actions.cancelTypedMsg(msgData))
-}
diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js
deleted file mode 100644
index c6e566b8b..000000000
--- a/ui/app/constants/common.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const ETH = 'ETH'
-export const GWEI = 'GWEI'
-export const WEI = 'WEI'
-
-export const PRIMARY = 'PRIMARY'
-export const SECONDARY = 'SECONDARY'
-
-export const NETWORK_TYPES = {
- MAINNET: 'mainnet',
-}
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 7eaf60ce8..f2f37bfa3 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -52,4 +52,4 @@
@import './tooltip.scss';
-@import '../../../components/index';
+@import '../../../components/app/index';
diff --git a/ui/app/css/itcss/components/tab-bar.scss b/ui/app/css/itcss/components/tab-bar.scss
index 4f3077974..bb9f8f261 100644
--- a/ui/app/css/itcss/components/tab-bar.scss
+++ b/ui/app/css/itcss/components/tab-bar.scss
@@ -1,21 +1,73 @@
.tab-bar {
display: flex;
- flex-direction: row;
+ flex-direction: column;
justify-content: flex-start;
- align-items: flex-end;
}
.tab-bar__tab {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-start;
min-width: 0;
flex: 0 0 auto;
- padding: 15px 25px;
- border-bottom: 1px solid $alto;
box-sizing: border-box;
- font-size: 18px;
-}
+ font-size: 16px;
+ padding: 16px 24px;
+ opacity: .5;
+ transition: opacity 200ms ease-in-out;
+
+ @media screen and (min-width: 576px) {
+ &:hover {
+ opacity: .4;
+ }
+
+ &:active {
+ opacity: .6;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ font-size: 18px;
+ padding: 24px;
+ border-bottom: 1px solid $alto;
+ opacity: 1;
+ }
+
+ &__content {
+ flex: 1 1 auto;
+ width: 0;
+
+ &__description {
+ display: none;
+
+ @media screen and (max-width: 575px) {
+ display: block;
+ font-size: 14px;
+ font-weight: 300;
+ margin-top: 8px;
+ min-height: 14px;
+ }
+ }
+ }
+
+ &__caret {
+ display: none;
+
+ @media screen and (max-width: 575px) {
+ display: block;
+ background-image: url('/images/caret-right.svg');
+ width: 36px;
+ height: 36px;
+ opacity: .5;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+ }
-.tab-bar__tab--active {
- border-color: $black;
+ &--active {
+ opacity: 1 !important;
+ }
}
.tab-bar__grow-tab {
diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js
new file mode 100644
index 000000000..acbb5c989
--- /dev/null
+++ b/ui/app/ducks/app/app.js
@@ -0,0 +1,788 @@
+const extend = require('xtend')
+const actions = require('../../store/actions')
+const txHelper = require('../../../lib/tx-helper')
+const log = require('loglevel')
+
+module.exports = reduceApp
+
+
+function reduceApp (state, action) {
+ log.debug('App Reducer got ' + action.type)
+ // clone and defaults
+ const selectedAddress = state.metamask.selectedAddress
+ const hasUnconfActions = checkUnconfActions(state)
+ let name = 'accounts'
+ if (selectedAddress) {
+ name = 'accountDetail'
+ }
+
+ if (hasUnconfActions) {
+ log.debug('pending txs detected, defaulting to conf-tx view.')
+ name = 'confTx'
+ }
+
+ var defaultView = {
+ name,
+ detailView: null,
+ context: selectedAddress,
+ }
+
+ // confirm seed words
+ var seedWords = state.metamask.seedWords
+ var seedConfView = {
+ name: 'createVaultComplete',
+ seedWords,
+ }
+
+ // default state
+ var appState = extend({
+ shouldClose: false,
+ menuOpen: false,
+ modal: {
+ open: false,
+ modalState: {
+ name: null,
+ props: {},
+ },
+ previousModalState: {
+ name: null,
+ },
+ },
+ sidebar: {
+ isOpen: false,
+ transitionName: '',
+ type: '',
+ props: {},
+ },
+ alertOpen: false,
+ alertMessage: null,
+ qrCodeData: null,
+ networkDropdownOpen: false,
+ currentView: seedWords ? seedConfView : defaultView,
+ accountDetail: {
+ subview: 'transactions',
+ },
+ // Used to render transition direction
+ transForward: true,
+ // Used to display loading indicator
+ isLoading: false,
+ // Used to display error text
+ warning: null,
+ buyView: {},
+ isMouseUser: false,
+ gasIsLoading: false,
+ networkNonce: null,
+ defaultHdPaths: {
+ trezor: `m/44'/60'/0'/0`,
+ ledger: `m/44'/60'/0'/0/0`,
+ },
+ lastSelectedProvider: null,
+ }, state.appState)
+
+ switch (action.type) {
+ // dropdown methods
+ case actions.NETWORK_DROPDOWN_OPEN:
+ return extend(appState, {
+ networkDropdownOpen: true,
+ })
+
+ case actions.NETWORK_DROPDOWN_CLOSE:
+ return extend(appState, {
+ networkDropdownOpen: false,
+ })
+
+ // sidebar methods
+ case actions.SIDEBAR_OPEN:
+ return extend(appState, {
+ sidebar: {
+ ...action.value,
+ isOpen: true,
+ },
+ })
+
+ case actions.SIDEBAR_CLOSE:
+ return extend(appState, {
+ sidebar: {
+ ...appState.sidebar,
+ isOpen: false,
+ },
+ })
+
+ // alert methods
+ case actions.ALERT_OPEN:
+ return extend(appState, {
+ alertOpen: true,
+ alertMessage: action.value,
+ })
+
+ case actions.ALERT_CLOSE:
+ return extend(appState, {
+ alertOpen: false,
+ alertMessage: null,
+ })
+
+ // qr scanner methods
+ case actions.QR_CODE_DETECTED:
+ return extend(appState, {
+ qrCodeData: action.value,
+ })
+
+
+ // modal methods:
+ case actions.MODAL_OPEN:
+ const { name, ...modalProps } = action.payload
+
+ return extend(appState, {
+ modal: {
+ open: true,
+ modalState: {
+ name: name,
+ props: { ...modalProps },
+ },
+ previousModalState: { ...appState.modal.modalState },
+ },
+ })
+
+ case actions.MODAL_CLOSE:
+ return extend(appState, {
+ modal: Object.assign(
+ state.appState.modal,
+ { open: false },
+ { modalState: { name: null, props: {} } },
+ { previousModalState: appState.modal.modalState},
+ ),
+ })
+
+ // transition methods
+ case actions.TRANSITION_FORWARD:
+ return extend(appState, {
+ transForward: true,
+ })
+
+ case actions.TRANSITION_BACKWARD:
+ return extend(appState, {
+ transForward: false,
+ })
+
+ // intialize
+
+ case actions.SHOW_CREATE_VAULT:
+ return extend(appState, {
+ currentView: {
+ name: 'createVault',
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SHOW_RESTORE_VAULT:
+ return extend(appState, {
+ currentView: {
+ name: 'restoreVault',
+ },
+ transForward: true,
+ forgottenPassword: true,
+ })
+
+ case actions.FORGOT_PASSWORD:
+ const newState = extend(appState, {
+ forgottenPassword: action.value,
+ })
+
+ if (action.value) {
+ newState.currentView = {
+ name: 'restoreVault',
+ }
+ }
+
+ return newState
+
+ case actions.SHOW_INIT_MENU:
+ return extend(appState, {
+ currentView: defaultView,
+ transForward: false,
+ })
+
+ case actions.SHOW_CONFIG_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'config',
+ context: appState.currentView.context,
+ },
+ transForward: action.value,
+ })
+
+ case actions.SHOW_ADD_TOKEN_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'add-token',
+ context: appState.currentView.context,
+ },
+ 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: {
+ name: 'import-menu',
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SHOW_NEW_ACCOUNT_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'new-account-page',
+ context: action.formToSelect,
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SET_NEW_ACCOUNT_FORM:
+ return extend(appState, {
+ currentView: {
+ name: appState.currentView.name,
+ context: action.formToSelect,
+ },
+ })
+
+ case actions.SHOW_INFO_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'info',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ })
+
+ case actions.CREATE_NEW_VAULT_IN_PROGRESS:
+ return extend(appState, {
+ currentView: {
+ name: 'createVault',
+ inProgress: true,
+ },
+ transForward: true,
+ isLoading: true,
+ })
+
+ case actions.SHOW_NEW_VAULT_SEED:
+ return extend(appState, {
+ currentView: {
+ name: 'createVaultComplete',
+ seedWords: action.value,
+ },
+ transForward: true,
+ isLoading: false,
+ })
+
+ case actions.NEW_ACCOUNT_SCREEN:
+ return extend(appState, {
+ currentView: {
+ name: 'new-account',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ })
+
+ case actions.SHOW_SEND_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'sendTransaction',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SHOW_SEND_TOKEN_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'sendToken',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SHOW_NEW_KEYCHAIN:
+ return extend(appState, {
+ currentView: {
+ name: 'newKeychain',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ })
+
+ // unlock
+
+ case actions.UNLOCK_METAMASK:
+ return extend(appState, {
+ forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
+ detailView: {},
+ transForward: true,
+ isLoading: false,
+ warning: null,
+ })
+
+ case actions.LOCK_METAMASK:
+ return extend(appState, {
+ currentView: defaultView,
+ transForward: false,
+ warning: null,
+ })
+
+ case actions.BACK_TO_INIT_MENU:
+ return extend(appState, {
+ warning: null,
+ transForward: false,
+ forgottenPassword: true,
+ currentView: {
+ name: 'InitMenu',
+ },
+ })
+
+ case actions.BACK_TO_UNLOCK_VIEW:
+ return extend(appState, {
+ warning: null,
+ transForward: true,
+ forgottenPassword: false,
+ currentView: {
+ name: 'UnlockScreen',
+ },
+ })
+ // reveal seed words
+
+ case actions.REVEAL_SEED_CONFIRMATION:
+ return extend(appState, {
+ currentView: {
+ name: 'reveal-seed-conf',
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ // accounts
+
+ case actions.SET_SELECTED_ACCOUNT:
+ return extend(appState, {
+ activeAddress: action.value,
+ })
+
+ case actions.GO_HOME:
+ return extend(appState, {
+ currentView: extend(appState.currentView, {
+ name: 'accountDetail',
+ }),
+ accountDetail: {
+ subview: 'transactions',
+ accountExport: 'none',
+ privateKey: '',
+ },
+ transForward: false,
+ warning: null,
+ })
+
+ case actions.SHOW_ACCOUNT_DETAIL:
+ return extend(appState, {
+ forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
+ currentView: {
+ name: 'accountDetail',
+ context: action.value,
+ },
+ accountDetail: {
+ subview: 'transactions',
+ accountExport: 'none',
+ privateKey: '',
+ },
+ transForward: false,
+ })
+
+ case actions.BACK_TO_ACCOUNT_DETAIL:
+ return extend(appState, {
+ currentView: {
+ name: 'accountDetail',
+ context: action.value,
+ },
+ accountDetail: {
+ subview: 'transactions',
+ accountExport: 'none',
+ privateKey: '',
+ },
+ transForward: false,
+ })
+
+ case actions.SHOW_ACCOUNTS_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: seedWords ? 'createVaultComplete' : 'accounts',
+ seedWords,
+ },
+ transForward: true,
+ isLoading: false,
+ warning: null,
+ scrollToBottom: false,
+ forgottenPassword: false,
+ })
+
+ case actions.SHOW_NOTICE:
+ return extend(appState, {
+ transForward: true,
+ isLoading: false,
+ })
+
+ case actions.REVEAL_ACCOUNT:
+ return extend(appState, {
+ scrollToBottom: true,
+ })
+
+ case actions.SHOW_CONF_TX_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'confTx',
+ context: action.id ? indexForPending(state, action.id) : 0,
+ },
+ transForward: action.transForward,
+ warning: null,
+ isLoading: false,
+ })
+
+ case actions.SHOW_CONF_MSG_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: hasUnconfActions ? 'confTx' : 'account-detail',
+ context: 0,
+ },
+ transForward: true,
+ warning: null,
+ isLoading: false,
+ })
+
+ case actions.COMPLETED_TX:
+ log.debug('reducing COMPLETED_TX for tx ' + action.value)
+ const otherUnconfActions = getUnconfActionList(state)
+ .filter(tx => tx.id !== action.value)
+ const hasOtherUnconfActions = otherUnconfActions.length > 0
+
+ if (hasOtherUnconfActions) {
+ log.debug('reducer detected txs - rendering confTx view')
+ return extend(appState, {
+ transForward: false,
+ currentView: {
+ name: 'confTx',
+ context: 0,
+ },
+ warning: null,
+ })
+ } else {
+ log.debug('attempting to close popup')
+ return extend(appState, {
+ // indicate notification should close
+ shouldClose: true,
+ transForward: false,
+ warning: null,
+ currentView: {
+ name: 'accountDetail',
+ context: state.metamask.selectedAddress,
+ },
+ accountDetail: {
+ subview: 'transactions',
+ },
+ })
+ }
+
+ case actions.NEXT_TX:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'confTx',
+ context: ++appState.currentView.context,
+ warning: null,
+ },
+ })
+
+ case actions.VIEW_PENDING_TX:
+ const context = indexForPending(state, action.value)
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'confTx',
+ context,
+ warning: null,
+ },
+ })
+
+ case actions.PREVIOUS_TX:
+ return extend(appState, {
+ transForward: false,
+ currentView: {
+ name: 'confTx',
+ context: --appState.currentView.context,
+ warning: null,
+ },
+ })
+
+ case actions.TRANSACTION_ERROR:
+ return extend(appState, {
+ currentView: {
+ name: 'confTx',
+ errorMessage: 'There was a problem submitting this transaction.',
+ },
+ })
+
+ case actions.UNLOCK_FAILED:
+ return extend(appState, {
+ warning: action.value || 'Incorrect password. Try again.',
+ })
+
+ case actions.UNLOCK_SUCCEEDED:
+ return extend(appState, {
+ warning: '',
+ })
+
+ case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH:
+ const { device, path } = action.value
+ const newDefaults = {...appState.defaultHdPaths}
+ newDefaults[device] = path
+
+ return extend(appState, {
+ defaultHdPaths: newDefaults,
+ })
+
+ case actions.SHOW_LOADING:
+ return extend(appState, {
+ isLoading: true,
+ loadingMessage: action.value,
+ })
+
+ case actions.HIDE_LOADING:
+ return extend(appState, {
+ isLoading: false,
+ })
+
+ case actions.SHOW_SUB_LOADING_INDICATION:
+ return extend(appState, {
+ isSubLoading: true,
+ })
+
+ case actions.HIDE_SUB_LOADING_INDICATION:
+ return extend(appState, {
+ isSubLoading: false,
+ })
+ case actions.CLEAR_SEED_WORD_CACHE:
+ return extend(appState, {
+ transForward: true,
+ currentView: {},
+ isLoading: false,
+ accountDetail: {
+ subview: 'transactions',
+ accountExport: 'none',
+ privateKey: '',
+ },
+ })
+
+ case actions.DISPLAY_WARNING:
+ return extend(appState, {
+ warning: action.value,
+ isLoading: false,
+ })
+
+ case actions.HIDE_WARNING:
+ return extend(appState, {
+ warning: undefined,
+ })
+
+ case actions.REQUEST_ACCOUNT_EXPORT:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'accountDetail',
+ context: appState.currentView.context,
+ },
+ accountDetail: {
+ subview: 'export',
+ accountExport: 'requested',
+ },
+ })
+
+ case actions.EXPORT_ACCOUNT:
+ return extend(appState, {
+ accountDetail: {
+ subview: 'export',
+ accountExport: 'completed',
+ },
+ })
+
+ case actions.SHOW_PRIVATE_KEY:
+ return extend(appState, {
+ accountDetail: {
+ subview: 'export',
+ accountExport: 'completed',
+ privateKey: action.value,
+ },
+ })
+
+ case actions.BUY_ETH_VIEW:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'buyEth',
+ context: appState.currentView.name,
+ },
+ identity: state.metamask.identities[action.value],
+ buyView: {
+ subview: 'Coinbase',
+ amount: '15.00',
+ buyAddress: action.value,
+ formView: {
+ coinbase: true,
+ shapeshift: false,
+ },
+ },
+ })
+
+ case actions.ONBOARDING_BUY_ETH_VIEW:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'onboardingBuyEth',
+ context: appState.currentView.name,
+ },
+ identity: state.metamask.identities[action.value],
+ })
+
+ case actions.COINBASE_SUBVIEW:
+ return extend(appState, {
+ buyView: {
+ subview: 'Coinbase',
+ formView: {
+ coinbase: true,
+ shapeshift: false,
+ },
+ buyAddress: appState.buyView.buyAddress,
+ amount: appState.buyView.amount,
+ },
+ })
+
+ case actions.SHAPESHIFT_SUBVIEW:
+ return extend(appState, {
+ buyView: {
+ subview: 'ShapeShift',
+ formView: {
+ coinbase: false,
+ shapeshift: true,
+ marketinfo: action.value.marketinfo,
+ coinOptions: action.value.coinOptions,
+ },
+ buyAddress: action.value.buyAddress || appState.buyView.buyAddress,
+ amount: appState.buyView.amount || 0,
+ },
+ })
+
+ case actions.PAIR_UPDATE:
+ return extend(appState, {
+ buyView: {
+ subview: 'ShapeShift',
+ formView: {
+ coinbase: false,
+ shapeshift: true,
+ marketinfo: action.value.marketinfo,
+ coinOptions: appState.buyView.formView.coinOptions,
+ },
+ buyAddress: appState.buyView.buyAddress,
+ amount: appState.buyView.amount,
+ warning: null,
+ },
+ })
+
+ case actions.SHOW_QR:
+ return extend(appState, {
+ qrRequested: true,
+ transForward: true,
+
+ Qr: {
+ message: action.value.message,
+ data: action.value.data,
+ },
+ })
+
+ case actions.SHOW_QR_VIEW:
+ return extend(appState, {
+ currentView: {
+ name: 'qr',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ Qr: {
+ message: action.value.message,
+ data: action.value.data,
+ },
+ })
+
+ case actions.SET_MOUSE_USER_STATE:
+ return extend(appState, {
+ isMouseUser: action.value,
+ })
+
+ case actions.GAS_LOADING_STARTED:
+ return extend(appState, {
+ gasIsLoading: true,
+ })
+
+ case actions.GAS_LOADING_FINISHED:
+ return extend(appState, {
+ gasIsLoading: false,
+ })
+
+ case actions.SET_NETWORK_NONCE:
+ return extend(appState, {
+ networkNonce: action.value,
+ })
+
+ case actions.SET_PREVIOUS_PROVIDER:
+ if (action.value === 'loading') {
+ return appState
+ }
+ return extend(appState, {
+ lastSelectedProvider: action.value,
+ })
+
+ default:
+ return appState
+ }
+}
+
+function checkUnconfActions (state) {
+ const unconfActionList = getUnconfActionList(state)
+ const hasUnconfActions = unconfActionList.length > 0
+ return hasUnconfActions
+}
+
+function getUnconfActionList (state) {
+ const { unapprovedTxs, unapprovedMsgs,
+ unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask
+
+ const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
+ return unconfActionList
+}
+
+function indexForPending (state, txId) {
+ const unconfTxList = getUnconfActionList(state)
+ const match = unconfTxList.find((tx) => tx.id === txId)
+ const index = unconfTxList.indexOf(match)
+ return index
+}
+
+// function indexForLastPending (state) {
+// return getUnconfActionList(state).length
+// }
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
deleted file mode 100644
index f75ff809a..000000000
--- a/ui/app/ducks/confirm-transaction.duck.js
+++ /dev/null
@@ -1,420 +0,0 @@
-import {
- conversionRateSelector,
- currentCurrencySelector,
- unconfirmedTransactionsHashSelector,
- getNativeCurrency,
-} from '../selectors/confirm-transaction'
-
-import {
- getValueFromWeiHex,
- getTransactionFee,
- getHexGasTotal,
- addFiat,
- addEth,
- increaseLastGasPrice,
- hexGreaterThan,
-} from '../helpers/confirm-transaction/util'
-
-import {
- getTokenData,
- getMethodData,
- isSmartContractAddress,
- sumHexes,
-} from '../helpers/transactions.util'
-
-import { getSymbolAndDecimals } from '../token-util'
-import { conversionUtil } from '../conversion-util'
-import { addHexPrefix } from 'ethereumjs-util'
-
-// Actions
-const createActionType = action => `metamask/confirm-transaction/${action}`
-
-const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA')
-const CLEAR_TX_DATA = createActionType('CLEAR_TX_DATA')
-const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA')
-const CLEAR_TOKEN_DATA = createActionType('CLEAR_TOKEN_DATA')
-const UPDATE_METHOD_DATA = createActionType('UPDATE_METHOD_DATA')
-const CLEAR_METHOD_DATA = createActionType('CLEAR_METHOD_DATA')
-const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
-const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
-const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
-const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
-const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
-const UPDATE_NONCE = createActionType('UPDATE_NONCE')
-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 = {
- txData: {},
- tokenData: {},
- methodData: {},
- tokenProps: {
- tokenDecimals: '',
- tokenSymbol: '',
- },
- fiatTransactionAmount: '',
- fiatTransactionFee: '',
- fiatTransactionTotal: '',
- ethTransactionAmount: '',
- ethTransactionFee: '',
- ethTransactionTotal: '',
- hexTransactionAmount: '',
- hexTransactionFee: '',
- hexTransactionTotal: '',
- nonce: '',
- toSmartContract: false,
- fetchingData: false,
-}
-
-// Reducer
-export default function reducer ({ confirmTransaction: confirmState = initState }, action = {}) {
- switch (action.type) {
- case UPDATE_TX_DATA:
- return {
- ...confirmState,
- txData: {
- ...action.payload,
- },
- }
- case CLEAR_TX_DATA:
- return {
- ...confirmState,
- txData: {},
- }
- case UPDATE_TOKEN_DATA:
- return {
- ...confirmState,
- tokenData: {
- ...action.payload,
- },
- }
- case CLEAR_TOKEN_DATA:
- return {
- ...confirmState,
- tokenData: {},
- }
- case UPDATE_METHOD_DATA:
- return {
- ...confirmState,
- methodData: {
- ...action.payload,
- },
- }
- case CLEAR_METHOD_DATA:
- return {
- ...confirmState,
- methodData: {},
- }
- case UPDATE_TRANSACTION_AMOUNTS:
- const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload
- return {
- ...confirmState,
- fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
- ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
- hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount,
- }
- case UPDATE_TRANSACTION_FEES:
- const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload
- return {
- ...confirmState,
- fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
- ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
- hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee,
- }
- case UPDATE_TRANSACTION_TOTALS:
- const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload
- return {
- ...confirmState,
- fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
- ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
- hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal,
- }
- case UPDATE_TOKEN_PROPS:
- const { tokenSymbol = '', tokenDecimals = '' } = action.payload
- return {
- ...confirmState,
- tokenProps: {
- ...confirmState.tokenProps,
- tokenSymbol,
- tokenDecimals,
- },
- }
- case UPDATE_NONCE:
- return {
- ...confirmState,
- nonce: action.payload,
- }
- case UPDATE_TO_SMART_CONTRACT:
- return {
- ...confirmState,
- toSmartContract: action.payload,
- }
- case FETCH_DATA_START:
- return {
- ...confirmState,
- fetchingData: true,
- }
- case FETCH_DATA_END:
- return {
- ...confirmState,
- fetchingData: false,
- }
- case CLEAR_CONFIRM_TRANSACTION:
- return initState
- default:
- return confirmState
- }
-}
-
-// Action Creators
-export function updateTxData (txData) {
- return {
- type: UPDATE_TX_DATA,
- payload: txData,
- }
-}
-
-export function clearTxData () {
- return {
- type: CLEAR_TX_DATA,
- }
-}
-
-export function updateTokenData (tokenData) {
- return {
- type: UPDATE_TOKEN_DATA,
- payload: tokenData,
- }
-}
-
-export function clearTokenData () {
- return {
- type: CLEAR_TOKEN_DATA,
- }
-}
-
-export function updateMethodData (methodData) {
- return {
- type: UPDATE_METHOD_DATA,
- payload: methodData,
- }
-}
-
-export function clearMethodData () {
- return {
- type: CLEAR_METHOD_DATA,
- }
-}
-
-export function updateTransactionAmounts (amounts) {
- return {
- type: UPDATE_TRANSACTION_AMOUNTS,
- payload: amounts,
- }
-}
-
-export function updateTransactionFees (fees) {
- return {
- type: UPDATE_TRANSACTION_FEES,
- payload: fees,
- }
-}
-
-export function updateTransactionTotals (totals) {
- return {
- type: UPDATE_TRANSACTION_TOTALS,
- payload: totals,
- }
-}
-
-export function updateTokenProps (tokenProps) {
- return {
- type: UPDATE_TOKEN_PROPS,
- payload: tokenProps,
- }
-}
-
-export function updateNonce (nonce) {
- return {
- type: UPDATE_NONCE,
- payload: nonce,
- }
-}
-
-export function updateToSmartContract (toSmartContract) {
- return {
- type: UPDATE_TO_SMART_CONTRACT,
- payload: toSmartContract,
- }
-}
-
-export function setFetchingData (isFetching) {
- return {
- type: isFetching ? FETCH_DATA_START : FETCH_DATA_END,
- }
-}
-
-export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
- gasLimit = addHexPrefix(gasLimit)
- gasPrice = addHexPrefix(gasPrice)
- return (dispatch, getState) => {
- const { confirmTransaction: { txData } } = getState()
- const newTxData = {
- ...txData,
- txParams: {
- ...txData.txParams,
- gas: gasLimit,
- gasPrice,
- },
- }
-
- dispatch(updateTxDataAndCalculate(newTxData))
- }
-}
-
-function increaseFromLastGasPrice (txData) {
- const { lastGasPrice, txParams: { gasPrice: previousGasPrice } = {} } = txData
-
- // Set the minimum to a 10% increase from the lastGasPrice.
- const minimumGasPrice = increaseLastGasPrice(lastGasPrice)
- const gasPriceBelowMinimum = hexGreaterThan(minimumGasPrice, previousGasPrice)
- const gasPrice = (!previousGasPrice || gasPriceBelowMinimum) ? minimumGasPrice : previousGasPrice
-
- return {
- ...txData,
- txParams: {
- ...txData.txParams,
- gasPrice,
- },
- }
-}
-
-export function updateTxDataAndCalculate (txData) {
- return (dispatch, getState) => {
- const state = getState()
- const currentCurrency = currentCurrencySelector(state)
- const conversionRate = conversionRateSelector(state)
- const nativeCurrency = getNativeCurrency(state)
-
- dispatch(updateTxData(txData))
-
- const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
-
- const fiatTransactionAmount = getValueFromWeiHex({
- value, fromCurrency: nativeCurrency, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
- })
- const ethTransactionAmount = getValueFromWeiHex({
- value, fromCurrency: nativeCurrency, toCurrency: nativeCurrency, conversionRate, numberOfDecimals: 6,
- })
-
- dispatch(updateTransactionAmounts({
- fiatTransactionAmount,
- ethTransactionAmount,
- hexTransactionAmount: value,
- }))
-
- const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice })
-
- const fiatTransactionFee = getTransactionFee({
- value: hexTransactionFee,
- fromCurrency: nativeCurrency,
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ethTransactionFee = getTransactionFee({
- value: hexTransactionFee,
- fromCurrency: nativeCurrency,
- toCurrency: nativeCurrency,
- numberOfDecimals: 6,
- conversionRate,
- })
-
- dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee }))
-
- const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
- const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
- const hexTransactionTotal = sumHexes(value, hexTransactionFee)
-
- dispatch(updateTransactionTotals({
- fiatTransactionTotal,
- ethTransactionTotal,
- hexTransactionTotal,
- }))
- }
-}
-
-export function setTransactionToConfirm (transactionId) {
- return async (dispatch, getState) => {
- const state = getState()
- const unconfirmedTransactionsHash = unconfirmedTransactionsHashSelector(state)
- const transaction = unconfirmedTransactionsHash[transactionId]
-
- if (!transaction) {
- console.error(`Transaction with id ${transactionId} not found`)
- return
- }
-
- if (transaction.txParams) {
- const { lastGasPrice } = transaction
- const txData = lastGasPrice ? increaseFromLastGasPrice(transaction) : transaction
- dispatch(updateTxDataAndCalculate(txData))
-
- const { txParams } = transaction
- const { to } = txParams
-
- if (txParams.data) {
- const { tokens: existingTokens } = state
- const { data, to: tokenAddress } = txParams
-
- try {
- dispatch(setFetchingData(true))
- const methodData = await getMethodData(data)
-
- dispatch(updateMethodData(methodData))
- } catch (error) {
- dispatch(updateMethodData({}))
- dispatch(setFetchingData(false))
- }
-
- try {
- const toSmartContract = await isSmartContractAddress(to)
- dispatch(updateToSmartContract(toSmartContract))
- dispatch(setFetchingData(false))
- } catch (error) {
- dispatch(setFetchingData(false))
- }
-
- const tokenData = getTokenData(data)
- dispatch(updateTokenData(tokenData))
-
- try {
- const tokenSymbolData = await getSymbolAndDecimals(tokenAddress, existingTokens) || {}
- const { symbol: tokenSymbol = '', decimals: tokenDecimals = '' } = tokenSymbolData
- dispatch(updateTokenProps({ tokenSymbol, tokenDecimals }))
- } catch (error) {
- dispatch(updateTokenProps({ tokenSymbol: '', tokenDecimals: '' }))
- }
- }
-
- if (txParams.nonce) {
- const nonce = conversionUtil(txParams.nonce, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
-
- dispatch(updateNonce(nonce))
- }
- } else {
- dispatch(updateTxData(transaction))
- }
- }
-}
-
-export function clearConfirmTransaction () {
- return {
- type: CLEAR_CONFIRM_TRANSACTION,
- }
-}
diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js
new file mode 100644
index 000000000..4edf8a70c
--- /dev/null
+++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js
@@ -0,0 +1,420 @@
+import {
+ conversionRateSelector,
+ currentCurrencySelector,
+ unconfirmedTransactionsHashSelector,
+ getNativeCurrency,
+} from '../../selectors/confirm-transaction'
+
+import {
+ getValueFromWeiHex,
+ getTransactionFee,
+ getHexGasTotal,
+ addFiat,
+ addEth,
+ increaseLastGasPrice,
+ hexGreaterThan,
+} from '../../helpers/utils/confirm-tx.util'
+
+import {
+ getTokenData,
+ getMethodData,
+ isSmartContractAddress,
+ sumHexes,
+} from '../../helpers/utils/transactions.util'
+
+import { getSymbolAndDecimals } from '../../helpers/utils/token-util'
+import { conversionUtil } from '../../helpers/utils/conversion-util'
+import { addHexPrefix } from 'ethereumjs-util'
+
+// Actions
+const createActionType = action => `metamask/confirm-transaction/${action}`
+
+const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA')
+const CLEAR_TX_DATA = createActionType('CLEAR_TX_DATA')
+const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA')
+const CLEAR_TOKEN_DATA = createActionType('CLEAR_TOKEN_DATA')
+const UPDATE_METHOD_DATA = createActionType('UPDATE_METHOD_DATA')
+const CLEAR_METHOD_DATA = createActionType('CLEAR_METHOD_DATA')
+const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
+const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
+const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
+const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
+const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
+const UPDATE_NONCE = createActionType('UPDATE_NONCE')
+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 = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexTransactionAmount: '',
+ hexTransactionFee: '',
+ hexTransactionTotal: '',
+ nonce: '',
+ toSmartContract: false,
+ fetchingData: false,
+}
+
+// Reducer
+export default function reducer ({ confirmTransaction: confirmState = initState }, action = {}) {
+ switch (action.type) {
+ case UPDATE_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {},
+ }
+ case UPDATE_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {},
+ }
+ case UPDATE_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {},
+ }
+ case UPDATE_TRANSACTION_AMOUNTS:
+ const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
+ ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
+ hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount,
+ }
+ case UPDATE_TRANSACTION_FEES:
+ const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
+ ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
+ hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee,
+ }
+ case UPDATE_TRANSACTION_TOTALS:
+ const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
+ ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
+ hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal,
+ }
+ case UPDATE_TOKEN_PROPS:
+ const { tokenSymbol = '', tokenDecimals = '' } = action.payload
+ return {
+ ...confirmState,
+ tokenProps: {
+ ...confirmState.tokenProps,
+ tokenSymbol,
+ tokenDecimals,
+ },
+ }
+ case UPDATE_NONCE:
+ return {
+ ...confirmState,
+ nonce: action.payload,
+ }
+ case UPDATE_TO_SMART_CONTRACT:
+ return {
+ ...confirmState,
+ toSmartContract: action.payload,
+ }
+ case FETCH_DATA_START:
+ return {
+ ...confirmState,
+ fetchingData: true,
+ }
+ case FETCH_DATA_END:
+ return {
+ ...confirmState,
+ fetchingData: false,
+ }
+ case CLEAR_CONFIRM_TRANSACTION:
+ return initState
+ default:
+ return confirmState
+ }
+}
+
+// Action Creators
+export function updateTxData (txData) {
+ return {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+}
+
+export function clearTxData () {
+ return {
+ type: CLEAR_TX_DATA,
+ }
+}
+
+export function updateTokenData (tokenData) {
+ return {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+}
+
+export function clearTokenData () {
+ return {
+ type: CLEAR_TOKEN_DATA,
+ }
+}
+
+export function updateMethodData (methodData) {
+ return {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+}
+
+export function clearMethodData () {
+ return {
+ type: CLEAR_METHOD_DATA,
+ }
+}
+
+export function updateTransactionAmounts (amounts) {
+ return {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: amounts,
+ }
+}
+
+export function updateTransactionFees (fees) {
+ return {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: fees,
+ }
+}
+
+export function updateTransactionTotals (totals) {
+ return {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: totals,
+ }
+}
+
+export function updateTokenProps (tokenProps) {
+ return {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+}
+
+export function updateNonce (nonce) {
+ return {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+}
+
+export function updateToSmartContract (toSmartContract) {
+ return {
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: toSmartContract,
+ }
+}
+
+export function setFetchingData (isFetching) {
+ return {
+ type: isFetching ? FETCH_DATA_START : FETCH_DATA_END,
+ }
+}
+
+export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
+ gasLimit = addHexPrefix(gasLimit)
+ gasPrice = addHexPrefix(gasPrice)
+ return (dispatch, getState) => {
+ const { confirmTransaction: { txData } } = getState()
+ const newTxData = {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gas: gasLimit,
+ gasPrice,
+ },
+ }
+
+ dispatch(updateTxDataAndCalculate(newTxData))
+ }
+}
+
+function increaseFromLastGasPrice (txData) {
+ const { lastGasPrice, txParams: { gasPrice: previousGasPrice } = {} } = txData
+
+ // Set the minimum to a 10% increase from the lastGasPrice.
+ const minimumGasPrice = increaseLastGasPrice(lastGasPrice)
+ const gasPriceBelowMinimum = hexGreaterThan(minimumGasPrice, previousGasPrice)
+ const gasPrice = (!previousGasPrice || gasPriceBelowMinimum) ? minimumGasPrice : previousGasPrice
+
+ return {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gasPrice,
+ },
+ }
+}
+
+export function updateTxDataAndCalculate (txData) {
+ return (dispatch, getState) => {
+ const state = getState()
+ const currentCurrency = currentCurrencySelector(state)
+ const conversionRate = conversionRateSelector(state)
+ const nativeCurrency = getNativeCurrency(state)
+
+ dispatch(updateTxData(txData))
+
+ const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
+
+ const fiatTransactionAmount = getValueFromWeiHex({
+ value, fromCurrency: nativeCurrency, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
+ })
+ const ethTransactionAmount = getValueFromWeiHex({
+ value, fromCurrency: nativeCurrency, toCurrency: nativeCurrency, conversionRate, numberOfDecimals: 6,
+ })
+
+ dispatch(updateTransactionAmounts({
+ fiatTransactionAmount,
+ ethTransactionAmount,
+ hexTransactionAmount: value,
+ }))
+
+ const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice })
+
+ const fiatTransactionFee = getTransactionFee({
+ value: hexTransactionFee,
+ fromCurrency: nativeCurrency,
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ethTransactionFee = getTransactionFee({
+ value: hexTransactionFee,
+ fromCurrency: nativeCurrency,
+ toCurrency: nativeCurrency,
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee }))
+
+ const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
+ const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
+ const hexTransactionTotal = sumHexes(value, hexTransactionFee)
+
+ dispatch(updateTransactionTotals({
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ hexTransactionTotal,
+ }))
+ }
+}
+
+export function setTransactionToConfirm (transactionId) {
+ return async (dispatch, getState) => {
+ const state = getState()
+ const unconfirmedTransactionsHash = unconfirmedTransactionsHashSelector(state)
+ const transaction = unconfirmedTransactionsHash[transactionId]
+
+ if (!transaction) {
+ console.error(`Transaction with id ${transactionId} not found`)
+ return
+ }
+
+ if (transaction.txParams) {
+ const { lastGasPrice } = transaction
+ const txData = lastGasPrice ? increaseFromLastGasPrice(transaction) : transaction
+ dispatch(updateTxDataAndCalculate(txData))
+
+ const { txParams } = transaction
+ const { to } = txParams
+
+ if (txParams.data) {
+ const { tokens: existingTokens } = state
+ const { data, to: tokenAddress } = txParams
+
+ try {
+ dispatch(setFetchingData(true))
+ const methodData = await getMethodData(data)
+
+ dispatch(updateMethodData(methodData))
+ } catch (error) {
+ dispatch(updateMethodData({}))
+ dispatch(setFetchingData(false))
+ }
+
+ try {
+ const toSmartContract = await isSmartContractAddress(to)
+ dispatch(updateToSmartContract(toSmartContract))
+ dispatch(setFetchingData(false))
+ } catch (error) {
+ dispatch(setFetchingData(false))
+ }
+
+ const tokenData = getTokenData(data)
+ dispatch(updateTokenData(tokenData))
+
+ try {
+ const tokenSymbolData = await getSymbolAndDecimals(tokenAddress, existingTokens) || {}
+ const { symbol: tokenSymbol = '', decimals: tokenDecimals = '' } = tokenSymbolData
+ dispatch(updateTokenProps({ tokenSymbol, tokenDecimals }))
+ } catch (error) {
+ dispatch(updateTokenProps({ tokenSymbol: '', tokenDecimals: '' }))
+ }
+ }
+
+ if (txParams.nonce) {
+ const nonce = conversionUtil(txParams.nonce, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+
+ dispatch(updateNonce(nonce))
+ }
+ } else {
+ dispatch(updateTxData(transaction))
+ }
+ }
+}
+
+export function clearConfirmTransaction () {
+ return {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+}
diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js
new file mode 100644
index 000000000..483f2f56d
--- /dev/null
+++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js
@@ -0,0 +1,685 @@
+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'
+
+const initialState = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexTransactionAmount: '',
+ hexTransactionFee: '',
+ hexTransactionTotal: '',
+ nonce: '',
+ toSmartContract: false,
+ fetchingData: false,
+}
+
+const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
+const CLEAR_TX_DATA = 'metamask/confirm-transaction/CLEAR_TX_DATA'
+const UPDATE_TOKEN_DATA = 'metamask/confirm-transaction/UPDATE_TOKEN_DATA'
+const CLEAR_TOKEN_DATA = 'metamask/confirm-transaction/CLEAR_TOKEN_DATA'
+const UPDATE_METHOD_DATA = 'metamask/confirm-transaction/UPDATE_METHOD_DATA'
+const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA'
+const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS'
+const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES'
+const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS'
+const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
+const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
+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', () => {
+ describe('State changes', () => {
+ const mockState = {
+ confirmTransaction: {
+ txData: {
+ id: 1,
+ },
+ tokenData: {
+ name: 'abcToken',
+ },
+ methodData: {
+ name: 'approve',
+ },
+ tokenProps: {
+ tokenDecimals: '3',
+ tokenSymbol: 'ABC',
+ },
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '1.000021',
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '469.27',
+ hexTransactionAmount: '',
+ hexTransactionFee: '0x1319718a5000',
+ hexTransactionTotal: '',
+ nonce: '0x0',
+ toSmartContract: false,
+ fetchingData: false,
+ },
+ }
+
+ it('should initialize state', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({}),
+ initialState
+ )
+ })
+
+ it('should return state unchanged if it does not match a dispatched actions type', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: 'someOtherAction',
+ value: 'someValue',
+ }),
+ { ...mockState.confirmTransaction },
+ )
+ })
+
+ it('should set txData when receiving a UPDATE_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TX_DATA,
+ payload: {
+ id: 2,
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {
+ ...mockState.confirmTransaction.txData,
+ id: 2,
+ },
+ }
+ )
+ })
+
+ it('should clear txData when receiving a CLEAR_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TX_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {},
+ }
+ )
+ })
+
+ it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_DATA,
+ payload: {
+ name: 'defToken',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {
+ ...mockState.confirmTransaction.tokenData,
+ name: 'defToken',
+ },
+ }
+ )
+ })
+
+ it('should clear tokenData when receiving a CLEAR_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TOKEN_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {},
+ }
+ )
+ })
+
+ it('should set methodData when receiving a UPDATE_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_METHOD_DATA,
+ payload: {
+ name: 'transferFrom',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {
+ ...mockState.confirmTransaction.methodData,
+ name: 'transferFrom',
+ },
+ }
+ )
+ })
+
+ it('should clear methodData when receiving a CLEAR_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_METHOD_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {},
+ }
+ )
+ })
+
+ it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: {
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ hexTransactionAmount: '0x1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ hexTransactionAmount: '0x1',
+ }
+ )
+ })
+
+ it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: {
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ hexTransactionFee: '0x1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ hexTransactionFee: '0x1',
+ }
+ )
+ })
+
+ it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: {
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ hexTransactionTotal: '0x1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ hexTransactionTotal: '0x1',
+ }
+ )
+ })
+
+ it('should update tokenProps when receiving an UPDATE_TOKEN_PROPS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_PROPS,
+ payload: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenProps: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }
+ )
+ })
+
+ it('should update nonce when receiving an UPDATE_NONCE action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_NONCE,
+ payload: '0x1',
+ }),
+ {
+ ...mockState.confirmTransaction,
+ nonce: '0x1',
+ }
+ )
+ })
+
+ it('should update nonce when receiving an UPDATE_TO_SMART_CONTRACT action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: true,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ toSmartContract: true,
+ }
+ )
+ })
+
+ it('should set fetchingData to true when receiving a FETCH_DATA_START action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: FETCH_DATA_START,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fetchingData: true,
+ }
+ )
+ })
+
+ 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,
+ }),
+ {
+ ...initialState,
+ }
+ )
+ })
+ })
+
+ describe('Single actions', () => {
+ it('should create an action to update txData', () => {
+ const txData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+
+ assert.deepEqual(
+ actions.updateTxData(txData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear txData', () => {
+ const expectedAction = {
+ type: CLEAR_TX_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTxData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenData', () => {
+ const tokenData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenData(tokenData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear tokenData', () => {
+ const expectedAction = {
+ type: CLEAR_TOKEN_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTokenData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update methodData', () => {
+ const methodData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+
+ assert.deepEqual(
+ actions.updateMethodData(methodData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear methodData', () => {
+ const expectedAction = {
+ type: CLEAR_METHOD_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearMethodData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction amounts', () => {
+ const transactionAmounts = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: transactionAmounts,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionAmounts(transactionAmounts),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction fees', () => {
+ const transactionFees = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: transactionFees,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionFees(transactionFees),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction totals', () => {
+ const transactionTotals = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: transactionTotals,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionTotals(transactionTotals),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenProps', () => {
+ const tokenProps = {
+ tokenDecimals: '1',
+ tokenSymbol: 'abc',
+ }
+ const expectedAction = {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenProps(tokenProps),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update nonce', () => {
+ const nonce = '0x1'
+ const expectedAction = {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+
+ assert.deepEqual(
+ actions.updateNonce(nonce),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingData to true', () => {
+ const expectedAction = {
+ type: FETCH_DATA_START,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingData(true),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingData to false', () => {
+ const expectedAction = {
+ type: FETCH_DATA_END,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingData(false),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear confirmTransaction', () => {
+ const expectedAction = {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+
+ assert.deepEqual(
+ actions.clearConfirmTransaction(),
+ expectedAction
+ )
+ })
+ })
+
+ 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: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateGasAndCalculate({ gasLimit: '0x2', gasPrice: '0x25' }))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates txData and updates gas values in confirmTransaction', () => {
+ const txData = {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ }
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ },
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateTxDataAndCalculate(txData))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates confirmTransaction transaction', done => {
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ network: '3',
+ unapprovedTxs: {
+ 2603411941761054: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ },
+ },
+ },
+ confirmTransaction: {},
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.setTransactionToConfirm(2603411941761054))
+ .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/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js
deleted file mode 100644
index 957b00163..000000000
--- a/ui/app/ducks/gas.duck.js
+++ /dev/null
@@ -1,517 +0,0 @@
-import { clone, uniqBy, flatten } from 'ramda'
-import BigNumber from 'bignumber.js'
-import {
- loadLocalStorageData,
- saveLocalStorageData,
-} from '../../lib/local-storage-helpers'
-import {
- decGWEIToHexWEI,
-} from '../helpers/conversions.util'
-
-// Actions
-const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
-const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
-const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
-const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
-const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
-const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'
-const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
-const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
-const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
-const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
-const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
-const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
-const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
-const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
-const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED'
-
-// TODO: determine if this approach to initState is consistent with conventional ducks pattern
-const initState = {
- customData: {
- price: null,
- limit: null,
- },
- basicEstimates: {
- average: null,
- fastestWait: null,
- fastWait: null,
- fast: null,
- safeLowWait: null,
- blockNum: null,
- avgWait: null,
- blockTime: null,
- speed: null,
- fastest: null,
- safeLow: null,
- },
- basicEstimateIsLoading: true,
- gasEstimatesLoading: true,
- priceAndTimeEstimates: [],
- basicPriceAndTimeEstimates: [],
- priceAndTimeEstimatesLastRetrieved: 0,
- basicPriceAndTimeEstimatesLastRetrieved: 0,
- basicPriceEstimatesLastRetrieved: 0,
- errors: {},
-}
-
-// Reducer
-export default function reducer ({ gas: gasState = initState }, action = {}) {
- const newState = clone(gasState)
-
- switch (action.type) {
- case BASIC_GAS_ESTIMATE_LOADING_STARTED:
- return {
- ...newState,
- basicEstimateIsLoading: true,
- }
- case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
- return {
- ...newState,
- basicEstimateIsLoading: false,
- }
- case GAS_ESTIMATE_LOADING_STARTED:
- return {
- ...newState,
- gasEstimatesLoading: true,
- }
- case GAS_ESTIMATE_LOADING_FINISHED:
- return {
- ...newState,
- gasEstimatesLoading: false,
- }
- case SET_BASIC_GAS_ESTIMATE_DATA:
- return {
- ...newState,
- basicEstimates: action.value,
- }
- case SET_CUSTOM_GAS_PRICE:
- return {
- ...newState,
- customData: {
- ...newState.customData,
- price: action.value,
- },
- }
- case SET_CUSTOM_GAS_LIMIT:
- return {
- ...newState,
- customData: {
- ...newState.customData,
- limit: action.value,
- },
- }
- case SET_CUSTOM_GAS_TOTAL:
- return {
- ...newState,
- customData: {
- ...newState.customData,
- total: action.value,
- },
- }
- case SET_PRICE_AND_TIME_ESTIMATES:
- return {
- ...newState,
- priceAndTimeEstimates: action.value,
- }
- case SET_CUSTOM_GAS_ERRORS:
- return {
- ...newState,
- errors: {
- ...newState.errors,
- ...action.value,
- },
- }
- case SET_API_ESTIMATES_LAST_RETRIEVED:
- return {
- ...newState,
- priceAndTimeEstimatesLastRetrieved: action.value,
- }
- case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED:
- return {
- ...newState,
- basicPriceAndTimeEstimatesLastRetrieved: action.value,
- }
- case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED:
- return {
- ...newState,
- basicPriceEstimatesLastRetrieved: action.value,
- }
- case RESET_CUSTOM_DATA:
- return {
- ...newState,
- customData: clone(initState.customData),
- }
- case RESET_CUSTOM_GAS_STATE:
- return clone(initState)
- default:
- return newState
- }
-}
-
-// Action Creators
-export function basicGasEstimatesLoadingStarted () {
- return {
- type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
- }
-}
-
-export function basicGasEstimatesLoadingFinished () {
- return {
- type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
- }
-}
-
-export function gasEstimatesLoadingStarted () {
- return {
- type: GAS_ESTIMATE_LOADING_STARTED,
- }
-}
-
-export function gasEstimatesLoadingFinished () {
- return {
- type: GAS_ESTIMATE_LOADING_FINISHED,
- }
-}
-
-export function fetchBasicGasEstimates () {
- return (dispatch, getState) => {
- const {
- basicPriceEstimatesLastRetrieved,
- basicPriceAndTimeEstimates,
- } = getState().gas
- const timeLastRetrieved = basicPriceEstimatesLastRetrieved || loadLocalStorageData('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') || 0
-
- dispatch(basicGasEstimatesLoadingStarted())
-
- const promiseToFetch = Date.now() - timeLastRetrieved > 75000
- ? fetch('https://dev.blockscale.net/api/gasexpress.json', {
- 'headers': {},
- 'referrer': 'https://dev.blockscale.net/api/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors'}
- )
- .then(r => r.json())
- .then(({
- safeLow,
- standard: average,
- fast,
- fastest,
- block_time: blockTime,
- blockNum,
- }) => {
- const basicEstimates = {
- safeLow,
- average,
- fast,
- fastest,
- blockTime,
- blockNum,
- }
-
- const timeRetrieved = Date.now()
- dispatch(setBasicPriceEstimatesLastRetrieved(timeRetrieved))
- saveLocalStorageData(timeRetrieved, 'BASIC_PRICE_ESTIMATES_LAST_RETRIEVED')
- saveLocalStorageData(basicEstimates, 'BASIC_PRICE_ESTIMATES')
-
- return basicEstimates
- })
- : Promise.resolve(basicPriceAndTimeEstimates.length
- ? basicPriceAndTimeEstimates
- : loadLocalStorageData('BASIC_PRICE_ESTIMATES')
- )
-
- return promiseToFetch.then(basicEstimates => {
- dispatch(setBasicGasEstimateData(basicEstimates))
- dispatch(basicGasEstimatesLoadingFinished())
- return basicEstimates
- })
- }
-}
-
-export function fetchBasicGasAndTimeEstimates () {
- return (dispatch, getState) => {
- const {
- basicPriceAndTimeEstimatesLastRetrieved,
- basicPriceAndTimeEstimates,
- } = getState().gas
- const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || 0
-
- dispatch(basicGasEstimatesLoadingStarted())
-
- const promiseToFetch = Date.now() - timeLastRetrieved > 75000
- ? fetch('https://ethgasstation.info/json/ethgasAPI.json', {
- 'headers': {},
- 'referrer': 'http://ethgasstation.info/json/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors'}
- )
- .then(r => r.json())
- .then(({
- average: averageTimes10,
- avgWait,
- block_time: blockTime,
- blockNum,
- fast: fastTimes10,
- fastest: fastestTimes10,
- fastestWait,
- fastWait,
- safeLow: safeLowTimes10,
- safeLowWait,
- speed,
- }) => {
- const [average, fast, fastest, safeLow] = [
- averageTimes10,
- fastTimes10,
- fastestTimes10,
- safeLowTimes10,
- ].map(price => (new BigNumber(price)).div(10).toNumber())
-
- const basicEstimates = {
- average,
- avgWait,
- blockTime,
- blockNum,
- fast,
- fastest,
- fastestWait,
- fastWait,
- safeLow,
- safeLowWait,
- speed,
- }
-
- const timeRetrieved = Date.now()
- dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved))
- saveLocalStorageData(timeRetrieved, 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED')
- saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES')
-
- return basicEstimates
- })
- : Promise.resolve(basicPriceAndTimeEstimates.length
- ? basicPriceAndTimeEstimates
- : loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES')
- )
-
- return promiseToFetch.then(basicEstimates => {
- dispatch(setBasicGasEstimateData(basicEstimates))
- dispatch(basicGasEstimatesLoadingFinished())
- return basicEstimates
- })
- }
-}
-
-function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) {
- higherY = new BigNumber(higherY, 10)
- lowerY = new BigNumber(lowerY, 10)
- higherX = new BigNumber(higherX, 10)
- lowerX = new BigNumber(lowerX, 10)
- xForExtrapolation = new BigNumber(xForExtrapolation, 10)
- const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX))
- const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated()
-
- return Number(newTimeEstimate.toPrecision(10))
-}
-
-function getRandomArbitrary (min, max) {
- min = new BigNumber(min, 10)
- max = new BigNumber(max, 10)
- const random = new BigNumber(String(Math.random()), 10)
- return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10)
-}
-
-function calcMedian (list) {
- const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2
- return medianPos === Math.floor(medianPos)
- ? (list[medianPos - 1] + list[medianPos]) / 2
- : list[Math.floor(medianPos)]
-}
-
-function quartiles (data) {
- const lowerHalf = data.slice(0, Math.floor(data.length / 2))
- const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1))
- const median = calcMedian(data)
- const lowerQuartile = calcMedian(lowerHalf)
- const upperQuartile = calcMedian(upperHalf)
- return {
- median,
- lowerQuartile,
- upperQuartile,
- }
-}
-
-function inliersByIQR (data, prop) {
- const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d))
- const IQR = upperQuartile - lowerQuartile
- const lowerBound = lowerQuartile - 1.5 * IQR
- const upperBound = upperQuartile + 1.5 * IQR
- return data.filter(d => {
- const value = prop ? d[prop] : d
- return value >= lowerBound && value <= upperBound
- })
-}
-
-export function fetchGasEstimates (blockTime) {
- return (dispatch, getState) => {
- const {
- priceAndTimeEstimatesLastRetrieved,
- priceAndTimeEstimates,
- } = getState().gas
- const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || 0
-
- dispatch(gasEstimatesLoadingStarted())
-
- const promiseToFetch = Date.now() - timeLastRetrieved > 75000
- ? fetch('https://ethgasstation.info/json/predictTable.json', {
- 'headers': {},
- 'referrer': 'http://ethgasstation.info/json/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors'}
- )
- .then(r => r.json())
- .then(r => {
- const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice }))
- const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes)
-
- const withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => {
- const next = arr[i + 1]
- if (!next) {
- return [{ expectedWait, gasprice }]
- } else {
- const supplementalPrice = getRandomArbitrary(gasprice, next.gasprice)
- const supplementalTime = extrapolateY({
- higherY: next.expectedWait,
- lowerY: expectedWait,
- higherX: next.gasprice,
- lowerX: gasprice,
- xForExtrapolation: supplementalPrice,
- })
- const supplementalPrice2 = getRandomArbitrary(supplementalPrice, next.gasprice)
- const supplementalTime2 = extrapolateY({
- higherY: next.expectedWait,
- lowerY: supplementalTime,
- higherX: next.gasprice,
- lowerX: supplementalPrice,
- xForExtrapolation: supplementalPrice2,
- })
- return [
- { expectedWait, gasprice },
- { expectedWait: supplementalTime, gasprice: supplementalPrice },
- { expectedWait: supplementalTime2, gasprice: supplementalPrice2 },
- ]
- }
- }))
- const withOutliersRemoved = inliersByIQR(withSupplementalTimeEstimates.slice(0).reverse(), 'expectedWait').reverse()
- const timeMappedToSeconds = withOutliersRemoved.map(({ expectedWait, gasprice }) => {
- const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toNumber()
- return {
- expectedTime,
- gasprice: (new BigNumber(gasprice, 10).toNumber()),
- }
- })
-
- const timeRetrieved = Date.now()
- dispatch(setApiEstimatesLastRetrieved(timeRetrieved))
- saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED')
- saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES')
-
- return timeMappedToSeconds
- })
- : Promise.resolve(priceAndTimeEstimates.length
- ? priceAndTimeEstimates
- : loadLocalStorageData('GAS_API_ESTIMATES')
- )
-
- return promiseToFetch.then(estimates => {
- dispatch(setPricesAndTimeEstimates(estimates))
- dispatch(gasEstimatesLoadingFinished())
- })
- }
-}
-
-export function setCustomGasPriceForRetry (newPrice) {
- return (dispatch) => {
- if (newPrice !== '0x0') {
- dispatch(setCustomGasPrice(newPrice))
- } else {
- const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
- dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)))
- }
- }
-}
-
-export function setBasicGasEstimateData (basicGasEstimateData) {
- return {
- type: SET_BASIC_GAS_ESTIMATE_DATA,
- value: basicGasEstimateData,
- }
-}
-
-export function setPricesAndTimeEstimates (estimatedPricesAndTimes) {
- return {
- type: SET_PRICE_AND_TIME_ESTIMATES,
- value: estimatedPricesAndTimes,
- }
-}
-
-export function setCustomGasPrice (newPrice) {
- return {
- type: SET_CUSTOM_GAS_PRICE,
- value: newPrice,
- }
-}
-
-export function setCustomGasLimit (newLimit) {
- return {
- type: SET_CUSTOM_GAS_LIMIT,
- value: newLimit,
- }
-}
-
-export function setCustomGasTotal (newTotal) {
- return {
- type: SET_CUSTOM_GAS_TOTAL,
- value: newTotal,
- }
-}
-
-export function setCustomGasErrors (newErrors) {
- return {
- type: SET_CUSTOM_GAS_ERRORS,
- value: newErrors,
- }
-}
-
-export function setApiEstimatesLastRetrieved (retrievalTime) {
- return {
- type: SET_API_ESTIMATES_LAST_RETRIEVED,
- value: retrievalTime,
- }
-}
-
-export function setBasicApiEstimatesLastRetrieved (retrievalTime) {
- return {
- type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
- value: retrievalTime,
- }
-}
-
-export function setBasicPriceEstimatesLastRetrieved (retrievalTime) {
- return {
- type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED,
- value: retrievalTime,
- }
-}
-
-export function resetCustomGasState () {
- return { type: RESET_CUSTOM_GAS_STATE }
-}
-
-export function resetCustomData () {
- return { type: RESET_CUSTOM_DATA }
-}
diff --git a/ui/app/ducks/gas/gas-duck.test.js b/ui/app/ducks/gas/gas-duck.test.js
new file mode 100644
index 000000000..c0152c74f
--- /dev/null
+++ b/ui/app/ducks/gas/gas-duck.test.js
@@ -0,0 +1,606 @@
+import assert from 'assert'
+import sinon from 'sinon'
+import proxyquire from 'proxyquire'
+
+
+const GasDuck = proxyquire('./gas.duck.js', {
+ '../../../lib/local-storage-helpers': {
+ loadLocalStorageData: sinon.spy(),
+ saveLocalStorageData: sinon.spy(),
+ },
+})
+
+const {
+ basicGasEstimatesLoadingStarted,
+ basicGasEstimatesLoadingFinished,
+ setBasicGasEstimateData,
+ setCustomGasPrice,
+ setCustomGasLimit,
+ setCustomGasTotal,
+ setCustomGasErrors,
+ resetCustomGasState,
+ fetchBasicGasAndTimeEstimates,
+ fetchBasicGasEstimates,
+ gasEstimatesLoadingStarted,
+ gasEstimatesLoadingFinished,
+ setPricesAndTimeEstimates,
+ fetchGasEstimates,
+ setApiEstimatesLastRetrieved,
+} = GasDuck
+const GasReducer = GasDuck.default
+
+describe('Gas Duck', () => {
+ let tempFetch
+ let tempDateNow
+ const mockEthGasApiResponse = {
+ average: 20,
+ avgWait: 'mockAvgWait',
+ block_time: 'mockBlock_time',
+ blockNum: 'mockBlockNum',
+ fast: 30,
+ fastest: 40,
+ fastestWait: 'mockFastestWait',
+ fastWait: 'mockFastWait',
+ safeLow: 10,
+ safeLowWait: 'mockSafeLowWait',
+ speed: 'mockSpeed',
+ standard: 20,
+ }
+ const mockPredictTableResponse = [
+ { expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' },
+ { expectedTime: 200, expectedWait: 20, gasprice: 0.5, somethingElse: 'foobar' },
+ { expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' },
+ { expectedTime: 75, expectedWait: 7.5, gasprice: 1.5, somethingElse: 'foobar' },
+ { expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' },
+ { expectedTime: 35, expectedWait: 4.5, gasprice: 3, somethingElse: 'foobar' },
+ { expectedTime: 34, expectedWait: 4.4, gasprice: 3.1, somethingElse: 'foobar' },
+ { expectedTime: 25, expectedWait: 4.2, gasprice: 3.5, somethingElse: 'foobar' },
+ { expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' },
+ { expectedTime: 19, expectedWait: 3.9, gasprice: 4.1, somethingElse: 'foobar' },
+ { expectedTime: 15, expectedWait: 3, gasprice: 7, somethingElse: 'foobar' },
+ { expectedTime: 14, expectedWait: 2.9, gasprice: 7.1, somethingElse: 'foobar' },
+ { expectedTime: 12, expectedWait: 2.5, gasprice: 8, somethingElse: 'foobar' },
+ { expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' },
+ { expectedTime: 9, expectedWait: 1.9, gasprice: 10.1, somethingElse: 'foobar' },
+ { expectedTime: 5, expectedWait: 1, gasprice: 15, somethingElse: 'foobar' },
+ { expectedTime: 4, expectedWait: 0.9, gasprice: 15.1, somethingElse: 'foobar' },
+ { expectedTime: 2, expectedWait: 0.8, gasprice: 17, somethingElse: 'foobar' },
+ { expectedTime: 1.1, expectedWait: 0.6, gasprice: 19.9, somethingElse: 'foobar' },
+ { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' },
+ ]
+ const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => {
+ const dataToResolve = url.match(/ethgasAPI|gasexpress/)
+ ? mockEthGasApiResponse
+ : mockPredictTableResponse
+ resolve({
+ json: () => new Promise(resolve => resolve(dataToResolve)),
+ })
+ }))
+
+ beforeEach(() => {
+ tempFetch = global.fetch
+ tempDateNow = global.Date.now
+ global.fetch = fetchStub
+ global.Date.now = () => 2000000
+ })
+
+ afterEach(() => {
+ fetchStub.resetHistory()
+ global.fetch = tempFetch
+ global.Date.now = tempDateNow
+ })
+
+ const mockState = {
+ gas: {
+ mockProp: 123,
+ },
+ }
+ const initState = {
+ customData: {
+ price: null,
+ limit: null,
+ },
+ basicEstimates: {
+ average: null,
+ fastestWait: null,
+ fastWait: null,
+ fast: null,
+ safeLowWait: null,
+ blockNum: null,
+ avgWait: null,
+ blockTime: null,
+ speed: null,
+ fastest: null,
+ safeLow: null,
+ },
+ basicEstimateIsLoading: true,
+ errors: {},
+ gasEstimatesLoading: true,
+ priceAndTimeEstimates: [],
+ priceAndTimeEstimatesLastRetrieved: 0,
+ basicPriceAndTimeEstimates: [],
+ basicPriceAndTimeEstimatesLastRetrieved: 0,
+ basicPriceEstimatesLastRetrieved: 0,
+ }
+ const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
+ const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
+ const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
+ const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
+ const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
+ const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
+ const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
+ const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
+ const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
+ const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
+ const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
+ const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
+ const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
+ const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED'
+
+ describe('GasReducer()', () => {
+ it('should initialize state', () => {
+ assert.deepEqual(
+ GasReducer({}),
+ initState
+ )
+ })
+
+ it('should return state unchanged if it does not match a dispatched actions type', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: 'someOtherAction',
+ value: 'someValue',
+ }),
+ Object.assign({}, mockState.gas)
+ )
+ })
+
+ it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
+ }),
+ Object.assign({basicEstimateIsLoading: true}, mockState.gas)
+ )
+ })
+
+ it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
+ }),
+ Object.assign({basicEstimateIsLoading: false}, mockState.gas)
+ )
+ })
+
+ it('should set gasEstimatesLoading to true when receiving a GAS_ESTIMATE_LOADING_STARTED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: GAS_ESTIMATE_LOADING_STARTED,
+ }),
+ Object.assign({gasEstimatesLoading: true}, mockState.gas)
+ )
+ })
+
+ it('should set gasEstimatesLoading to false when receiving a GAS_ESTIMATE_LOADING_FINISHED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: GAS_ESTIMATE_LOADING_FINISHED,
+ }),
+ Object.assign({gasEstimatesLoading: false}, mockState.gas)
+ )
+ })
+
+ it('should return a new object (and not just modify the existing state object)', () => {
+ assert.deepEqual(GasReducer(mockState), mockState.gas)
+ assert.notEqual(GasReducer(mockState), mockState.gas)
+ })
+
+ it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_BASIC_GAS_ESTIMATE_DATA,
+ value: { someProp: 'someData123' },
+ }),
+ Object.assign({basicEstimates: {someProp: 'someData123'} }, mockState.gas)
+ )
+ })
+
+ it('should set priceAndTimeEstimates when receiving a SET_PRICE_AND_TIME_ESTIMATES action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_PRICE_AND_TIME_ESTIMATES,
+ value: { someProp: 'someData123' },
+ }),
+ Object.assign({priceAndTimeEstimates: {someProp: 'someData123'} }, mockState.gas)
+ )
+ })
+
+ it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_CUSTOM_GAS_PRICE,
+ value: 4321,
+ }),
+ Object.assign({customData: {price: 4321} }, mockState.gas)
+ )
+ })
+
+ it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_CUSTOM_GAS_LIMIT,
+ value: 9876,
+ }),
+ Object.assign({customData: {limit: 9876} }, mockState.gas)
+ )
+ })
+
+ it('should set customData.total when receiving a SET_CUSTOM_GAS_TOTAL action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_CUSTOM_GAS_TOTAL,
+ value: 10000,
+ }),
+ Object.assign({customData: {total: 10000} }, mockState.gas)
+ )
+ })
+
+ it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_API_ESTIMATES_LAST_RETRIEVED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_API_ESTIMATES_LAST_RETRIEVED,
+ value: 1500000000000,
+ }),
+ Object.assign({ priceAndTimeEstimatesLastRetrieved: 1500000000000 }, mockState.gas)
+ )
+ })
+
+ it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_BASIC_API_ESTIMATES_LAST_RETRIEVED action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
+ value: 1700000000000,
+ }),
+ Object.assign({ basicPriceAndTimeEstimatesLastRetrieved: 1700000000000 }, mockState.gas)
+ )
+ })
+
+ it('should set errors when receiving a SET_CUSTOM_GAS_ERRORS action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: SET_CUSTOM_GAS_ERRORS,
+ value: { someError: 'error_error' },
+ }),
+ Object.assign({errors: {someError: 'error_error'} }, mockState.gas)
+ )
+ })
+
+ it('should return the initial state in response to a RESET_CUSTOM_GAS_STATE action', () => {
+ assert.deepEqual(
+ GasReducer(mockState, {
+ type: RESET_CUSTOM_GAS_STATE,
+ }),
+ Object.assign({}, initState)
+ )
+ })
+ })
+
+ describe('basicGasEstimatesLoadingStarted', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ basicGasEstimatesLoadingStarted(),
+ { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }
+ )
+ })
+ })
+
+ describe('basicGasEstimatesLoadingFinished', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ basicGasEstimatesLoadingFinished(),
+ { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }
+ )
+ })
+ })
+
+ describe('fetchBasicGasEstimates', () => {
+ const mockDistpatch = sinon.spy()
+ it('should call fetch with the expected params', async () => {
+ await fetchBasicGasEstimates()(mockDistpatch, () => ({ gas: Object.assign(
+ {},
+ initState,
+ { basicPriceAEstimatesLastRetrieved: 1000000 }
+ ) }))
+ assert.deepEqual(
+ mockDistpatch.getCall(0).args,
+ [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
+ )
+ assert.deepEqual(
+ global.fetch.getCall(0).args,
+ [
+ 'https://dev.blockscale.net/api/gasexpress.json',
+ {
+ 'headers': {},
+ 'referrer': 'https://dev.blockscale.net/api/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors',
+ },
+ ]
+ )
+
+ assert.deepEqual(
+ mockDistpatch.getCall(1).args,
+ [{ type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ]
+ )
+
+ assert.deepEqual(
+ mockDistpatch.getCall(2).args,
+ [{
+ type: SET_BASIC_GAS_ESTIMATE_DATA,
+ value: {
+ average: 20,
+ blockTime: 'mockBlock_time',
+ blockNum: 'mockBlockNum',
+ fast: 30,
+ fastest: 40,
+ safeLow: 10,
+ },
+ }]
+ )
+ assert.deepEqual(
+ mockDistpatch.getCall(3).args,
+ [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }]
+ )
+ })
+ })
+
+ describe('fetchBasicGasAndTimeEstimates', () => {
+ const mockDistpatch = sinon.spy()
+ it('should call fetch with the expected params', async () => {
+ await fetchBasicGasAndTimeEstimates()(mockDistpatch, () => ({ gas: Object.assign(
+ {},
+ initState,
+ { basicPriceAndTimeEstimatesLastRetrieved: 1000000 }
+ ),
+ metamask: { provider: { type: 'ropsten' } },
+ }))
+ assert.deepEqual(
+ mockDistpatch.getCall(0).args,
+ [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
+ )
+ assert.deepEqual(
+ global.fetch.getCall(0).args,
+ [
+ 'https://ethgasstation.info/json/ethgasAPI.json',
+ {
+ 'headers': {},
+ 'referrer': 'http://ethgasstation.info/json/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors',
+ },
+ ]
+ )
+
+ assert.deepEqual(
+ mockDistpatch.getCall(1).args,
+ [{ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ]
+ )
+
+ assert.deepEqual(
+ mockDistpatch.getCall(2).args,
+ [{
+ type: SET_BASIC_GAS_ESTIMATE_DATA,
+ value: {
+ average: 2,
+ avgWait: 'mockAvgWait',
+ blockTime: 'mockBlock_time',
+ blockNum: 'mockBlockNum',
+ fast: 3,
+ fastest: 4,
+ fastestWait: 'mockFastestWait',
+ fastWait: 'mockFastWait',
+ safeLow: 1,
+ safeLowWait: 'mockSafeLowWait',
+ speed: 'mockSpeed',
+ },
+ }]
+ )
+ assert.deepEqual(
+ mockDistpatch.getCall(3).args,
+ [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }]
+ )
+ })
+ })
+
+ describe('fetchGasEstimates', () => {
+ const mockDistpatch = sinon.spy()
+
+ beforeEach(() => {
+ mockDistpatch.resetHistory()
+ })
+
+ it('should call fetch with the expected params', async () => {
+ global.fetch.resetHistory()
+ await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
+ {},
+ initState,
+ { priceAndTimeEstimatesLastRetrieved: 1000000 }
+ ),
+ metamask: { provider: { type: 'ropsten' } },
+ }))
+ assert.deepEqual(
+ mockDistpatch.getCall(0).args,
+ [{ type: GAS_ESTIMATE_LOADING_STARTED} ]
+ )
+ assert.deepEqual(
+ global.fetch.getCall(0).args,
+ [
+ 'https://ethgasstation.info/json/predictTable.json',
+ {
+ 'headers': {},
+ 'referrer': 'http://ethgasstation.info/json/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors',
+ },
+ ]
+ )
+
+ assert.deepEqual(
+ mockDistpatch.getCall(1).args,
+ [{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }]
+ )
+
+ const { type: thirdDispatchCallType, value: priceAndTimeEstimateResult } = mockDistpatch.getCall(2).args[0]
+ assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES)
+ assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2)
+ assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100))
+ assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime))
+ assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice))
+
+ assert.deepEqual(
+ mockDistpatch.getCall(3).args,
+ [{ type: GAS_ESTIMATE_LOADING_FINISHED }]
+ )
+ })
+
+ it('should not call fetch if the estimates were retrieved < 75000 ms ago', async () => {
+ global.fetch.resetHistory()
+ await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
+ {},
+ initState,
+ {
+ priceAndTimeEstimatesLastRetrieved: Date.now(),
+ priceAndTimeEstimates: [{
+ expectedTime: '10',
+ expectedWait: 2,
+ gasprice: 50,
+ }],
+ }
+ ),
+ metamask: { provider: { type: 'ropsten' } },
+ }))
+ assert.deepEqual(
+ mockDistpatch.getCall(0).args,
+ [{ type: GAS_ESTIMATE_LOADING_STARTED} ]
+ )
+ assert.equal(global.fetch.callCount, 0)
+
+ assert.deepEqual(
+ mockDistpatch.getCall(1).args,
+ [{
+ type: SET_PRICE_AND_TIME_ESTIMATES,
+ value: [
+ {
+ expectedTime: '10',
+ expectedWait: 2,
+ gasprice: 50,
+ },
+ ],
+
+ }]
+ )
+ assert.deepEqual(
+ mockDistpatch.getCall(2).args,
+ [{ type: GAS_ESTIMATE_LOADING_FINISHED }]
+ )
+ })
+ })
+
+ describe('gasEstimatesLoadingStarted', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ gasEstimatesLoadingStarted(),
+ { type: GAS_ESTIMATE_LOADING_STARTED }
+ )
+ })
+ })
+
+ describe('gasEstimatesLoadingFinished', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ gasEstimatesLoadingFinished(),
+ { type: GAS_ESTIMATE_LOADING_FINISHED }
+ )
+ })
+ })
+
+ describe('setPricesAndTimeEstimates', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setPricesAndTimeEstimates('mockPricesAndTimeEstimates'),
+ { type: SET_PRICE_AND_TIME_ESTIMATES, value: 'mockPricesAndTimeEstimates' }
+ )
+ })
+ })
+
+ describe('setBasicGasEstimateData', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setBasicGasEstimateData('mockBasicEstimatData'),
+ { type: SET_BASIC_GAS_ESTIMATE_DATA, value: 'mockBasicEstimatData' }
+ )
+ })
+ })
+
+ describe('setCustomGasPrice', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setCustomGasPrice('mockCustomGasPrice'),
+ { type: SET_CUSTOM_GAS_PRICE, value: 'mockCustomGasPrice' }
+ )
+ })
+ })
+
+ describe('setCustomGasLimit', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setCustomGasLimit('mockCustomGasLimit'),
+ { type: SET_CUSTOM_GAS_LIMIT, value: 'mockCustomGasLimit' }
+ )
+ })
+ })
+
+ describe('setCustomGasTotal', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setCustomGasTotal('mockCustomGasTotal'),
+ { type: SET_CUSTOM_GAS_TOTAL, value: 'mockCustomGasTotal' }
+ )
+ })
+ })
+
+ describe('setCustomGasErrors', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setCustomGasErrors('mockErrorObject'),
+ { type: SET_CUSTOM_GAS_ERRORS, value: 'mockErrorObject' }
+ )
+ })
+ })
+
+ describe('setApiEstimatesLastRetrieved', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ setApiEstimatesLastRetrieved(1234),
+ { type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 1234 }
+ )
+ })
+ })
+
+ describe('resetCustomGasState', () => {
+ it('should create the correct action', () => {
+ assert.deepEqual(
+ resetCustomGasState(),
+ { type: RESET_CUSTOM_GAS_STATE }
+ )
+ })
+ })
+
+})
diff --git a/ui/app/ducks/gas/gas.duck.js b/ui/app/ducks/gas/gas.duck.js
new file mode 100644
index 000000000..8eb68f846
--- /dev/null
+++ b/ui/app/ducks/gas/gas.duck.js
@@ -0,0 +1,526 @@
+import { clone, uniqBy, flatten } from 'ramda'
+import BigNumber from 'bignumber.js'
+import {
+ loadLocalStorageData,
+ saveLocalStorageData,
+} from '../../../lib/local-storage-helpers'
+import {
+ decGWEIToHexWEI,
+} from '../../helpers/utils/conversions.util'
+import {
+ isEthereumNetwork,
+} from '../../selectors/selectors'
+
+// Actions
+const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
+const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
+const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
+const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
+const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
+const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'
+const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
+const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
+const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
+const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
+const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
+const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
+const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
+const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
+const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED'
+
+// TODO: determine if this approach to initState is consistent with conventional ducks pattern
+const initState = {
+ customData: {
+ price: null,
+ limit: null,
+ },
+ basicEstimates: {
+ average: null,
+ fastestWait: null,
+ fastWait: null,
+ fast: null,
+ safeLowWait: null,
+ blockNum: null,
+ avgWait: null,
+ blockTime: null,
+ speed: null,
+ fastest: null,
+ safeLow: null,
+ },
+ basicEstimateIsLoading: true,
+ gasEstimatesLoading: true,
+ priceAndTimeEstimates: [],
+ basicPriceAndTimeEstimates: [],
+ priceAndTimeEstimatesLastRetrieved: 0,
+ basicPriceAndTimeEstimatesLastRetrieved: 0,
+ basicPriceEstimatesLastRetrieved: 0,
+ errors: {},
+}
+
+// Reducer
+export default function reducer ({ gas: gasState = initState }, action = {}) {
+ const newState = clone(gasState)
+
+ switch (action.type) {
+ case BASIC_GAS_ESTIMATE_LOADING_STARTED:
+ return {
+ ...newState,
+ basicEstimateIsLoading: true,
+ }
+ case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
+ return {
+ ...newState,
+ basicEstimateIsLoading: false,
+ }
+ case GAS_ESTIMATE_LOADING_STARTED:
+ return {
+ ...newState,
+ gasEstimatesLoading: true,
+ }
+ case GAS_ESTIMATE_LOADING_FINISHED:
+ return {
+ ...newState,
+ gasEstimatesLoading: false,
+ }
+ case SET_BASIC_GAS_ESTIMATE_DATA:
+ return {
+ ...newState,
+ basicEstimates: action.value,
+ }
+ case SET_CUSTOM_GAS_PRICE:
+ return {
+ ...newState,
+ customData: {
+ ...newState.customData,
+ price: action.value,
+ },
+ }
+ case SET_CUSTOM_GAS_LIMIT:
+ return {
+ ...newState,
+ customData: {
+ ...newState.customData,
+ limit: action.value,
+ },
+ }
+ case SET_CUSTOM_GAS_TOTAL:
+ return {
+ ...newState,
+ customData: {
+ ...newState.customData,
+ total: action.value,
+ },
+ }
+ case SET_PRICE_AND_TIME_ESTIMATES:
+ return {
+ ...newState,
+ priceAndTimeEstimates: action.value,
+ }
+ case SET_CUSTOM_GAS_ERRORS:
+ return {
+ ...newState,
+ errors: {
+ ...newState.errors,
+ ...action.value,
+ },
+ }
+ case SET_API_ESTIMATES_LAST_RETRIEVED:
+ return {
+ ...newState,
+ priceAndTimeEstimatesLastRetrieved: action.value,
+ }
+ case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED:
+ return {
+ ...newState,
+ basicPriceAndTimeEstimatesLastRetrieved: action.value,
+ }
+ case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED:
+ return {
+ ...newState,
+ basicPriceEstimatesLastRetrieved: action.value,
+ }
+ case RESET_CUSTOM_DATA:
+ return {
+ ...newState,
+ customData: clone(initState.customData),
+ }
+ case RESET_CUSTOM_GAS_STATE:
+ return clone(initState)
+ default:
+ return newState
+ }
+}
+
+// Action Creators
+export function basicGasEstimatesLoadingStarted () {
+ return {
+ type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
+ }
+}
+
+export function basicGasEstimatesLoadingFinished () {
+ return {
+ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
+ }
+}
+
+export function gasEstimatesLoadingStarted () {
+ return {
+ type: GAS_ESTIMATE_LOADING_STARTED,
+ }
+}
+
+export function gasEstimatesLoadingFinished () {
+ return {
+ type: GAS_ESTIMATE_LOADING_FINISHED,
+ }
+}
+
+export function fetchBasicGasEstimates () {
+ return (dispatch, getState) => {
+ const {
+ basicPriceEstimatesLastRetrieved,
+ basicPriceAndTimeEstimates,
+ } = getState().gas
+ const timeLastRetrieved = basicPriceEstimatesLastRetrieved || loadLocalStorageData('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') || 0
+
+ dispatch(basicGasEstimatesLoadingStarted())
+
+ const promiseToFetch = Date.now() - timeLastRetrieved > 75000
+ ? fetch('https://dev.blockscale.net/api/gasexpress.json', {
+ 'headers': {},
+ 'referrer': 'https://dev.blockscale.net/api/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors'}
+ )
+ .then(r => r.json())
+ .then(({
+ safeLow,
+ standard: average,
+ fast,
+ fastest,
+ block_time: blockTime,
+ blockNum,
+ }) => {
+ const basicEstimates = {
+ safeLow,
+ average,
+ fast,
+ fastest,
+ blockTime,
+ blockNum,
+ }
+
+ const timeRetrieved = Date.now()
+ dispatch(setBasicPriceEstimatesLastRetrieved(timeRetrieved))
+ saveLocalStorageData(timeRetrieved, 'BASIC_PRICE_ESTIMATES_LAST_RETRIEVED')
+ saveLocalStorageData(basicEstimates, 'BASIC_PRICE_ESTIMATES')
+
+ return basicEstimates
+ })
+ : Promise.resolve(basicPriceAndTimeEstimates.length
+ ? basicPriceAndTimeEstimates
+ : loadLocalStorageData('BASIC_PRICE_ESTIMATES')
+ )
+
+ return promiseToFetch.then(basicEstimates => {
+ dispatch(setBasicGasEstimateData(basicEstimates))
+ dispatch(basicGasEstimatesLoadingFinished())
+ return basicEstimates
+ })
+ }
+}
+
+export function fetchBasicGasAndTimeEstimates () {
+ return (dispatch, getState) => {
+ const {
+ basicPriceAndTimeEstimatesLastRetrieved,
+ basicPriceAndTimeEstimates,
+ } = getState().gas
+ const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || 0
+
+ dispatch(basicGasEstimatesLoadingStarted())
+
+ const promiseToFetch = Date.now() - timeLastRetrieved > 75000
+ ? fetch('https://ethgasstation.info/json/ethgasAPI.json', {
+ 'headers': {},
+ 'referrer': 'http://ethgasstation.info/json/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors'}
+ )
+ .then(r => r.json())
+ .then(({
+ average: averageTimes10,
+ avgWait,
+ block_time: blockTime,
+ blockNum,
+ fast: fastTimes10,
+ fastest: fastestTimes10,
+ fastestWait,
+ fastWait,
+ safeLow: safeLowTimes10,
+ safeLowWait,
+ speed,
+ }) => {
+ const [average, fast, fastest, safeLow] = [
+ averageTimes10,
+ fastTimes10,
+ fastestTimes10,
+ safeLowTimes10,
+ ].map(price => (new BigNumber(price)).div(10).toNumber())
+
+ const basicEstimates = {
+ average,
+ avgWait,
+ blockTime,
+ blockNum,
+ fast,
+ fastest,
+ fastestWait,
+ fastWait,
+ safeLow,
+ safeLowWait,
+ speed,
+ }
+
+ const timeRetrieved = Date.now()
+ dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved))
+ saveLocalStorageData(timeRetrieved, 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED')
+ saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES')
+
+ return basicEstimates
+ })
+ : Promise.resolve(basicPriceAndTimeEstimates.length
+ ? basicPriceAndTimeEstimates
+ : loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES')
+ )
+
+ return promiseToFetch.then(basicEstimates => {
+ dispatch(setBasicGasEstimateData(basicEstimates))
+ dispatch(basicGasEstimatesLoadingFinished())
+ return basicEstimates
+ })
+ }
+}
+
+function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) {
+ higherY = new BigNumber(higherY, 10)
+ lowerY = new BigNumber(lowerY, 10)
+ higherX = new BigNumber(higherX, 10)
+ lowerX = new BigNumber(lowerX, 10)
+ xForExtrapolation = new BigNumber(xForExtrapolation, 10)
+ const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX))
+ const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated()
+
+ return Number(newTimeEstimate.toPrecision(10))
+}
+
+function getRandomArbitrary (min, max) {
+ min = new BigNumber(min, 10)
+ max = new BigNumber(max, 10)
+ const random = new BigNumber(String(Math.random()), 10)
+ return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10)
+}
+
+function calcMedian (list) {
+ const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2
+ return medianPos === Math.floor(medianPos)
+ ? (list[medianPos - 1] + list[medianPos]) / 2
+ : list[Math.floor(medianPos)]
+}
+
+function quartiles (data) {
+ const lowerHalf = data.slice(0, Math.floor(data.length / 2))
+ const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1))
+ const median = calcMedian(data)
+ const lowerQuartile = calcMedian(lowerHalf)
+ const upperQuartile = calcMedian(upperHalf)
+ return {
+ median,
+ lowerQuartile,
+ upperQuartile,
+ }
+}
+
+function inliersByIQR (data, prop) {
+ const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d))
+ const IQR = upperQuartile - lowerQuartile
+ const lowerBound = lowerQuartile - 1.5 * IQR
+ const upperBound = upperQuartile + 1.5 * IQR
+ return data.filter(d => {
+ const value = prop ? d[prop] : d
+ return value >= lowerBound && value <= upperBound
+ })
+}
+
+export function fetchGasEstimates (blockTime) {
+ return (dispatch, getState) => {
+ const state = getState()
+
+ if (isEthereumNetwork(state)) {
+ return Promise.resolve(null)
+ }
+
+ const {
+ priceAndTimeEstimatesLastRetrieved,
+ priceAndTimeEstimates,
+ } = state.gas
+ const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || 0
+
+ dispatch(gasEstimatesLoadingStarted())
+
+ const promiseToFetch = Date.now() - timeLastRetrieved > 75000
+ ? fetch('https://ethgasstation.info/json/predictTable.json', {
+ 'headers': {},
+ 'referrer': 'http://ethgasstation.info/json/',
+ 'referrerPolicy': 'no-referrer-when-downgrade',
+ 'body': null,
+ 'method': 'GET',
+ 'mode': 'cors'}
+ )
+ .then(r => r.json())
+ .then(r => {
+ const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice }))
+ const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes)
+
+ const withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => {
+ const next = arr[i + 1]
+ if (!next) {
+ return [{ expectedWait, gasprice }]
+ } else {
+ const supplementalPrice = getRandomArbitrary(gasprice, next.gasprice)
+ const supplementalTime = extrapolateY({
+ higherY: next.expectedWait,
+ lowerY: expectedWait,
+ higherX: next.gasprice,
+ lowerX: gasprice,
+ xForExtrapolation: supplementalPrice,
+ })
+ const supplementalPrice2 = getRandomArbitrary(supplementalPrice, next.gasprice)
+ const supplementalTime2 = extrapolateY({
+ higherY: next.expectedWait,
+ lowerY: supplementalTime,
+ higherX: next.gasprice,
+ lowerX: supplementalPrice,
+ xForExtrapolation: supplementalPrice2,
+ })
+ return [
+ { expectedWait, gasprice },
+ { expectedWait: supplementalTime, gasprice: supplementalPrice },
+ { expectedWait: supplementalTime2, gasprice: supplementalPrice2 },
+ ]
+ }
+ }))
+ const withOutliersRemoved = inliersByIQR(withSupplementalTimeEstimates.slice(0).reverse(), 'expectedWait').reverse()
+ const timeMappedToSeconds = withOutliersRemoved.map(({ expectedWait, gasprice }) => {
+ const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toNumber()
+ return {
+ expectedTime,
+ gasprice: (new BigNumber(gasprice, 10).toNumber()),
+ }
+ })
+
+ const timeRetrieved = Date.now()
+ dispatch(setApiEstimatesLastRetrieved(timeRetrieved))
+ saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED')
+ saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES')
+
+ return timeMappedToSeconds
+ })
+ : Promise.resolve(priceAndTimeEstimates.length
+ ? priceAndTimeEstimates
+ : loadLocalStorageData('GAS_API_ESTIMATES')
+ )
+
+ return promiseToFetch.then(estimates => {
+ dispatch(setPricesAndTimeEstimates(estimates))
+ dispatch(gasEstimatesLoadingFinished())
+ })
+ }
+}
+
+export function setCustomGasPriceForRetry (newPrice) {
+ return (dispatch) => {
+ if (newPrice !== '0x0') {
+ dispatch(setCustomGasPrice(newPrice))
+ } else {
+ const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
+ dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)))
+ }
+ }
+}
+
+export function setBasicGasEstimateData (basicGasEstimateData) {
+ return {
+ type: SET_BASIC_GAS_ESTIMATE_DATA,
+ value: basicGasEstimateData,
+ }
+}
+
+export function setPricesAndTimeEstimates (estimatedPricesAndTimes) {
+ return {
+ type: SET_PRICE_AND_TIME_ESTIMATES,
+ value: estimatedPricesAndTimes,
+ }
+}
+
+export function setCustomGasPrice (newPrice) {
+ return {
+ type: SET_CUSTOM_GAS_PRICE,
+ value: newPrice,
+ }
+}
+
+export function setCustomGasLimit (newLimit) {
+ return {
+ type: SET_CUSTOM_GAS_LIMIT,
+ value: newLimit,
+ }
+}
+
+export function setCustomGasTotal (newTotal) {
+ return {
+ type: SET_CUSTOM_GAS_TOTAL,
+ value: newTotal,
+ }
+}
+
+export function setCustomGasErrors (newErrors) {
+ return {
+ type: SET_CUSTOM_GAS_ERRORS,
+ value: newErrors,
+ }
+}
+
+export function setApiEstimatesLastRetrieved (retrievalTime) {
+ return {
+ type: SET_API_ESTIMATES_LAST_RETRIEVED,
+ value: retrievalTime,
+ }
+}
+
+export function setBasicApiEstimatesLastRetrieved (retrievalTime) {
+ return {
+ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
+ value: retrievalTime,
+ }
+}
+
+export function setBasicPriceEstimatesLastRetrieved (retrievalTime) {
+ return {
+ type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED,
+ value: retrievalTime,
+ }
+}
+
+export function resetCustomGasState () {
+ return { type: RESET_CUSTOM_GAS_STATE }
+}
+
+export function resetCustomData () {
+ return { type: RESET_CUSTOM_DATA }
+}
diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js
new file mode 100644
index 000000000..2d33edcfa
--- /dev/null
+++ b/ui/app/ducks/index.js
@@ -0,0 +1,95 @@
+const clone = require('clone')
+const extend = require('xtend')
+const copyToClipboard = require('copy-to-clipboard')
+
+//
+// Sub-Reducers take in the complete state and return their sub-state
+//
+const reduceMetamask = require('./metamask/metamask')
+const reduceApp = require('./app/app')
+const reduceLocale = require('./locale/locale')
+const reduceSend = require('./send/send.duck').default
+import reduceConfirmTransaction from './confirm-transaction/confirm-transaction.duck'
+import reduceGas from './gas/gas.duck'
+
+window.METAMASK_CACHED_LOG_STATE = null
+
+module.exports = rootReducer
+
+function rootReducer (state, action) {
+ // clone
+ state = extend(state)
+
+ if (action.type === 'GLOBAL_FORCE_UPDATE') {
+ return action.value
+ }
+
+ //
+ // MetaMask
+ //
+
+ state.metamask = reduceMetamask(state, action)
+
+ //
+ // AppState
+ //
+
+ state.appState = reduceApp(state, action)
+
+ //
+ // LocaleMessages
+ //
+
+ state.localeMessages = reduceLocale(state, action)
+
+ //
+ // Send
+ //
+
+ state.send = reduceSend(state, action)
+
+ state.confirmTransaction = reduceConfirmTransaction(state, action)
+
+ state.gas = reduceGas(state, action)
+
+ window.METAMASK_CACHED_LOG_STATE = state
+ return state
+}
+
+window.getCleanAppState = function () {
+ const state = clone(window.METAMASK_CACHED_LOG_STATE)
+ // append additional information
+ state.version = global.platform.getVersion()
+ state.browser = window.navigator.userAgent
+ // ensure seedWords are not included
+ if (state.metamask) delete state.metamask.seedWords
+ if (state.appState.currentView) delete state.appState.currentView.seedWords
+ return state
+}
+
+window.logStateString = function (cb) {
+ const state = window.getCleanAppState()
+ global.platform.getPlatformInfo((err, platform) => {
+ if (err) return cb(err)
+ state.platform = platform
+ const stateString = JSON.stringify(state, removeSeedWords, 2)
+ cb(null, stateString)
+ })
+}
+
+window.logState = function (toClipboard) {
+ return window.logStateString((err, result) => {
+ if (err) {
+ console.error(err.message)
+ } else if (toClipboard) {
+ copyToClipboard(result)
+ console.log('State log copied')
+ } else {
+ console.log(result)
+ }
+ })
+}
+
+function removeSeedWords (key, value) {
+ return key === 'seedWords' ? undefined : value
+}
diff --git a/ui/app/ducks/locale/locale.js b/ui/app/ducks/locale/locale.js
new file mode 100644
index 000000000..bb8e1b08e
--- /dev/null
+++ b/ui/app/ducks/locale/locale.js
@@ -0,0 +1,17 @@
+const extend = require('xtend')
+const actions = require('../../store/actions')
+
+module.exports = reduceMetamask
+
+function reduceMetamask (state, action) {
+ const localeMessagesState = extend({}, state.localeMessages)
+
+ switch (action.type) {
+ case actions.SET_LOCALE_MESSAGES:
+ return extend(localeMessagesState, {
+ current: action.value,
+ })
+ default:
+ return localeMessagesState
+ }
+}
diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js
new file mode 100644
index 000000000..864229e83
--- /dev/null
+++ b/ui/app/ducks/metamask/metamask.js
@@ -0,0 +1,419 @@
+const extend = require('xtend')
+const actions = require('../../store/actions')
+const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
+const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/controllers/network/enums')
+
+module.exports = reduceMetamask
+
+function reduceMetamask (state, action) {
+ let newState
+
+ // clone + defaults
+ var metamaskState = extend({
+ isInitialized: false,
+ isUnlocked: false,
+ isAccountMenuOpen: false,
+ isPopup: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP,
+ rpcTarget: 'https://rawtestrpc.metamask.io/',
+ identities: {},
+ unapprovedTxs: {},
+ noActiveNotices: true,
+ nextUnreadNotice: undefined,
+ frequentRpcList: [],
+ addressBook: [],
+ selectedTokenAddress: null,
+ contractExchangeRates: {},
+ tokenExchangeRates: {},
+ tokens: [],
+ pendingTokens: {},
+ send: {
+ gasLimit: null,
+ gasPrice: null,
+ gasTotal: null,
+ tokenBalance: '0x0',
+ from: '',
+ to: '',
+ amount: '0',
+ memo: '',
+ errors: {},
+ maxModeOn: false,
+ editingTransactionId: null,
+ forceGasMin: null,
+ toNickname: '',
+ },
+ coinOptions: {},
+ useBlockie: false,
+ featureFlags: {},
+ networkEndpointType: OLD_UI_NETWORK_TYPE,
+ isRevealingSeedWords: false,
+ welcomeScreenSeen: false,
+ currentLocale: '',
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ showFiatInTestnets: false,
+ },
+ firstTimeFlowType: null,
+ completedOnboarding: false,
+ knownMethodData: {},
+ participateInMetaMetrics: null,
+ metaMetricsSendCount: 0,
+ }, state.metamask)
+
+ switch (action.type) {
+
+ case actions.SHOW_ACCOUNTS_PAGE:
+ newState = extend(metamaskState, {
+ isRevealingSeedWords: false,
+ })
+ delete newState.seedWords
+ return newState
+
+ case actions.SHOW_NOTICE:
+ return extend(metamaskState, {
+ noActiveNotices: false,
+ nextUnreadNotice: action.value,
+ })
+
+ case actions.CLEAR_NOTICES:
+ return extend(metamaskState, {
+ noActiveNotices: true,
+ nextUnreadNotice: undefined,
+ })
+
+ case actions.UPDATE_METAMASK_STATE:
+ return extend(metamaskState, action.value)
+
+ case actions.UNLOCK_METAMASK:
+ return extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: true,
+ selectedAddress: action.value,
+ })
+
+ case actions.LOCK_METAMASK:
+ return extend(metamaskState, {
+ isUnlocked: false,
+ })
+
+ case actions.SET_RPC_LIST:
+ return extend(metamaskState, {
+ frequentRpcList: action.value,
+ })
+
+ case actions.SET_RPC_TARGET:
+ return extend(metamaskState, {
+ provider: {
+ type: 'rpc',
+ rpcTarget: action.value,
+ },
+ })
+
+ case actions.SET_PROVIDER_TYPE:
+ return extend(metamaskState, {
+ provider: {
+ type: action.value,
+ },
+ })
+
+ case actions.COMPLETED_TX:
+ var stringId = String(action.id)
+ newState = extend(metamaskState, {
+ unapprovedTxs: {},
+ unapprovedMsgs: {},
+ })
+ for (const id in metamaskState.unapprovedTxs) {
+ if (id !== stringId) {
+ newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id]
+ }
+ }
+ for (const id in metamaskState.unapprovedMsgs) {
+ if (id !== stringId) {
+ newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id]
+ }
+ }
+ return newState
+
+ case actions.EDIT_TX:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ editingTransactionId: action.value,
+ },
+ })
+
+
+ case actions.SHOW_NEW_VAULT_SEED:
+ return extend(metamaskState, {
+ isRevealingSeedWords: true,
+ seedWords: action.value,
+ })
+
+ case actions.CLEAR_SEED_WORD_CACHE:
+ newState = extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: true,
+ selectedAddress: action.value,
+ })
+ delete newState.seedWords
+ return newState
+
+ case actions.SHOW_ACCOUNT_DETAIL:
+ newState = extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: true,
+ selectedAddress: action.value,
+ })
+ delete newState.seedWords
+ return newState
+
+ case actions.SET_SELECTED_TOKEN:
+ return extend(metamaskState, {
+ selectedTokenAddress: action.value,
+ })
+
+ case actions.SET_ACCOUNT_LABEL:
+ const account = action.value.account
+ const name = action.value.label
+ const id = {}
+ id[account] = extend(metamaskState.identities[account], { name })
+ const identities = extend(metamaskState.identities, id)
+ return extend(metamaskState, { identities })
+
+ case actions.SET_CURRENT_FIAT:
+ return extend(metamaskState, {
+ currentCurrency: action.value.currentCurrency,
+ conversionRate: action.value.conversionRate,
+ conversionDate: action.value.conversionDate,
+ })
+
+ case actions.UPDATE_TOKENS:
+ return extend(metamaskState, {
+ tokens: action.newTokens,
+ })
+
+ // metamask.send
+ case actions.UPDATE_GAS_LIMIT:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasLimit: action.value,
+ },
+ })
+
+ case actions.UPDATE_GAS_PRICE:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasPrice: action.value,
+ },
+ })
+
+ case actions.TOGGLE_ACCOUNT_MENU:
+ return extend(metamaskState, {
+ isAccountMenuOpen: !metamaskState.isAccountMenuOpen,
+ })
+
+ case actions.UPDATE_GAS_TOTAL:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasTotal: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_TOKEN_BALANCE:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ tokenBalance: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_HEX_DATA:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ data: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_FROM:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ from: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_TO:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ to: action.value.to,
+ toNickname: action.value.nickname,
+ },
+ })
+
+ case actions.UPDATE_SEND_AMOUNT:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ amount: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_MEMO:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ memo: action.value,
+ },
+ })
+
+ case actions.UPDATE_MAX_MODE:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ maxModeOn: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ ...action.value,
+ },
+ })
+
+ case actions.CLEAR_SEND:
+ return extend(metamaskState, {
+ send: {
+ gasLimit: null,
+ gasPrice: null,
+ gasTotal: null,
+ tokenBalance: null,
+ from: '',
+ to: '',
+ amount: '0x0',
+ memo: '',
+ errors: {},
+ maxModeOn: false,
+ editingTransactionId: null,
+ forceGasMin: null,
+ toNickname: '',
+ },
+ })
+
+ case actions.UPDATE_TRANSACTION_PARAMS:
+ const { id: txId, value } = action
+ let { selectedAddressTxList } = metamaskState
+ selectedAddressTxList = selectedAddressTxList.map(tx => {
+ if (tx.id === txId) {
+ tx.txParams = value
+ }
+ return tx
+ })
+
+ return extend(metamaskState, {
+ selectedAddressTxList,
+ })
+
+ case actions.PAIR_UPDATE:
+ const { value: { marketinfo: pairMarketInfo } } = action
+ return extend(metamaskState, {
+ tokenExchangeRates: {
+ ...metamaskState.tokenExchangeRates,
+ [pairMarketInfo.pair]: pairMarketInfo,
+ },
+ })
+
+ case actions.SHAPESHIFT_SUBVIEW:
+ const { value: { marketinfo: ssMarketInfo, coinOptions } } = action
+ return extend(metamaskState, {
+ tokenExchangeRates: {
+ ...metamaskState.tokenExchangeRates,
+ [ssMarketInfo.pair]: ssMarketInfo,
+ },
+ coinOptions,
+ })
+
+ case actions.SET_PARTICIPATE_IN_METAMETRICS:
+ return extend(metamaskState, {
+ participateInMetaMetrics: action.value,
+ })
+
+ case actions.SET_METAMETRICS_SEND_COUNT:
+ return extend(metamaskState, {
+ metaMetricsSendCount: action.value,
+ })
+
+ case actions.SET_USE_BLOCKIE:
+ return extend(metamaskState, {
+ useBlockie: action.value,
+ })
+
+ case actions.UPDATE_FEATURE_FLAGS:
+ return extend(metamaskState, {
+ featureFlags: action.value,
+ })
+
+ case actions.UPDATE_NETWORK_ENDPOINT_TYPE:
+ return extend(metamaskState, {
+ networkEndpointType: action.value,
+ })
+
+ case actions.CLOSE_WELCOME_SCREEN:
+ return extend(metamaskState, {
+ welcomeScreenSeen: true,
+ })
+
+ case actions.SET_CURRENT_LOCALE:
+ return extend(metamaskState, {
+ currentLocale: action.value,
+ })
+
+ case actions.SET_PENDING_TOKENS:
+ return extend(metamaskState, {
+ pendingTokens: { ...action.payload },
+ })
+
+ case actions.CLEAR_PENDING_TOKENS: {
+ return extend(metamaskState, {
+ pendingTokens: {},
+ })
+ }
+
+ case actions.UPDATE_PREFERENCES: {
+ return extend(metamaskState, {
+ preferences: {
+ ...metamaskState.preferences,
+ ...action.payload,
+ },
+ })
+ }
+
+ case actions.COMPLETE_ONBOARDING: {
+ return extend(metamaskState, {
+ completedOnboarding: true,
+ })
+ }
+
+ case actions.COMPLETE_UI_MIGRATION: {
+ return extend(metamaskState, {
+ completedUiMigration: true,
+ })
+ }
+
+ case actions.SET_FIRST_TIME_FLOW_TYPE: {
+ return extend(metamaskState, {
+ firstTimeFlowType: action.value,
+ })
+ }
+
+ default:
+ return metamaskState
+
+ }
+}
diff --git a/ui/app/ducks/mock-gas-estimate-data.js b/ui/app/ducks/mock-gas-estimate-data.js
deleted file mode 100644
index f2943df7c..000000000
--- a/ui/app/ducks/mock-gas-estimate-data.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- mockGasEstimateData: [{'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 6.2827225131, 'pct_remaining5m': 0.0, 'sum': 6.7965923077, 'tx_atabove': 3969.0, 'hashpower_accepting': 15.8974358974, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 1.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.1623036649, 'pct_remaining5m': 0.0, 'sum': 6.7841307692, 'tx_atabove': 3969.0, 'hashpower_accepting': 16.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 8.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.3403141361, 'pct_remaining5m': 0.0, 'sum': 6.7841307692, 'tx_atabove': 3969.0, 'hashpower_accepting': 16.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 13.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 7.3926701571, 'pct_remaining5m': 0.0, 'sum': 6.7841307692, 'tx_atabove': 3969.0, 'hashpower_accepting': 16.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 1.5, 'pct_mined_5m': 0.0, 'total_seen_5m': 40.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 356.0, 'hashpower_accepting2': 7.5706806283, 'pct_remaining5m': 25.0, 'sum': 6.7769307692, 'tx_atabove': 3957.0, 'hashpower_accepting': 16.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': 33.0, 'int2': 6.9238, 'pct_remaining30m': 9.0, 'gasprice': 1.6, 'pct_mined_5m': 0.0, 'total_seen_5m': 346.0, 'pct_mined_30m': 6.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1048.5, 'hashpower_accepting2': 7.6020942408, 'pct_remaining5m': 95.0, 'sum': 6.0111923077, 'tx_atabove': 2930.0, 'hashpower_accepting': 22.5641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 2.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 23.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 56.0, 'hashpower_accepting2': 7.6020942408, 'pct_remaining5m': 99.0, 'sum': 5.2227923077, 'tx_atabove': 1616.0, 'hashpower_accepting': 22.5641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 66.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 2.1, 'pct_mined_5m': 0.0, 'total_seen_5m': 131.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 41.0, 'hashpower_accepting2': 7.6020942408, 'pct_remaining5m': 100.0, 'sum': 4.8633923077, 'tx_atabove': 1017.0, 'hashpower_accepting': 22.5641025641, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.8, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 31.0, 'hashpower_accepting2': 7.612565445, 'pct_remaining5m': 100.0, 'sum': 4.8615923077, 'tx_atabove': 1014.0, 'hashpower_accepting': 22.5641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 2.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 147.0, 'hashpower_accepting2': 12.3246073298, 'pct_remaining5m': 50.0, 'sum': 4.6965923077, 'tx_atabove': 1009.0, 'hashpower_accepting': 29.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 50.0, 'gasprice': 3.0, 'pct_mined_5m': 50.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 50.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 73.0, 'hashpower_accepting2': 12.3246073298, 'pct_remaining5m': 50.0, 'sum': 4.6437923077, 'tx_atabove': 921.0, 'hashpower_accepting': 29.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 3.1, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 93.0, 'hashpower_accepting2': 12.3246073298, 'pct_remaining5m': 100.0, 'sum': 4.6413923077, 'tx_atabove': 917.0, 'hashpower_accepting': 29.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 3.2, 'pct_mined_5m': 0.0, 'total_seen_5m': 8.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 44.5, 'hashpower_accepting2': 12.3246073298, 'pct_remaining5m': 100.0, 'sum': 4.6107923077, 'tx_atabove': 866.0, 'hashpower_accepting': 29.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 50.0, 'gasprice': 3.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 256.68, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 11.0, 'hashpower_accepting2': 12.3664921466, 'pct_remaining5m': 100.0, 'sum': 4.5893307692, 'tx_atabove': 851.0, 'hashpower_accepting': 29.7435897436, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 3.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 25.27, 'avgdiff': 0, 'expectedWait': 98.4285367101, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 31.0, 'hashpower_accepting2': 12.4712041885, 'pct_remaining5m': 100.0, 'sum': 4.5887307692, 'tx_atabove': 850.0, 'hashpower_accepting': 29.7435897436, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 3.5, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 25.25, 'avgdiff': 0, 'expectedWait': 98.3694973017, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 823.0, 'hashpower_accepting2': 12.4921465969, 'pct_remaining5m': 0.0, 'sum': 4.5428076923, 'tx_atabove': 815.0, 'hashpower_accepting': 30.7692307692, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 3.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 24.12, 'avgdiff': 0, 'expectedWait': 93.9542246928, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 35.0, 'hashpower_accepting2': 12.7539267016, 'pct_remaining5m': 10.0, 'sum': 4.5279461538, 'tx_atabove': 811.0, 'hashpower_accepting': 31.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 3.9, 'pct_mined_5m': 80.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 23.76, 'avgdiff': 0, 'expectedWait': 92.5682447753, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 30.0, 'hashpower_accepting2': 15.4869109948, 'pct_remaining5m': 92.0, 'sum': 4.3896692308, 'tx_atabove': 809.0, 'hashpower_accepting': 36.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 16.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.0, 'pct_mined_5m': 5.0, 'total_seen_5m': 124.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 20.69, 'avgdiff': 0, 'expectedWait': 80.613750022, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 24.0, 'hashpower_accepting2': 16.7853403141, 'pct_remaining5m': 93.0, 'sum': 4.2840692308, 'tx_atabove': 633.0, 'hashpower_accepting': 36.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 20.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.1, 'pct_mined_5m': 6.0, 'total_seen_5m': 165.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 18.62, 'avgdiff': 0, 'expectedWait': 72.5350019424, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 126.0, 'hashpower_accepting2': 16.9005235602, 'pct_remaining5m': 50.0, 'sum': 4.1260846154, 'tx_atabove': 432.0, 'hashpower_accepting': 38.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 15.9, 'avgdiff': 0, 'expectedWait': 61.9349484316, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 33.0, 'hashpower_accepting2': 17.1204188482, 'pct_remaining5m': 100.0, 'sum': 4.1140846154, 'tx_atabove': 412.0, 'hashpower_accepting': 38.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 15.71, 'avgdiff': 0, 'expectedWait': 61.1961705828, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 24.0, 'hashpower_accepting2': 17.2460732984, 'pct_remaining5m': 100.0, 'sum': 4.1092846154, 'tx_atabove': 404.0, 'hashpower_accepting': 38.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 4.6, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 15.63, 'avgdiff': 0, 'expectedWait': 60.9031328173, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 19.0, 'hashpower_accepting2': 17.4659685864, 'pct_remaining5m': 0.0, 'sum': 4.0570384615, 'tx_atabove': 400.0, 'hashpower_accepting': 40.5128205128, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 4.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 14.84, 'avgdiff': 0, 'expectedWait': 57.8028719141, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 3.0, 'hashpower_accepting2': 17.612565445, 'pct_remaining5m': 0.0, 'sum': 4.0403769231, 'tx_atabove': 393.0, 'hashpower_accepting': 41.0256410256, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 14.59, 'avgdiff': 0, 'expectedWait': 56.8477660026, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 31.0, 'hashpower_accepting2': 17.6439790576, 'pct_remaining5m': 100.0, 'sum': 4.0397769231, 'tx_atabove': 392.0, 'hashpower_accepting': 41.0256410256, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 4.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 14.58, 'avgdiff': 0, 'expectedWait': 56.8136675736, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 3.0, 'hashpower_accepting2': 20.502617801, 'pct_remaining5m': 3.0, 'sum': 2.2680615385, 'tx_atabove': 390.0, 'hashpower_accepting': 46.1538461538, 'hpa_coef2': -0.067, 'total_seen_30m': 43.0, 'int2': 6.9238, 'pct_remaining30m': 2.0, 'gasprice': 5.0, 'pct_mined_5m': 93.0, 'total_seen_5m': 66.0, 'pct_mined_30m': 97.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.48, 'avgdiff': 1, 'expectedWait': 9.660655842, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 20.5863874346, 'pct_remaining5m': 0.0, 'sum': 2.2308615385, 'tx_atabove': 328.0, 'hashpower_accepting': 46.1538461538, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 5.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.39, 'avgdiff': 1, 'expectedWait': 9.3078817242, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 20.7015706806, 'pct_remaining5m': 0.0, 'sum': 2.216, 'tx_atabove': 324.0, 'hashpower_accepting': 46.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 5.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.35, 'avgdiff': 1, 'expectedWait': 9.170575103, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 20.8481675393, 'pct_remaining5m': 0.0, 'sum': 2.1910769231, 'tx_atabove': 324.0, 'hashpower_accepting': 47.6923076923, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 5.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.3, 'avgdiff': 1, 'expectedWait': 8.9448408351, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 21.0261780105, 'pct_remaining5m': 0.0, 'sum': 2.1898769231, 'tx_atabove': 322.0, 'hashpower_accepting': 47.6923076923, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 5.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.29, 'avgdiff': 1, 'expectedWait': 8.9341134638, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 21.2041884817, 'pct_remaining5m': 0.0, 'sum': 2.1518923077, 'tx_atabove': 321.0, 'hashpower_accepting': 49.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 5.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.21, 'avgdiff': 1, 'expectedWait': 8.6011189709, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 21.277486911, 'pct_remaining5m': 0.0, 'sum': 2.1512923077, 'tx_atabove': 320.0, 'hashpower_accepting': 49.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 5.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.21, 'avgdiff': 1, 'expectedWait': 8.5959598474, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 21.3926701571, 'pct_remaining5m': 0.0, 'sum': 2.1494923077, 'tx_atabove': 317.0, 'hashpower_accepting': 49.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 5.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.2, 'avgdiff': 1, 'expectedWait': 8.5805010368, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 21.4136125654, 'pct_remaining5m': 0.0, 'sum': 2.1494923077, 'tx_atabove': 317.0, 'hashpower_accepting': 49.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 5.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 2.2, 'avgdiff': 1, 'expectedWait': 8.5805010368, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 25.5497382199, 'pct_remaining5m': 0.0, 'sum': 2.0124153846, 'tx_atabove': 317.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': 44.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 118.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.92, 'avgdiff': 1, 'expectedWait': 7.4813659176, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.5916230366, 'pct_remaining5m': 0.0, 'sum': 2.0010153846, 'tx_atabove': 298.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.9, 'avgdiff': 1, 'expectedWait': 7.3965626432, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.7068062827, 'pct_remaining5m': 0.0, 'sum': 2.0004153846, 'tx_atabove': 297.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.9, 'avgdiff': 1, 'expectedWait': 7.3921260367, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.8848167539, 'pct_remaining5m': 0.0, 'sum': 1.9986153846, 'tx_atabove': 294.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.89, 'avgdiff': 1, 'expectedWait': 7.3788321779, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.8952879581, 'pct_remaining5m': 0.0, 'sum': 1.9986153846, 'tx_atabove': 294.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 6.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.89, 'avgdiff': 1, 'expectedWait': 7.3788321779, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.9685863874, 'pct_remaining5m': 0.0, 'sum': 1.9986153846, 'tx_atabove': 294.0, 'hashpower_accepting': 54.8717948718, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.89, 'avgdiff': 1, 'expectedWait': 7.3788321779, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 26.3769633508, 'pct_remaining5m': 0.0, 'sum': 1.9612307692, 'tx_atabove': 294.0, 'hashpower_accepting': 56.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 6.6, 'pct_mined_5m': 96.0, 'total_seen_5m': 29.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.82, 'avgdiff': 1, 'expectedWait': 7.1080700777, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 26.7120418848, 'pct_remaining5m': 0.0, 'sum': 1.9421692308, 'tx_atabove': 283.0, 'hashpower_accepting': 56.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.79, 'avgdiff': 1, 'expectedWait': 6.9738624916, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 26.9109947644, 'pct_remaining5m': 0.0, 'sum': 1.9421692308, 'tx_atabove': 283.0, 'hashpower_accepting': 56.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.79, 'avgdiff': 1, 'expectedWait': 6.9738624916, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 27.109947644, 'pct_remaining5m': 0.0, 'sum': 1.9415692308, 'tx_atabove': 282.0, 'hashpower_accepting': 56.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.79, 'avgdiff': 1, 'expectedWait': 6.9696794292, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 3.0, 'hashpower_accepting2': 29.2356020942, 'pct_remaining5m': 0.0, 'sum': 1.8294153846, 'tx_atabove': 282.0, 'hashpower_accepting': 61.5384615385, 'hpa_coef2': -0.067, 'total_seen_30m': 60.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 50.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.6, 'avgdiff': 1, 'expectedWait': 6.2302432976, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 29.780104712, 'pct_remaining5m': 0.0, 'sum': 1.8115538462, 'tx_atabove': 273.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 18.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1199495079, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 29.8848167539, 'pct_remaining5m': 0.0, 'sum': 1.8109538462, 'tx_atabove': 272.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1162786396, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 29.9476439791, 'pct_remaining5m': 0.0, 'sum': 1.8103538462, 'tx_atabove': 271.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 7.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1126099731, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 30.0209424084, 'pct_remaining5m': null, 'sum': 1.8085538462, 'tx_atabove': 268.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1016171717, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 30.1151832461, 'pct_remaining5m': 0.0, 'sum': 1.8085538462, 'tx_atabove': 268.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1016171717, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 30.2827225131, 'pct_remaining5m': 0.0, 'sum': 1.8085538462, 'tx_atabove': 268.0, 'hashpower_accepting': 62.0512820513, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.57, 'avgdiff': 1, 'expectedWait': 6.1016171717, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 30.8586387435, 'pct_remaining5m': 0.0, 'sum': 1.7681692308, 'tx_atabove': 263.0, 'hashpower_accepting': 63.5897435897, 'hpa_coef2': -0.067, 'total_seen_30m': 12.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.5, 'avgdiff': 1, 'expectedWait': 5.8601150164, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 30.9528795812, 'pct_remaining5m': 0.0, 'sum': 1.7681692308, 'tx_atabove': 263.0, 'hashpower_accepting': 63.5897435897, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 7.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.5, 'avgdiff': 1, 'expectedWait': 5.8601150164, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 31.3089005236, 'pct_remaining5m': 0.0, 'sum': 1.7432461538, 'tx_atabove': 263.0, 'hashpower_accepting': 64.6153846154, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 7.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.47, 'avgdiff': 1, 'expectedWait': 5.7158679264, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 33.2670157068, 'pct_remaining5m': 0.0, 'sum': 1.6928, 'tx_atabove': 262.0, 'hashpower_accepting': 66.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 65.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 38.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.39, 'avgdiff': 1, 'expectedWait': 5.4346765153, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 33.4869109948, 'pct_remaining5m': 0.0, 'sum': 1.6624769231, 'tx_atabove': 253.0, 'hashpower_accepting': 67.6923076923, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.35, 'avgdiff': 1, 'expectedWait': 5.2723538995, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 34.2617801047, 'pct_remaining5m': null, 'sum': 1.6494153846, 'tx_atabove': 252.0, 'hashpower_accepting': 68.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.34, 'avgdiff': 1, 'expectedWait': 5.2039366363, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 34.2827225131, 'pct_remaining5m': null, 'sum': 1.6494153846, 'tx_atabove': 252.0, 'hashpower_accepting': 68.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.34, 'avgdiff': 1, 'expectedWait': 5.2039366363, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 35.2356020942, 'pct_remaining5m': 0.0, 'sum': 1.6244923077, 'tx_atabove': 252.0, 'hashpower_accepting': 69.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 33.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.3, 'avgdiff': 1, 'expectedWait': 5.0758414173, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 35.2984293194, 'pct_remaining5m': null, 'sum': 1.6244923077, 'tx_atabove': 252.0, 'hashpower_accepting': 69.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.6, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.3, 'avgdiff': 1, 'expectedWait': 5.0758414173, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 35.3612565445, 'pct_remaining5m': null, 'sum': 1.6244923077, 'tx_atabove': 252.0, 'hashpower_accepting': 69.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.3, 'avgdiff': 1, 'expectedWait': 5.0758414173, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 37.6335078534, 'pct_remaining5m': 0.0, 'sum': 1.5372615385, 'tx_atabove': 252.0, 'hashpower_accepting': 72.8205128205, 'hpa_coef2': -0.067, 'total_seen_30m': 35.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 91.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.19, 'avgdiff': 1, 'expectedWait': 4.6518339443, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 38.0209424084, 'pct_remaining5m': 0.0, 'sum': 1.5336615385, 'tx_atabove': 246.0, 'hashpower_accepting': 72.8205128205, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.19, 'avgdiff': 1, 'expectedWait': 4.6351174498, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.0732984293, 'pct_remaining5m': 0.0, 'sum': 1.5206, 'tx_atabove': 245.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5749693534, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.1151832461, 'pct_remaining5m': 0.0, 'sum': 1.5206, 'tx_atabove': 245.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 9.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5749693534, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.1465968586, 'pct_remaining5m': null, 'sum': 1.5206, 'tx_atabove': 245.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5749693534, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.2722513089, 'pct_remaining5m': null, 'sum': 1.52, 'tx_atabove': 244.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.5, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5722251951, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.6701570681, 'pct_remaining5m': null, 'sum': 1.5194, 'tx_atabove': 243.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.6, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5694826829, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.6910994764, 'pct_remaining5m': 0.0, 'sum': 1.5188, 'tx_atabove': 242.0, 'hashpower_accepting': 73.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 9.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.17, 'avgdiff': 1, 'expectedWait': 4.5667418156, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 39.3403141361, 'pct_remaining5m': null, 'sum': 1.4979384615, 'tx_atabove': 228.0, 'hashpower_accepting': 73.8461538462, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.15, 'avgdiff': 1, 'expectedWait': 4.4724594129, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 39.7277486911, 'pct_remaining5m': 0.0, 'sum': 1.4979384615, 'tx_atabove': 228.0, 'hashpower_accepting': 73.8461538462, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 1.15, 'avgdiff': 1, 'expectedWait': 4.4724594129, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 44.2827225131, 'pct_remaining5m': 0.0, 'sum': 1.3472, 'tx_atabove': 226.0, 'hashpower_accepting': 80.0, 'hpa_coef2': -0.067, 'total_seen_30m': 126.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.0, 'pct_mined_5m': 98.0, 'total_seen_5m': 113.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.8466398462, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 44.4293193717, 'pct_remaining5m': 0.0, 'sum': 1.3448, 'tx_atabove': 222.0, 'hashpower_accepting': 80.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.8374189801, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 44.4921465969, 'pct_remaining5m': 0.0, 'sum': 1.3448, 'tx_atabove': 222.0, 'hashpower_accepting': 80.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.8374189801, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 44.502617801, 'pct_remaining5m': null, 'sum': 1.3448, 'tx_atabove': 222.0, 'hashpower_accepting': 80.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.8374189801, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 49.4240837696, 'pct_remaining5m': 0.0, 'sum': 1.2077230769, 'tx_atabove': 222.0, 'hashpower_accepting': 85.641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 169.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 95.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.3458577122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 49.4659685864, 'pct_remaining5m': 0.0, 'sum': 1.2077230769, 'tx_atabove': 222.0, 'hashpower_accepting': 85.641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.3458577122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 49.4869109948, 'pct_remaining5m': 0.0, 'sum': 1.2077230769, 'tx_atabove': 222.0, 'hashpower_accepting': 85.641025641, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 10.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.3458577122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 49.6858638743, 'pct_remaining5m': 0.0, 'sum': 1.2077230769, 'tx_atabove': 222.0, 'hashpower_accepting': 85.641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.3458577122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 50.0628272251, 'pct_remaining5m': 0.0, 'sum': 1.2071230769, 'tx_atabove': 221.0, 'hashpower_accepting': 85.641025641, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 11.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.3438507997, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 56.1884816754, 'pct_remaining5m': 0.0, 'sum': 1.1358153846, 'tx_atabove': 206.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 144.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 178.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.8, 'avgdiff': 1, 'expectedWait': 3.1137113805, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 56.7434554974, 'pct_remaining5m': 0.0, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 56.7748691099, 'pct_remaining5m': null, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 57.109947644, 'pct_remaining5m': 0.0, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 58.2931937173, 'pct_remaining5m': 0.0, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 58.3141361257, 'pct_remaining5m': null, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 58.3769633508, 'pct_remaining5m': 0.0, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 58.4293193717, 'pct_remaining5m': 0.0, 'sum': 1.1280153846, 'tx_atabove': 193.0, 'hashpower_accepting': 88.2051282051, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 11.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.79, 'avgdiff': 1, 'expectedWait': 3.089518905, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.7958115183, 'pct_remaining5m': 0.0, 'sum': 1.1030923077, 'tx_atabove': 193.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 50.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 87.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0134702079, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 61.1413612565, 'pct_remaining5m': 0.0, 'sum': 1.1018923077, 'tx_atabove': 191.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 15.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 8.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0098562125, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 61.1727748691, 'pct_remaining5m': 0.0, 'sum': 1.1018923077, 'tx_atabove': 191.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0098562125, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 61.1832460733, 'pct_remaining5m': 0.0, 'sum': 1.1018923077, 'tx_atabove': 191.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0098562125, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 61.1937172775, 'pct_remaining5m': 0.0, 'sum': 1.1018923077, 'tx_atabove': 191.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0098562125, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 62.1989528796, 'pct_remaining5m': 0.0, 'sum': 1.1018923077, 'tx_atabove': 191.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0098562125, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 62.3246073298, 'pct_remaining5m': 0.0, 'sum': 1.1006923077, 'tx_atabove': 189.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0062465513, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 62.3560209424, 'pct_remaining5m': 0.0, 'sum': 1.1000923077, 'tx_atabove': 188.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0044433444, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 62.7434554974, 'pct_remaining5m': 0.0, 'sum': 1.1000923077, 'tx_atabove': 188.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0044433444, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 62.7643979058, 'pct_remaining5m': 0.0, 'sum': 1.1000923077, 'tx_atabove': 188.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 12.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0044433444, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 65.5183246073, 'pct_remaining5m': 0.0, 'sum': 1.0988923077, 'tx_atabove': 186.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 43.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 22.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0008401747, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 65.612565445, 'pct_remaining5m': 0.0, 'sum': 1.0988923077, 'tx_atabove': 186.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 3.0008401747, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 65.7277486911, 'pct_remaining5m': 0.0, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 65.7382198953, 'pct_remaining5m': null, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.1832460733, 'pct_remaining5m': 0.0, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 18.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 11.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.2041884817, 'pct_remaining5m': 0.0, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 13.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.3717277487, 'pct_remaining5m': null, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.3926701571, 'pct_remaining5m': 0.0, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 13.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.4136125654, 'pct_remaining5m': 0.0, 'sum': 1.0982923077, 'tx_atabove': 185.0, 'hashpower_accepting': 89.2307692308, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.77, 'avgdiff': 1, 'expectedWait': 2.9990402106, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 69.8219895288, 'pct_remaining5m': 0.0, 'sum': 1.0727692308, 'tx_atabove': 184.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': 25.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 59.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9234640474, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.8743455497, 'pct_remaining5m': 0.0, 'sum': 1.0691692308, 'tx_atabove': 178.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 14.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9129584982, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.9057591623, 'pct_remaining5m': 0.0, 'sum': 1.0691692308, 'tx_atabove': 178.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 14.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9129584982, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.9581151832, 'pct_remaining5m': 0.0, 'sum': 1.0691692308, 'tx_atabove': 178.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 14.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9129584982, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.0104712042, 'pct_remaining5m': null, 'sum': 1.0691692308, 'tx_atabove': 178.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.6, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9129584982, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.1047120419, 'pct_remaining5m': 0.0, 'sum': 1.0691692308, 'tx_atabove': 178.0, 'hashpower_accepting': 90.2564102564, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.7, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9129584982, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 72.7434554974, 'pct_remaining5m': 0.0, 'sum': 1.0442461538, 'tx_atabove': 178.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 83.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 59.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8412558463, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 72.7853403141, 'pct_remaining5m': 0.0, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 72.8062827225, 'pct_remaining5m': null, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 72.8586387435, 'pct_remaining5m': 0.0, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 15.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 73.3298429319, 'pct_remaining5m': 0.0, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 16.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 73.3403141361, 'pct_remaining5m': 0.0, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 15.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.4397905759, 'pct_remaining5m': 0.0, 'sum': 1.0400461538, 'tx_atabove': 171.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 36.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 28.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.8293475966, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.4502617801, 'pct_remaining5m': 0.0, 'sum': 1.0340461538, 'tx_atabove': 161.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 16.1, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8124223376, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.4607329843, 'pct_remaining5m': 0.0, 'sum': 1.0340461538, 'tx_atabove': 161.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 16.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8124223376, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.502617801, 'pct_remaining5m': 0.0, 'sum': 1.0340461538, 'tx_atabove': 161.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.3, 'pct_mined_5m': 50.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8124223376, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.6806282723, 'pct_remaining5m': 0.0, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.7120418848, 'pct_remaining5m': 0.0, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 16.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 74.7434554974, 'pct_remaining5m': null, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.0785340314, 'pct_remaining5m': 0.0, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 17.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.1204188482, 'pct_remaining5m': 0.0, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.1413612565, 'pct_remaining5m': 0.0, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 17.7, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.1518324607, 'pct_remaining5m': null, 'sum': 1.0334461538, 'tx_atabove': 160.0, 'hashpower_accepting': 91.2820512821, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8107353903, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.4764397906, 'pct_remaining5m': 0.0, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.5183246073, 'pct_remaining5m': 0.0, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 18.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.5287958115, 'pct_remaining5m': null, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.5497382199, 'pct_remaining5m': 0.0, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.5811518325, 'pct_remaining5m': null, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.6230366492, 'pct_remaining5m': 0.0, 'sum': 1.0209846154, 'tx_atabove': 160.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7759266389, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.6753926702, 'pct_remaining5m': 0.0, 'sum': 1.0203846154, 'tx_atabove': 159.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7742615825, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.7068062827, 'pct_remaining5m': 0.0, 'sum': 1.0197846154, 'tx_atabove': 158.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 19.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7725975248, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.7172774869, 'pct_remaining5m': 0.0, 'sum': 1.0197846154, 'tx_atabove': 158.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 19.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7725975248, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 76.1570680628, 'pct_remaining5m': 0.0, 'sum': 1.0197846154, 'tx_atabove': 158.0, 'hashpower_accepting': 91.7948717949, 'hpa_coef2': -0.067, 'total_seen_30m': 16.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 12.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.7725975248, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 80.335078534, 'pct_remaining5m': 0.0, 'sum': 0.9076307692, 'tx_atabove': 158.0, 'hashpower_accepting': 96.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': 109.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 132.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.64, 'avgdiff': 1, 'expectedWait': 2.4784435671, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.3455497382, 'pct_remaining5m': 0.0, 'sum': 0.8446307692, 'tx_atabove': 53.0, 'hashpower_accepting': 96.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 20.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3271184122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.3769633508, 'pct_remaining5m': 0.0, 'sum': 0.8446307692, 'tx_atabove': 53.0, 'hashpower_accepting': 96.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3271184122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.3979057592, 'pct_remaining5m': 0.0, 'sum': 0.8446307692, 'tx_atabove': 53.0, 'hashpower_accepting': 96.4102564103, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 20.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3271184122, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 82.6596858639, 'pct_remaining5m': 0.0, 'sum': 0.8321692308, 'tx_atabove': 53.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 37.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 30.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2982988774, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 82.8586387435, 'pct_remaining5m': null, 'sum': 0.8303692308, 'tx_atabove': 50.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.1, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2941656605, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 82.9005235602, 'pct_remaining5m': 0.0, 'sum': 0.8297692308, 'tx_atabove': 49.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 21.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2927895739, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 82.9633507853, 'pct_remaining5m': 0.0, 'sum': 0.8297692308, 'tx_atabove': 49.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 21.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2927895739, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 82.9738219895, 'pct_remaining5m': null, 'sum': 0.8297692308, 'tx_atabove': 49.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2927895739, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 82.9947643979, 'pct_remaining5m': null, 'sum': 0.8297692308, 'tx_atabove': 49.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2927895739, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 83.2041884817, 'pct_remaining5m': 0.0, 'sum': 0.8297692308, 'tx_atabove': 49.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2927895739, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 83.2565445026, 'pct_remaining5m': null, 'sum': 0.8291692308, 'tx_atabove': 48.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.6, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2914143128, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 83.2670157068, 'pct_remaining5m': null, 'sum': 0.8291692308, 'tx_atabove': 48.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2914143128, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 83.9895287958, 'pct_remaining5m': 0.0, 'sum': 0.8291692308, 'tx_atabove': 48.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 28.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 23.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 15.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2914143128, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.0523560209, 'pct_remaining5m': null, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 23.1, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.0628272251, 'pct_remaining5m': 0.0, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 23.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.0837696335, 'pct_remaining5m': null, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 23.5, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.1570680628, 'pct_remaining5m': 0.0, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 24.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.167539267, 'pct_remaining5m': null, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 24.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.1780104712, 'pct_remaining5m': null, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 24.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.1884816754, 'pct_remaining5m': 0.0, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 24.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.3664921466, 'pct_remaining5m': 0.0, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 25.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.502617801, 'pct_remaining5m': 0.0, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 26.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.5340314136, 'pct_remaining5m': null, 'sum': 0.8285692308, 'tx_atabove': 47.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 27.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2900398766, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.7120418848, 'pct_remaining5m': 0.0, 'sum': 0.8279692308, 'tx_atabove': 46.0, 'hashpower_accepting': 96.9230769231, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 28.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.2886662648, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.9633507853, 'pct_remaining5m': 0.0, 'sum': 0.8155076923, 'tx_atabove': 46.0, 'hashpower_accepting': 97.4358974359, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 29.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.58, 'avgdiff': 1, 'expectedWait': 2.2603229297, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 85.664921466, 'pct_remaining5m': 0.0, 'sum': 0.8143076923, 'tx_atabove': 44.0, 'hashpower_accepting': 97.4358974359, 'hpa_coef2': -0.067, 'total_seen_30m': 17.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 30.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 26.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.58, 'avgdiff': 1, 'expectedWait': 2.2576121689, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 85.7696335079, 'pct_remaining5m': 0.0, 'sum': 0.8143076923, 'tx_atabove': 44.0, 'hashpower_accepting': 97.4358974359, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 31.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.58, 'avgdiff': 1, 'expectedWait': 2.2576121689, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.0628272251, 'pct_remaining5m': 0.0, 'sum': 0.7893846154, 'tx_atabove': 44.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 32.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.57, 'avgdiff': 1, 'expectedWait': 2.2020409071, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.2617801047, 'pct_remaining5m': 0.0, 'sum': 0.7887846154, 'tx_atabove': 43.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 33.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.2007200789, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.3560209424, 'pct_remaining5m': 0.0, 'sum': 0.7887846154, 'tx_atabove': 43.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 34.0, 'pct_mined_5m': 50.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.2007200789, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.5235602094, 'pct_remaining5m': 0.0, 'sum': 0.7887846154, 'tx_atabove': 43.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 35.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.2007200789, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.5654450262, 'pct_remaining5m': 0.0, 'sum': 0.7881846154, 'tx_atabove': 42.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 36.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1994000429, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.5863874346, 'pct_remaining5m': null, 'sum': 0.7881846154, 'tx_atabove': 42.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 37.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1994000429, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.6178010471, 'pct_remaining5m': 0.0, 'sum': 0.7881846154, 'tx_atabove': 42.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 38.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1994000429, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.6701570681, 'pct_remaining5m': null, 'sum': 0.7881846154, 'tx_atabove': 42.0, 'hashpower_accepting': 98.4615384615, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 39.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1994000429, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 87.3612565445, 'pct_remaining5m': 0.0, 'sum': 0.7757230769, 'tx_atabove': 42.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 13.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 40.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 19.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1721621998, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 89.8115183246, 'pct_remaining5m': 0.0, 'sum': 0.7751230769, 'tx_atabove': 41.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 65.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 41.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 84.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.1708592934, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.3141361257, 'pct_remaining5m': 0.0, 'sum': 0.7709230769, 'tx_atabove': 34.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 13.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 42.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 16.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1617608046, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.3455497382, 'pct_remaining5m': 0.0, 'sum': 0.7697230769, 'tx_atabove': 32.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 43.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1591682475, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.3769633508, 'pct_remaining5m': 0.0, 'sum': 0.7697230769, 'tx_atabove': 32.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 44.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1591682475, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 92.0314136126, 'pct_remaining5m': 0.0, 'sum': 0.7697230769, 'tx_atabove': 32.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 19.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 45.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 28.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1591682475, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 92.0628272251, 'pct_remaining5m': 0.0, 'sum': 0.7691230769, 'tx_atabove': 31.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 46.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1578731351, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 92.1047120419, 'pct_remaining5m': 0.0, 'sum': 0.7691230769, 'tx_atabove': 31.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 47.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1578731351, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 92.1570680628, 'pct_remaining5m': 0.0, 'sum': 0.7685230769, 'tx_atabove': 30.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 48.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1565787996, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 92.1884816754, 'pct_remaining5m': 0.0, 'sum': 0.7685230769, 'tx_atabove': 30.0, 'hashpower_accepting': 98.9743589744, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 49.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1565787996, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 95.5287958115, 'pct_remaining5m': 0.0, 'sum': 0.7436, 'tx_atabove': 30.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 87.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 50.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 54.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1034944803, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 95.7172774869, 'pct_remaining5m': 0.0, 'sum': 0.734, 'tx_atabove': 14.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 51.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0833975529, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 95.9057591623, 'pct_remaining5m': 0.0, 'sum': 0.734, 'tx_atabove': 14.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 52.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0833975529, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 95.9371727749, 'pct_remaining5m': 0.0, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 54.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 95.9895287958, 'pct_remaining5m': null, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 55.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 96.0418848168, 'pct_remaining5m': 0.0, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 57.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 96.0523560209, 'pct_remaining5m': null, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 58.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 96.0628272251, 'pct_remaining5m': 0.0, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 59.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 96.4083769634, 'pct_remaining5m': 0.0, 'sum': 0.7316, 'tx_atabove': 10.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 60.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 13.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0784033942, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 97.445026178, 'pct_remaining5m': 0.0, 'sum': 0.7298, 'tx_atabove': 7.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 21.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 61.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 14.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0746656331, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.1570680628, 'pct_remaining5m': 0.0, 'sum': 0.7292, 'tx_atabove': 6.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 12.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 63.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 15.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.073421207, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.3979057592, 'pct_remaining5m': 0.0, 'sum': 0.7286, 'tx_atabove': 5.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 64.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0721775275, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.4293193717, 'pct_remaining5m': 0.0, 'sum': 0.7286, 'tx_atabove': 5.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 65.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0721775275, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.6492146597, 'pct_remaining5m': 0.0, 'sum': 0.7286, 'tx_atabove': 5.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 66.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0721775275, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.6701570681, 'pct_remaining5m': 0.0, 'sum': 0.7286, 'tx_atabove': 5.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 67.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0721775275, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.6806282723, 'pct_remaining5m': null, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 68.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.7120418848, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 70.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.7958115183, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 71.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.8376963351, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 77.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.0261780105, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 80.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.0471204188, 'pct_remaining5m': null, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 81.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.057591623, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 85.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.0680628272, 'pct_remaining5m': null, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 86.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.1937172775, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 88.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.3717277487, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 90.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.4031413613, 'pct_remaining5m': null, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 91.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.4136125654, 'pct_remaining5m': null, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 94.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.4240837696, 'pct_remaining5m': null, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 98.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.5287958115, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 99.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.780104712, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 100.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.7905759162, 'pct_remaining5m': 0.0, 'sum': 0.7262, 'tx_atabove': 1.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 101.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0672102645, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.8848167539, 'pct_remaining5m': 0.0, 'sum': 0.7262, 'tx_atabove': 1.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 102.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0672102645, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.8952879581, 'pct_remaining5m': 0.0, 'sum': 0.7262, 'tx_atabove': 1.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 120.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0672102645, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.9371727749, 'pct_remaining5m': 0.0, 'sum': 0.7262, 'tx_atabove': 1.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 122.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0672102645, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.9790575916, 'pct_remaining5m': null, 'sum': 0.7256, 'tx_atabove': 0.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 134.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0659703104, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 100.0, 'pct_remaining5m': 0.0, 'sum': 0.7256, 'tx_atabove': 0.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 180.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 500, 'safelow': 500, 'nomine': 330, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0659703104, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}],
-}
diff --git a/ui/app/ducks/send/send-duck.test.js b/ui/app/ducks/send/send-duck.test.js
new file mode 100644
index 000000000..92c8dffd8
--- /dev/null
+++ b/ui/app/ducks/send/send-duck.test.js
@@ -0,0 +1,186 @@
+import assert from 'assert'
+
+import SendReducer, {
+ openToDropdown,
+ closeToDropdown,
+ updateSendErrors,
+ showGasButtonGroup,
+ hideGasButtonGroup,
+ updateSendWarnings,
+} from './send.duck.js'
+
+describe('Send Duck', () => {
+ const mockState = {
+ send: {
+ mockProp: 123,
+ },
+ }
+ const initState = {
+ fromDropdownOpen: false,
+ toDropdownOpen: false,
+ errors: {},
+ gasButtonGroupShown: true,
+ warnings: {},
+ }
+ const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN'
+ const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'
+ const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
+ const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
+ const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
+ const UPDATE_SEND_WARNINGS = 'metamask/send/UPDATE_SEND_WARNINGS'
+ const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
+ const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'
+ const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'
+
+ describe('SendReducer()', () => {
+ it('should initialize state', () => {
+ assert.deepEqual(
+ SendReducer({}),
+ initState
+ )
+ })
+
+ it('should return state unchanged if it does not match a dispatched actions type', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: 'someOtherAction',
+ value: 'someValue',
+ }),
+ Object.assign({}, mockState.send)
+ )
+ })
+
+ it('should set fromDropdownOpen to true when receiving a OPEN_FROM_DROPDOWN action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: OPEN_FROM_DROPDOWN,
+ }),
+ Object.assign({fromDropdownOpen: true}, mockState.send)
+ )
+ })
+
+ it('should return a new object (and not just modify the existing state object)', () => {
+ assert.deepEqual(SendReducer(mockState), mockState.send)
+ assert.notEqual(SendReducer(mockState), mockState.send)
+ })
+
+ it('should set fromDropdownOpen to false when receiving a CLOSE_FROM_DROPDOWN action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: CLOSE_FROM_DROPDOWN,
+ }),
+ Object.assign({fromDropdownOpen: false}, mockState.send)
+ )
+ })
+
+ it('should set toDropdownOpen to true when receiving a OPEN_TO_DROPDOWN action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: OPEN_TO_DROPDOWN,
+ }),
+ Object.assign({toDropdownOpen: true}, mockState.send)
+ )
+ })
+
+ it('should set toDropdownOpen to false when receiving a CLOSE_TO_DROPDOWN action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: CLOSE_TO_DROPDOWN,
+ }),
+ Object.assign({toDropdownOpen: false}, mockState.send)
+ )
+ })
+
+ it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => {
+ assert.deepEqual(
+ SendReducer(Object.assign({}, mockState, { gasButtonGroupShown: false }), {
+ type: SHOW_GAS_BUTTON_GROUP,
+ }),
+ Object.assign({gasButtonGroupShown: true}, mockState.send)
+ )
+ })
+
+ it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: HIDE_GAS_BUTTON_GROUP,
+ }),
+ Object.assign({gasButtonGroupShown: false}, mockState.send)
+ )
+ })
+
+ it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => {
+ const modifiedMockState = Object.assign({}, mockState, {
+ send: {
+ errors: {
+ someError: false,
+ },
+ },
+ })
+ assert.deepEqual(
+ SendReducer(modifiedMockState, {
+ type: UPDATE_SEND_ERRORS,
+ value: { someOtherError: true },
+ }),
+ Object.assign({}, modifiedMockState.send, {
+ errors: {
+ someError: false,
+ someOtherError: true,
+ },
+ })
+ )
+ })
+
+ it('should return the initial state in response to a RESET_SEND_STATE action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: RESET_SEND_STATE,
+ }),
+ Object.assign({}, initState)
+ )
+ })
+ })
+
+ describe('openToDropdown', () => {
+ assert.deepEqual(
+ openToDropdown(),
+ { type: OPEN_TO_DROPDOWN }
+ )
+ })
+
+ describe('closeToDropdown', () => {
+ assert.deepEqual(
+ closeToDropdown(),
+ { type: CLOSE_TO_DROPDOWN }
+ )
+ })
+
+ describe('showGasButtonGroup', () => {
+ assert.deepEqual(
+ showGasButtonGroup(),
+ { type: SHOW_GAS_BUTTON_GROUP }
+ )
+ })
+
+ describe('hideGasButtonGroup', () => {
+ assert.deepEqual(
+ hideGasButtonGroup(),
+ { type: HIDE_GAS_BUTTON_GROUP }
+ )
+ })
+
+ describe('updateSendErrors', () => {
+ assert.deepEqual(
+ updateSendErrors('mockErrorObject'),
+ { type: UPDATE_SEND_ERRORS, value: 'mockErrorObject' }
+ )
+ })
+
+ describe('updateSendWarnings', () => {
+ assert.deepEqual(
+ updateSendWarnings('mockWarningObject'),
+ { type: UPDATE_SEND_WARNINGS, value: 'mockWarningObject' }
+ )
+ })
+
+})
diff --git a/ui/app/ducks/send.duck.js b/ui/app/ducks/send/send.duck.js
index 90e92140b..90e92140b 100644
--- a/ui/app/ducks/send.duck.js
+++ b/ui/app/ducks/send/send.duck.js
diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js
deleted file mode 100644
index eceacd0bd..000000000
--- a/ui/app/ducks/tests/confirm-transaction.duck.test.js
+++ /dev/null
@@ -1,685 +0,0 @@
-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'
-
-const initialState = {
- txData: {},
- tokenData: {},
- methodData: {},
- tokenProps: {
- tokenDecimals: '',
- tokenSymbol: '',
- },
- fiatTransactionAmount: '',
- fiatTransactionFee: '',
- fiatTransactionTotal: '',
- ethTransactionAmount: '',
- ethTransactionFee: '',
- ethTransactionTotal: '',
- hexTransactionAmount: '',
- hexTransactionFee: '',
- hexTransactionTotal: '',
- nonce: '',
- toSmartContract: false,
- fetchingData: false,
-}
-
-const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
-const CLEAR_TX_DATA = 'metamask/confirm-transaction/CLEAR_TX_DATA'
-const UPDATE_TOKEN_DATA = 'metamask/confirm-transaction/UPDATE_TOKEN_DATA'
-const CLEAR_TOKEN_DATA = 'metamask/confirm-transaction/CLEAR_TOKEN_DATA'
-const UPDATE_METHOD_DATA = 'metamask/confirm-transaction/UPDATE_METHOD_DATA'
-const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA'
-const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS'
-const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES'
-const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS'
-const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
-const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
-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', () => {
- describe('State changes', () => {
- const mockState = {
- confirmTransaction: {
- txData: {
- id: 1,
- },
- tokenData: {
- name: 'abcToken',
- },
- methodData: {
- name: 'approve',
- },
- tokenProps: {
- tokenDecimals: '3',
- tokenSymbol: 'ABC',
- },
- fiatTransactionAmount: '469.26',
- fiatTransactionFee: '0.01',
- fiatTransactionTotal: '1.000021',
- ethTransactionAmount: '1',
- ethTransactionFee: '0.000021',
- ethTransactionTotal: '469.27',
- hexTransactionAmount: '',
- hexTransactionFee: '0x1319718a5000',
- hexTransactionTotal: '',
- nonce: '0x0',
- toSmartContract: false,
- fetchingData: false,
- },
- }
-
- it('should initialize state', () => {
- assert.deepEqual(
- ConfirmTransactionReducer({}),
- initialState
- )
- })
-
- it('should return state unchanged if it does not match a dispatched actions type', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: 'someOtherAction',
- value: 'someValue',
- }),
- { ...mockState.confirmTransaction },
- )
- })
-
- it('should set txData when receiving a UPDATE_TX_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TX_DATA,
- payload: {
- id: 2,
- },
- }),
- {
- ...mockState.confirmTransaction,
- txData: {
- ...mockState.confirmTransaction.txData,
- id: 2,
- },
- }
- )
- })
-
- it('should clear txData when receiving a CLEAR_TX_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: CLEAR_TX_DATA,
- }),
- {
- ...mockState.confirmTransaction,
- txData: {},
- }
- )
- })
-
- it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TOKEN_DATA,
- payload: {
- name: 'defToken',
- },
- }),
- {
- ...mockState.confirmTransaction,
- tokenData: {
- ...mockState.confirmTransaction.tokenData,
- name: 'defToken',
- },
- }
- )
- })
-
- it('should clear tokenData when receiving a CLEAR_TOKEN_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: CLEAR_TOKEN_DATA,
- }),
- {
- ...mockState.confirmTransaction,
- tokenData: {},
- }
- )
- })
-
- it('should set methodData when receiving a UPDATE_METHOD_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_METHOD_DATA,
- payload: {
- name: 'transferFrom',
- },
- }),
- {
- ...mockState.confirmTransaction,
- methodData: {
- ...mockState.confirmTransaction.methodData,
- name: 'transferFrom',
- },
- }
- )
- })
-
- it('should clear methodData when receiving a CLEAR_METHOD_DATA action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: CLEAR_METHOD_DATA,
- }),
- {
- ...mockState.confirmTransaction,
- methodData: {},
- }
- )
- })
-
- it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TRANSACTION_AMOUNTS,
- payload: {
- fiatTransactionAmount: '123.45',
- ethTransactionAmount: '.5',
- hexTransactionAmount: '0x1',
- },
- }),
- {
- ...mockState.confirmTransaction,
- fiatTransactionAmount: '123.45',
- ethTransactionAmount: '.5',
- hexTransactionAmount: '0x1',
- }
- )
- })
-
- it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TRANSACTION_FEES,
- payload: {
- fiatTransactionFee: '123.45',
- ethTransactionFee: '.5',
- hexTransactionFee: '0x1',
- },
- }),
- {
- ...mockState.confirmTransaction,
- fiatTransactionFee: '123.45',
- ethTransactionFee: '.5',
- hexTransactionFee: '0x1',
- }
- )
- })
-
- it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TRANSACTION_TOTALS,
- payload: {
- fiatTransactionTotal: '123.45',
- ethTransactionTotal: '.5',
- hexTransactionTotal: '0x1',
- },
- }),
- {
- ...mockState.confirmTransaction,
- fiatTransactionTotal: '123.45',
- ethTransactionTotal: '.5',
- hexTransactionTotal: '0x1',
- }
- )
- })
-
- it('should update tokenProps when receiving an UPDATE_TOKEN_PROPS action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TOKEN_PROPS,
- payload: {
- tokenSymbol: 'DEF',
- tokenDecimals: '1',
- },
- }),
- {
- ...mockState.confirmTransaction,
- tokenProps: {
- tokenSymbol: 'DEF',
- tokenDecimals: '1',
- },
- }
- )
- })
-
- it('should update nonce when receiving an UPDATE_NONCE action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_NONCE,
- payload: '0x1',
- }),
- {
- ...mockState.confirmTransaction,
- nonce: '0x1',
- }
- )
- })
-
- it('should update nonce when receiving an UPDATE_TO_SMART_CONTRACT action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: UPDATE_TO_SMART_CONTRACT,
- payload: true,
- }),
- {
- ...mockState.confirmTransaction,
- toSmartContract: true,
- }
- )
- })
-
- it('should set fetchingData to true when receiving a FETCH_DATA_START action', () => {
- assert.deepEqual(
- ConfirmTransactionReducer(mockState, {
- type: FETCH_DATA_START,
- }),
- {
- ...mockState.confirmTransaction,
- fetchingData: true,
- }
- )
- })
-
- 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,
- }),
- {
- ...initialState,
- }
- )
- })
- })
-
- describe('Single actions', () => {
- it('should create an action to update txData', () => {
- const txData = { test: 123 }
- const expectedAction = {
- type: UPDATE_TX_DATA,
- payload: txData,
- }
-
- assert.deepEqual(
- actions.updateTxData(txData),
- expectedAction
- )
- })
-
- it('should create an action to clear txData', () => {
- const expectedAction = {
- type: CLEAR_TX_DATA,
- }
-
- assert.deepEqual(
- actions.clearTxData(),
- expectedAction
- )
- })
-
- it('should create an action to update tokenData', () => {
- const tokenData = { test: 123 }
- const expectedAction = {
- type: UPDATE_TOKEN_DATA,
- payload: tokenData,
- }
-
- assert.deepEqual(
- actions.updateTokenData(tokenData),
- expectedAction
- )
- })
-
- it('should create an action to clear tokenData', () => {
- const expectedAction = {
- type: CLEAR_TOKEN_DATA,
- }
-
- assert.deepEqual(
- actions.clearTokenData(),
- expectedAction
- )
- })
-
- it('should create an action to update methodData', () => {
- const methodData = { test: 123 }
- const expectedAction = {
- type: UPDATE_METHOD_DATA,
- payload: methodData,
- }
-
- assert.deepEqual(
- actions.updateMethodData(methodData),
- expectedAction
- )
- })
-
- it('should create an action to clear methodData', () => {
- const expectedAction = {
- type: CLEAR_METHOD_DATA,
- }
-
- assert.deepEqual(
- actions.clearMethodData(),
- expectedAction
- )
- })
-
- it('should create an action to update transaction amounts', () => {
- const transactionAmounts = { test: 123 }
- const expectedAction = {
- type: UPDATE_TRANSACTION_AMOUNTS,
- payload: transactionAmounts,
- }
-
- assert.deepEqual(
- actions.updateTransactionAmounts(transactionAmounts),
- expectedAction
- )
- })
-
- it('should create an action to update transaction fees', () => {
- const transactionFees = { test: 123 }
- const expectedAction = {
- type: UPDATE_TRANSACTION_FEES,
- payload: transactionFees,
- }
-
- assert.deepEqual(
- actions.updateTransactionFees(transactionFees),
- expectedAction
- )
- })
-
- it('should create an action to update transaction totals', () => {
- const transactionTotals = { test: 123 }
- const expectedAction = {
- type: UPDATE_TRANSACTION_TOTALS,
- payload: transactionTotals,
- }
-
- assert.deepEqual(
- actions.updateTransactionTotals(transactionTotals),
- expectedAction
- )
- })
-
- it('should create an action to update tokenProps', () => {
- const tokenProps = {
- tokenDecimals: '1',
- tokenSymbol: 'abc',
- }
- const expectedAction = {
- type: UPDATE_TOKEN_PROPS,
- payload: tokenProps,
- }
-
- assert.deepEqual(
- actions.updateTokenProps(tokenProps),
- expectedAction
- )
- })
-
- it('should create an action to update nonce', () => {
- const nonce = '0x1'
- const expectedAction = {
- type: UPDATE_NONCE,
- payload: nonce,
- }
-
- assert.deepEqual(
- actions.updateNonce(nonce),
- expectedAction
- )
- })
-
- it('should create an action to set fetchingData to true', () => {
- const expectedAction = {
- type: FETCH_DATA_START,
- }
-
- assert.deepEqual(
- actions.setFetchingData(true),
- expectedAction
- )
- })
-
- it('should create an action to set fetchingData to false', () => {
- const expectedAction = {
- type: FETCH_DATA_END,
- }
-
- assert.deepEqual(
- actions.setFetchingData(false),
- expectedAction
- )
- })
-
- it('should create an action to clear confirmTransaction', () => {
- const expectedAction = {
- type: CLEAR_CONFIRM_TRANSACTION,
- }
-
- assert.deepEqual(
- actions.clearConfirmTransaction(),
- expectedAction
- )
- })
- })
-
- 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: {
- conversionRate: 468.58,
- currentCurrency: 'usd',
- },
- confirmTransaction: {
- ethTransactionAmount: '1',
- ethTransactionFee: '0.000021',
- ethTransactionTotal: '1.000021',
- fetchingData: false,
- fiatTransactionAmount: '469.26',
- fiatTransactionFee: '0.01',
- fiatTransactionTotal: '469.27',
- hexGasTotal: '0x1319718a5000',
- methodData: {},
- nonce: '',
- tokenData: {},
- tokenProps: {
- tokenDecimals: '',
- tokenSymbol: '',
- },
- txData: {
- estimatedGas: '0x5208',
- gasLimitSpecified: false,
- gasPriceSpecified: false,
- history: [],
- id: 2603411941761054,
- loadingDefaults: false,
- metamaskNetworkId: '3',
- origin: 'faucet.metamask.io',
- simpleSend: true,
- status: 'unapproved',
- time: 1530838113716,
- },
- },
- }
-
- const middlewares = [thunk]
- const mockStore = configureMockStore(middlewares)
- const store = mockStore(mockState)
- const expectedActions = [
- 'metamask/confirm-transaction/UPDATE_TX_DATA',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
- ]
-
- store.dispatch(actions.updateGasAndCalculate({ gasLimit: '0x2', gasPrice: '0x25' }))
-
- const storeActions = store.getActions()
- assert.equal(storeActions.length, expectedActions.length)
- storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
- })
-
- it('updates txData and updates gas values in confirmTransaction', () => {
- const txData = {
- estimatedGas: '0x5208',
- gasLimitSpecified: false,
- gasPriceSpecified: false,
- history: [],
- id: 2603411941761054,
- loadingDefaults: false,
- metamaskNetworkId: '3',
- origin: 'faucet.metamask.io',
- simpleSend: true,
- status: 'unapproved',
- time: 1530838113716,
- txParams: {
- from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
- gas: '0x33450',
- gasPrice: '0x2540be400',
- to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
- value: '0xde0b6b3a7640000',
- },
- }
- const mockState = {
- metamask: {
- conversionRate: 468.58,
- currentCurrency: 'usd',
- },
- confirmTransaction: {
- ethTransactionAmount: '1',
- ethTransactionFee: '0.000021',
- ethTransactionTotal: '1.000021',
- fetchingData: false,
- fiatTransactionAmount: '469.26',
- fiatTransactionFee: '0.01',
- fiatTransactionTotal: '469.27',
- hexGasTotal: '0x1319718a5000',
- methodData: {},
- nonce: '',
- tokenData: {},
- tokenProps: {
- tokenDecimals: '',
- tokenSymbol: '',
- },
- txData: {
- ...txData,
- txParams: {
- ...txData.txParams,
- },
- },
- },
- }
-
- const middlewares = [thunk]
- const mockStore = configureMockStore(middlewares)
- const store = mockStore(mockState)
- const expectedActions = [
- 'metamask/confirm-transaction/UPDATE_TX_DATA',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
- ]
-
- store.dispatch(actions.updateTxDataAndCalculate(txData))
-
- const storeActions = store.getActions()
- assert.equal(storeActions.length, expectedActions.length)
- storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
- })
-
- it('updates confirmTransaction transaction', done => {
- const mockState = {
- metamask: {
- conversionRate: 468.58,
- currentCurrency: 'usd',
- network: '3',
- unapprovedTxs: {
- 2603411941761054: {
- estimatedGas: '0x5208',
- gasLimitSpecified: false,
- gasPriceSpecified: false,
- history: [],
- id: 2603411941761054,
- loadingDefaults: false,
- metamaskNetworkId: '3',
- origin: 'faucet.metamask.io',
- simpleSend: true,
- status: 'unapproved',
- time: 1530838113716,
- txParams: {
- from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
- gas: '0x33450',
- gasPrice: '0x2540be400',
- to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
- value: '0xde0b6b3a7640000',
- },
- },
- },
- },
- confirmTransaction: {},
- }
-
- const middlewares = [thunk]
- const mockStore = configureMockStore(middlewares)
- const store = mockStore(mockState)
- const expectedActions = [
- 'metamask/confirm-transaction/UPDATE_TX_DATA',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
- 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
- ]
-
- store.dispatch(actions.setTransactionToConfirm(2603411941761054))
- .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/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js
deleted file mode 100644
index cd963aed4..000000000
--- a/ui/app/ducks/tests/gas-duck.test.js
+++ /dev/null
@@ -1,600 +0,0 @@
-import assert from 'assert'
-import sinon from 'sinon'
-import proxyquire from 'proxyquire'
-
-
-const GasDuck = proxyquire('../gas.duck.js', {
- '../../lib/local-storage-helpers': {
- loadLocalStorageData: sinon.spy(),
- saveLocalStorageData: sinon.spy(),
- },
-})
-
-const {
- basicGasEstimatesLoadingStarted,
- basicGasEstimatesLoadingFinished,
- setBasicGasEstimateData,
- setCustomGasPrice,
- setCustomGasLimit,
- setCustomGasTotal,
- setCustomGasErrors,
- resetCustomGasState,
- fetchBasicGasAndTimeEstimates,
- fetchBasicGasEstimates,
- gasEstimatesLoadingStarted,
- gasEstimatesLoadingFinished,
- setPricesAndTimeEstimates,
- fetchGasEstimates,
- setApiEstimatesLastRetrieved,
-} = GasDuck
-const GasReducer = GasDuck.default
-
-describe('Gas Duck', () => {
- let tempFetch
- let tempDateNow
- const mockEthGasApiResponse = {
- average: 20,
- avgWait: 'mockAvgWait',
- block_time: 'mockBlock_time',
- blockNum: 'mockBlockNum',
- fast: 30,
- fastest: 40,
- fastestWait: 'mockFastestWait',
- fastWait: 'mockFastWait',
- safeLow: 10,
- safeLowWait: 'mockSafeLowWait',
- speed: 'mockSpeed',
- standard: 20,
- }
- const mockPredictTableResponse = [
- { expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' },
- { expectedTime: 200, expectedWait: 20, gasprice: 0.5, somethingElse: 'foobar' },
- { expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' },
- { expectedTime: 75, expectedWait: 7.5, gasprice: 1.5, somethingElse: 'foobar' },
- { expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' },
- { expectedTime: 35, expectedWait: 4.5, gasprice: 3, somethingElse: 'foobar' },
- { expectedTime: 34, expectedWait: 4.4, gasprice: 3.1, somethingElse: 'foobar' },
- { expectedTime: 25, expectedWait: 4.2, gasprice: 3.5, somethingElse: 'foobar' },
- { expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' },
- { expectedTime: 19, expectedWait: 3.9, gasprice: 4.1, somethingElse: 'foobar' },
- { expectedTime: 15, expectedWait: 3, gasprice: 7, somethingElse: 'foobar' },
- { expectedTime: 14, expectedWait: 2.9, gasprice: 7.1, somethingElse: 'foobar' },
- { expectedTime: 12, expectedWait: 2.5, gasprice: 8, somethingElse: 'foobar' },
- { expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' },
- { expectedTime: 9, expectedWait: 1.9, gasprice: 10.1, somethingElse: 'foobar' },
- { expectedTime: 5, expectedWait: 1, gasprice: 15, somethingElse: 'foobar' },
- { expectedTime: 4, expectedWait: 0.9, gasprice: 15.1, somethingElse: 'foobar' },
- { expectedTime: 2, expectedWait: 0.8, gasprice: 17, somethingElse: 'foobar' },
- { expectedTime: 1.1, expectedWait: 0.6, gasprice: 19.9, somethingElse: 'foobar' },
- { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' },
- ]
- const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => {
- const dataToResolve = url.match(/ethgasAPI|gasexpress/)
- ? mockEthGasApiResponse
- : mockPredictTableResponse
- resolve({
- json: () => new Promise(resolve => resolve(dataToResolve)),
- })
- }))
-
- beforeEach(() => {
- tempFetch = global.fetch
- tempDateNow = global.Date.now
- global.fetch = fetchStub
- global.Date.now = () => 2000000
- })
-
- afterEach(() => {
- fetchStub.resetHistory()
- global.fetch = tempFetch
- global.Date.now = tempDateNow
- })
-
- const mockState = {
- gas: {
- mockProp: 123,
- },
- }
- const initState = {
- customData: {
- price: null,
- limit: null,
- },
- basicEstimates: {
- average: null,
- fastestWait: null,
- fastWait: null,
- fast: null,
- safeLowWait: null,
- blockNum: null,
- avgWait: null,
- blockTime: null,
- speed: null,
- fastest: null,
- safeLow: null,
- },
- basicEstimateIsLoading: true,
- errors: {},
- gasEstimatesLoading: true,
- priceAndTimeEstimates: [],
- priceAndTimeEstimatesLastRetrieved: 0,
- basicPriceAndTimeEstimates: [],
- basicPriceAndTimeEstimatesLastRetrieved: 0,
- basicPriceEstimatesLastRetrieved: 0,
- }
- const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
- const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
- const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
- const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
- const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
- const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
- const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
- const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
- const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
- const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
- const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
- const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
- const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
- const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED'
-
- describe('GasReducer()', () => {
- it('should initialize state', () => {
- assert.deepEqual(
- GasReducer({}),
- initState
- )
- })
-
- it('should return state unchanged if it does not match a dispatched actions type', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: 'someOtherAction',
- value: 'someValue',
- }),
- Object.assign({}, mockState.gas)
- )
- })
-
- it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
- }),
- Object.assign({basicEstimateIsLoading: true}, mockState.gas)
- )
- })
-
- it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
- }),
- Object.assign({basicEstimateIsLoading: false}, mockState.gas)
- )
- })
-
- it('should set gasEstimatesLoading to true when receiving a GAS_ESTIMATE_LOADING_STARTED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: GAS_ESTIMATE_LOADING_STARTED,
- }),
- Object.assign({gasEstimatesLoading: true}, mockState.gas)
- )
- })
-
- it('should set gasEstimatesLoading to false when receiving a GAS_ESTIMATE_LOADING_FINISHED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: GAS_ESTIMATE_LOADING_FINISHED,
- }),
- Object.assign({gasEstimatesLoading: false}, mockState.gas)
- )
- })
-
- it('should return a new object (and not just modify the existing state object)', () => {
- assert.deepEqual(GasReducer(mockState), mockState.gas)
- assert.notEqual(GasReducer(mockState), mockState.gas)
- })
-
- it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_BASIC_GAS_ESTIMATE_DATA,
- value: { someProp: 'someData123' },
- }),
- Object.assign({basicEstimates: {someProp: 'someData123'} }, mockState.gas)
- )
- })
-
- it('should set priceAndTimeEstimates when receiving a SET_PRICE_AND_TIME_ESTIMATES action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_PRICE_AND_TIME_ESTIMATES,
- value: { someProp: 'someData123' },
- }),
- Object.assign({priceAndTimeEstimates: {someProp: 'someData123'} }, mockState.gas)
- )
- })
-
- it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_CUSTOM_GAS_PRICE,
- value: 4321,
- }),
- Object.assign({customData: {price: 4321} }, mockState.gas)
- )
- })
-
- it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_CUSTOM_GAS_LIMIT,
- value: 9876,
- }),
- Object.assign({customData: {limit: 9876} }, mockState.gas)
- )
- })
-
- it('should set customData.total when receiving a SET_CUSTOM_GAS_TOTAL action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_CUSTOM_GAS_TOTAL,
- value: 10000,
- }),
- Object.assign({customData: {total: 10000} }, mockState.gas)
- )
- })
-
- it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_API_ESTIMATES_LAST_RETRIEVED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_API_ESTIMATES_LAST_RETRIEVED,
- value: 1500000000000,
- }),
- Object.assign({ priceAndTimeEstimatesLastRetrieved: 1500000000000 }, mockState.gas)
- )
- })
-
- it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_BASIC_API_ESTIMATES_LAST_RETRIEVED action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
- value: 1700000000000,
- }),
- Object.assign({ basicPriceAndTimeEstimatesLastRetrieved: 1700000000000 }, mockState.gas)
- )
- })
-
- it('should set errors when receiving a SET_CUSTOM_GAS_ERRORS action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: SET_CUSTOM_GAS_ERRORS,
- value: { someError: 'error_error' },
- }),
- Object.assign({errors: {someError: 'error_error'} }, mockState.gas)
- )
- })
-
- it('should return the initial state in response to a RESET_CUSTOM_GAS_STATE action', () => {
- assert.deepEqual(
- GasReducer(mockState, {
- type: RESET_CUSTOM_GAS_STATE,
- }),
- Object.assign({}, initState)
- )
- })
- })
-
- describe('basicGasEstimatesLoadingStarted', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- basicGasEstimatesLoadingStarted(),
- { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }
- )
- })
- })
-
- describe('basicGasEstimatesLoadingFinished', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- basicGasEstimatesLoadingFinished(),
- { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }
- )
- })
- })
-
- describe('fetchBasicGasEstimates', () => {
- const mockDistpatch = sinon.spy()
- it('should call fetch with the expected params', async () => {
- await fetchBasicGasEstimates()(mockDistpatch, () => ({ gas: Object.assign(
- {},
- initState,
- { basicPriceAEstimatesLastRetrieved: 1000000 }
- ) }))
- assert.deepEqual(
- mockDistpatch.getCall(0).args,
- [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
- )
- assert.deepEqual(
- global.fetch.getCall(0).args,
- [
- 'https://dev.blockscale.net/api/gasexpress.json',
- {
- 'headers': {},
- 'referrer': 'https://dev.blockscale.net/api/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors',
- },
- ]
- )
-
- assert.deepEqual(
- mockDistpatch.getCall(1).args,
- [{ type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ]
- )
-
- assert.deepEqual(
- mockDistpatch.getCall(2).args,
- [{
- type: SET_BASIC_GAS_ESTIMATE_DATA,
- value: {
- average: 20,
- blockTime: 'mockBlock_time',
- blockNum: 'mockBlockNum',
- fast: 30,
- fastest: 40,
- safeLow: 10,
- },
- }]
- )
- assert.deepEqual(
- mockDistpatch.getCall(3).args,
- [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }]
- )
- })
- })
-
- describe('fetchBasicGasAndTimeEstimates', () => {
- const mockDistpatch = sinon.spy()
- it('should call fetch with the expected params', async () => {
- await fetchBasicGasAndTimeEstimates()(mockDistpatch, () => ({ gas: Object.assign(
- {},
- initState,
- { basicPriceAndTimeEstimatesLastRetrieved: 1000000 }
- ) }))
- assert.deepEqual(
- mockDistpatch.getCall(0).args,
- [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
- )
- assert.deepEqual(
- global.fetch.getCall(0).args,
- [
- 'https://ethgasstation.info/json/ethgasAPI.json',
- {
- 'headers': {},
- 'referrer': 'http://ethgasstation.info/json/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors',
- },
- ]
- )
-
- assert.deepEqual(
- mockDistpatch.getCall(1).args,
- [{ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ]
- )
-
- assert.deepEqual(
- mockDistpatch.getCall(2).args,
- [{
- type: SET_BASIC_GAS_ESTIMATE_DATA,
- value: {
- average: 2,
- avgWait: 'mockAvgWait',
- blockTime: 'mockBlock_time',
- blockNum: 'mockBlockNum',
- fast: 3,
- fastest: 4,
- fastestWait: 'mockFastestWait',
- fastWait: 'mockFastWait',
- safeLow: 1,
- safeLowWait: 'mockSafeLowWait',
- speed: 'mockSpeed',
- },
- }]
- )
- assert.deepEqual(
- mockDistpatch.getCall(3).args,
- [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }]
- )
- })
- })
-
- describe('fetchGasEstimates', () => {
- const mockDistpatch = sinon.spy()
-
- beforeEach(() => {
- mockDistpatch.resetHistory()
- })
-
- it('should call fetch with the expected params', async () => {
- global.fetch.resetHistory()
- await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
- {},
- initState,
- { priceAndTimeEstimatesLastRetrieved: 1000000 }
- ) }))
- assert.deepEqual(
- mockDistpatch.getCall(0).args,
- [{ type: GAS_ESTIMATE_LOADING_STARTED} ]
- )
- assert.deepEqual(
- global.fetch.getCall(0).args,
- [
- 'https://ethgasstation.info/json/predictTable.json',
- {
- 'headers': {},
- 'referrer': 'http://ethgasstation.info/json/',
- 'referrerPolicy': 'no-referrer-when-downgrade',
- 'body': null,
- 'method': 'GET',
- 'mode': 'cors',
- },
- ]
- )
-
- assert.deepEqual(
- mockDistpatch.getCall(1).args,
- [{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }]
- )
-
- const { type: thirdDispatchCallType, value: priceAndTimeEstimateResult } = mockDistpatch.getCall(2).args[0]
- assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES)
- assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2)
- assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100))
- assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime))
- assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice))
-
- assert.deepEqual(
- mockDistpatch.getCall(3).args,
- [{ type: GAS_ESTIMATE_LOADING_FINISHED }]
- )
- })
-
- it('should not call fetch if the estimates were retrieved < 75000 ms ago', async () => {
- global.fetch.resetHistory()
- await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
- {},
- initState,
- {
- priceAndTimeEstimatesLastRetrieved: Date.now(),
- priceAndTimeEstimates: [{
- expectedTime: '10',
- expectedWait: 2,
- gasprice: 50,
- }],
- }
- ) }))
- assert.deepEqual(
- mockDistpatch.getCall(0).args,
- [{ type: GAS_ESTIMATE_LOADING_STARTED} ]
- )
- assert.equal(global.fetch.callCount, 0)
-
- assert.deepEqual(
- mockDistpatch.getCall(1).args,
- [{
- type: SET_PRICE_AND_TIME_ESTIMATES,
- value: [
- {
- expectedTime: '10',
- expectedWait: 2,
- gasprice: 50,
- },
- ],
-
- }]
- )
- assert.deepEqual(
- mockDistpatch.getCall(2).args,
- [{ type: GAS_ESTIMATE_LOADING_FINISHED }]
- )
- })
- })
-
- describe('gasEstimatesLoadingStarted', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- gasEstimatesLoadingStarted(),
- { type: GAS_ESTIMATE_LOADING_STARTED }
- )
- })
- })
-
- describe('gasEstimatesLoadingFinished', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- gasEstimatesLoadingFinished(),
- { type: GAS_ESTIMATE_LOADING_FINISHED }
- )
- })
- })
-
- describe('setPricesAndTimeEstimates', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setPricesAndTimeEstimates('mockPricesAndTimeEstimates'),
- { type: SET_PRICE_AND_TIME_ESTIMATES, value: 'mockPricesAndTimeEstimates' }
- )
- })
- })
-
- describe('setBasicGasEstimateData', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setBasicGasEstimateData('mockBasicEstimatData'),
- { type: SET_BASIC_GAS_ESTIMATE_DATA, value: 'mockBasicEstimatData' }
- )
- })
- })
-
- describe('setCustomGasPrice', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setCustomGasPrice('mockCustomGasPrice'),
- { type: SET_CUSTOM_GAS_PRICE, value: 'mockCustomGasPrice' }
- )
- })
- })
-
- describe('setCustomGasLimit', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setCustomGasLimit('mockCustomGasLimit'),
- { type: SET_CUSTOM_GAS_LIMIT, value: 'mockCustomGasLimit' }
- )
- })
- })
-
- describe('setCustomGasTotal', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setCustomGasTotal('mockCustomGasTotal'),
- { type: SET_CUSTOM_GAS_TOTAL, value: 'mockCustomGasTotal' }
- )
- })
- })
-
- describe('setCustomGasErrors', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setCustomGasErrors('mockErrorObject'),
- { type: SET_CUSTOM_GAS_ERRORS, value: 'mockErrorObject' }
- )
- })
- })
-
- describe('setApiEstimatesLastRetrieved', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- setApiEstimatesLastRetrieved(1234),
- { type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 1234 }
- )
- })
- })
-
- describe('resetCustomGasState', () => {
- it('should create the correct action', () => {
- assert.deepEqual(
- resetCustomGasState(),
- { type: RESET_CUSTOM_GAS_STATE }
- )
- })
- })
-
-})
diff --git a/ui/app/ducks/tests/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js
deleted file mode 100644
index 43f51c631..000000000
--- a/ui/app/ducks/tests/send-duck.test.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import assert from 'assert'
-
-import SendReducer, {
- openToDropdown,
- closeToDropdown,
- updateSendErrors,
- showGasButtonGroup,
- hideGasButtonGroup,
- updateSendWarnings,
-} from '../send.duck.js'
-
-describe('Send Duck', () => {
- const mockState = {
- send: {
- mockProp: 123,
- },
- }
- const initState = {
- fromDropdownOpen: false,
- toDropdownOpen: false,
- errors: {},
- gasButtonGroupShown: true,
- warnings: {},
- }
- const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN'
- const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'
- const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
- const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
- const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
- const UPDATE_SEND_WARNINGS = 'metamask/send/UPDATE_SEND_WARNINGS'
- const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
- const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'
- const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'
-
- describe('SendReducer()', () => {
- it('should initialize state', () => {
- assert.deepEqual(
- SendReducer({}),
- initState
- )
- })
-
- it('should return state unchanged if it does not match a dispatched actions type', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: 'someOtherAction',
- value: 'someValue',
- }),
- Object.assign({}, mockState.send)
- )
- })
-
- it('should set fromDropdownOpen to true when receiving a OPEN_FROM_DROPDOWN action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: OPEN_FROM_DROPDOWN,
- }),
- Object.assign({fromDropdownOpen: true}, mockState.send)
- )
- })
-
- it('should return a new object (and not just modify the existing state object)', () => {
- assert.deepEqual(SendReducer(mockState), mockState.send)
- assert.notEqual(SendReducer(mockState), mockState.send)
- })
-
- it('should set fromDropdownOpen to false when receiving a CLOSE_FROM_DROPDOWN action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: CLOSE_FROM_DROPDOWN,
- }),
- Object.assign({fromDropdownOpen: false}, mockState.send)
- )
- })
-
- it('should set toDropdownOpen to true when receiving a OPEN_TO_DROPDOWN action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: OPEN_TO_DROPDOWN,
- }),
- Object.assign({toDropdownOpen: true}, mockState.send)
- )
- })
-
- it('should set toDropdownOpen to false when receiving a CLOSE_TO_DROPDOWN action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: CLOSE_TO_DROPDOWN,
- }),
- Object.assign({toDropdownOpen: false}, mockState.send)
- )
- })
-
- it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => {
- assert.deepEqual(
- SendReducer(Object.assign({}, mockState, { gasButtonGroupShown: false }), {
- type: SHOW_GAS_BUTTON_GROUP,
- }),
- Object.assign({gasButtonGroupShown: true}, mockState.send)
- )
- })
-
- it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: HIDE_GAS_BUTTON_GROUP,
- }),
- Object.assign({gasButtonGroupShown: false}, mockState.send)
- )
- })
-
- it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => {
- const modifiedMockState = Object.assign({}, mockState, {
- send: {
- errors: {
- someError: false,
- },
- },
- })
- assert.deepEqual(
- SendReducer(modifiedMockState, {
- type: UPDATE_SEND_ERRORS,
- value: { someOtherError: true },
- }),
- Object.assign({}, modifiedMockState.send, {
- errors: {
- someError: false,
- someOtherError: true,
- },
- })
- )
- })
-
- it('should return the initial state in response to a RESET_SEND_STATE action', () => {
- assert.deepEqual(
- SendReducer(mockState, {
- type: RESET_SEND_STATE,
- }),
- Object.assign({}, initState)
- )
- })
- })
-
- describe('openToDropdown', () => {
- assert.deepEqual(
- openToDropdown(),
- { type: OPEN_TO_DROPDOWN }
- )
- })
-
- describe('closeToDropdown', () => {
- assert.deepEqual(
- closeToDropdown(),
- { type: CLOSE_TO_DROPDOWN }
- )
- })
-
- describe('showGasButtonGroup', () => {
- assert.deepEqual(
- showGasButtonGroup(),
- { type: SHOW_GAS_BUTTON_GROUP }
- )
- })
-
- describe('hideGasButtonGroup', () => {
- assert.deepEqual(
- hideGasButtonGroup(),
- { type: HIDE_GAS_BUTTON_GROUP }
- )
- })
-
- describe('updateSendErrors', () => {
- assert.deepEqual(
- updateSendErrors('mockErrorObject'),
- { type: UPDATE_SEND_ERRORS, value: 'mockErrorObject' }
- )
- })
-
- describe('updateSendWarnings', () => {
- assert.deepEqual(
- updateSendWarnings('mockWarningObject'),
- { type: UPDATE_SEND_WARNINGS, value: 'mockWarningObject' }
- )
- })
-
-})
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
deleted file mode 100644
index 0451824e8..000000000
--- a/ui/app/helpers/confirm-transaction/util.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import currencyFormatter from 'currency-formatter'
-import currencies from 'currency-formatter/currencies'
-import ethUtil from 'ethereumjs-util'
-import BigNumber from 'bignumber.js'
-
-import {
- conversionUtil,
- addCurrencies,
- multiplyCurrencies,
- conversionGreaterThan,
-} from '../../conversion-util'
-
-import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
-
-export function increaseLastGasPrice (lastGasPrice) {
- return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- }))
-}
-
-export function hexGreaterThan (a, b) {
- return conversionGreaterThan(
- { value: a, fromNumericBase: 'hex' },
- { value: b, fromNumericBase: 'hex' },
- )
-}
-
-export function getHexGasTotal ({ gasLimit, gasPrice }) {
- return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, {
- toNumericBase: 'hex',
- multiplicandBase: 16,
- multiplierBase: 16,
- }))
-}
-
-export function addEth (...args) {
- return args.reduce((acc, base) => {
- return addCurrencies(acc, base, {
- toNumericBase: 'dec',
- numberOfDecimals: 6,
- })
- })
-}
-
-export function addFiat (...args) {
- return args.reduce((acc, base) => {
- return addCurrencies(acc, base, {
- toNumericBase: 'dec',
- numberOfDecimals: 2,
- })
- })
-}
-
-export function getValueFromWeiHex ({
- value,
- fromCurrency = 'ETH',
- toCurrency,
- conversionRate,
- numberOfDecimals,
- toDenomination,
-}) {
- return conversionUtil(value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency,
- toCurrency,
- numberOfDecimals,
- fromDenomination: 'WEI',
- toDenomination,
- conversionRate,
- })
-}
-
-export function getTransactionFee ({
- value,
- fromCurrency = 'ETH',
- toCurrency,
- conversionRate,
- numberOfDecimals,
-}) {
- return conversionUtil(value, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency,
- toCurrency,
- numberOfDecimals,
- conversionRate,
- })
-}
-
-export function formatCurrency (value, currencyCode) {
- const upperCaseCurrencyCode = currencyCode.toUpperCase()
-
- return currencies.find(currency => currency.code === upperCaseCurrencyCode)
- ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode, style: 'currency' })
- : value
-}
-
-export function convertTokenToFiat ({
- value,
- fromCurrency = 'ETH',
- toCurrency,
- conversionRate,
- contractExchangeRate,
-}) {
- const totalExchangeRate = conversionRate * contractExchangeRate
-
- return conversionUtil(value, {
- fromNumericBase: 'dec',
- toNumericBase: 'dec',
- fromCurrency,
- toCurrency,
- numberOfDecimals: 2,
- conversionRate: totalExchangeRate,
- })
-}
-
-export function hasUnconfirmedTransactions (state) {
- return unconfirmedTransactionsCountSelector(state) > 0
-}
-
-export function roundExponential (value) {
- const PRECISION = 4
- const bigNumberValue = new BigNumber(String(value))
-
- // In JS, numbers with exponentials greater than 20 get displayed as an exponential.
- return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
-}
diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js
deleted file mode 100644
index 4c1a3e16b..000000000
--- a/ui/app/helpers/confirm-transaction/util.test.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import * as utils from './util'
-import assert from 'assert'
-
-describe('Confirm Transaction utils', () => {
- describe('increaseLastGasPrice', () => {
- it('should increase the gasPrice by 10%', () => {
- const increasedGasPrice = utils.increaseLastGasPrice('0xa')
- assert.equal(increasedGasPrice, '0xb')
- })
-
- it('should prefix the result with 0x', () => {
- const increasedGasPrice = utils.increaseLastGasPrice('a')
- assert.equal(increasedGasPrice, '0xb')
- })
- })
-
- describe('hexGreaterThan', () => {
- it('should return true if the first value is greater than the second value', () => {
- assert.equal(
- utils.hexGreaterThan('0xb', '0xa'),
- true
- )
- })
-
- it('should return false if the first value is less than the second value', () => {
- assert.equal(
- utils.hexGreaterThan('0xa', '0xb'),
- false
- )
- })
-
- it('should return false if the first value is equal to the second value', () => {
- assert.equal(
- utils.hexGreaterThan('0xa', '0xa'),
- false
- )
- })
-
- it('should correctly compare prefixed and non-prefixed hex values', () => {
- assert.equal(
- utils.hexGreaterThan('0xb', 'a'),
- true
- )
- })
- })
-
- describe('getHexGasTotal', () => {
- it('should multiply the hex gasLimit and hex gasPrice values together', () => {
- assert.equal(
- utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }),
- '0x1319718a5000'
- )
- })
-
- it('should prefix the result with 0x', () => {
- assert.equal(
- utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }),
- '0x1319718a5000'
- )
- })
- })
-
- describe('addEth', () => {
- it('should add two values together rounding to 6 decimal places', () => {
- assert.equal(
- utils.addEth('0.12345678', '0'),
- '0.123457'
- )
- })
-
- it('should add any number of values together rounding to 6 decimal places', () => {
- assert.equal(
- utils.addEth('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
- '0.123457'
- )
- })
- })
-
- describe('addFiat', () => {
- it('should add two values together rounding to 2 decimal places', () => {
- assert.equal(
- utils.addFiat('0.12345678', '0'),
- '0.12'
- )
- })
-
- it('should add any number of values together rounding to 2 decimal places', () => {
- assert.equal(
- utils.addFiat('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
- '0.12'
- )
- })
- })
-
- describe('getValueFromWeiHex', () => {
- it('should get the transaction amount in ETH', () => {
- const ethTransactionAmount = utils.getValueFromWeiHex({
- value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
- })
-
- assert.equal(ethTransactionAmount, '1')
- })
-
- it('should get the transaction amount in fiat', () => {
- const fiatTransactionAmount = utils.getValueFromWeiHex({
- value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
- })
-
- assert.equal(fiatTransactionAmount, '468.58')
- })
- })
-
- describe('getTransactionFee', () => {
- it('should get the transaction fee in ETH', () => {
- const ethTransactionFee = utils.getTransactionFee({
- value: '0x1319718a5000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
- })
-
- assert.equal(ethTransactionFee, '0.000021')
- })
-
- it('should get the transaction fee in fiat', () => {
- const fiatTransactionFee = utils.getTransactionFee({
- value: '0x1319718a5000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
- })
-
- assert.equal(fiatTransactionFee, '0.01')
- })
- })
-
- describe('formatCurrency', () => {
- it('should format USD values', () => {
- const value = utils.formatCurrency('123.45', 'usd')
- assert.equal(value, '$123.45')
- })
- })
-})
diff --git a/ui/app/helpers/constants/common.js b/ui/app/helpers/constants/common.js
new file mode 100644
index 000000000..58fae5e5f
--- /dev/null
+++ b/ui/app/helpers/constants/common.js
@@ -0,0 +1,13 @@
+export const ETH = 'ETH'
+export const GWEI = 'GWEI'
+export const WEI = 'WEI'
+
+export const PRIMARY = 'PRIMARY'
+export const SECONDARY = 'SECONDARY'
+
+export const NETWORK_TYPES = {
+ KOVAN: 'kovan',
+ MAINNET: 'mainnet',
+ RINKEBY: 'rinkeby',
+ ROPSTEN: 'ropsten',
+}
diff --git a/ui/app/constants/error-keys.js b/ui/app/helpers/constants/error-keys.js
index 704064c96..704064c96 100644
--- a/ui/app/constants/error-keys.js
+++ b/ui/app/helpers/constants/error-keys.js
diff --git a/ui/app/infura-conversion.json b/ui/app/helpers/constants/infura-conversion.json
index 9a96fe069..9a96fe069 100644
--- a/ui/app/infura-conversion.json
+++ b/ui/app/helpers/constants/infura-conversion.json
diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js
new file mode 100644
index 000000000..c15027ff4
--- /dev/null
+++ b/ui/app/helpers/constants/routes.js
@@ -0,0 +1,93 @@
+const DEFAULT_ROUTE = '/'
+const UNLOCK_ROUTE = '/unlock'
+const LOCK_ROUTE = '/lock'
+const SETTINGS_ROUTE = '/settings'
+const GENERAL_ROUTE = '/settings/general'
+const INFO_ROUTE = '/settings/info'
+const ADVANCED_ROUTE = '/settings/advanced'
+const SECURITY_ROUTE = '/settings/security'
+const COMPANY_ROUTE = '/settings/company'
+const ABOUT_US_ROUTE = '/settings/about-us'
+const REVEAL_SEED_ROUTE = '/seed'
+const MOBILE_SYNC_ROUTE = '/mobile-sync'
+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'
+const SEND_ROUTE = '/send'
+const NOTICE_ROUTE = '/notice'
+const WELCOME_ROUTE = '/welcome'
+
+const INITIALIZE_ROUTE = '/initialize'
+const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'
+const INITIALIZE_UNLOCK_ROUTE = '/initialize/unlock'
+const INITIALIZE_CREATE_PASSWORD_ROUTE = '/initialize/create-password'
+const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/create-password/import-account'
+const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/create-password/import-with-seed-phrase'
+const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/create-password/unique-image'
+const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
+const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action'
+const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase'
+const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow'
+const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm'
+const INITIALIZE_METAMETRICS_OPT_IN_ROUTE = '/initialize/metametrics-opt-in'
+
+const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
+const CONFIRM_SEND_ETHER_PATH = '/send-ether'
+const CONFIRM_SEND_TOKEN_PATH = '/send-token'
+const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'
+const CONFIRM_APPROVE_PATH = '/approve'
+const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
+const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
+const SIGNATURE_REQUEST_PATH = '/signature-request'
+
+module.exports = {
+ DEFAULT_ROUTE,
+ UNLOCK_ROUTE,
+ LOCK_ROUTE,
+ SETTINGS_ROUTE,
+ INFO_ROUTE,
+ REVEAL_SEED_ROUTE,
+ MOBILE_SYNC_ROUTE,
+ CONFIRM_SEED_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+ SEND_ROUTE,
+ NOTICE_ROUTE,
+ WELCOME_ROUTE,
+ INITIALIZE_ROUTE,
+ INITIALIZE_WELCOME_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_ACCOUNT_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_UNIQUE_IMAGE_ROUTE,
+ INITIALIZE_NOTICE_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+ ADVANCED_ROUTE,
+ SECURITY_ROUTE,
+ COMPANY_ROUTE,
+ GENERAL_ROUTE,
+ ABOUT_US_ROUTE,
+}
diff --git a/ui/app/constants/transactions.js b/ui/app/helpers/constants/transactions.js
index d0a819b9b..d0a819b9b 100644
--- a/ui/app/constants/transactions.js
+++ b/ui/app/helpers/constants/transactions.js
diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js
deleted file mode 100644
index 065d67e8e..000000000
--- a/ui/app/helpers/conversions.util.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import ethUtil from 'ethereumjs-util'
-import { ETH, GWEI, WEI } from '../constants/common'
-import { conversionUtil, addCurrencies } from '../conversion-util'
-
-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, fromCurrency = ETH, conversionRate, numberOfDecimals = 6 }) {
- const denominations = [fromCurrency, GWEI, WEI]
-
- let nonZeroDenomination
-
- for (let i = 0; i < denominations.length; i++) {
- const convertedValue = getValueFromWeiHex({
- value,
- conversionRate,
- fromCurrency,
- toCurrency: fromCurrency,
- numberOfDecimals,
- toDenomination: denominations[i],
- })
-
- if (convertedValue !== '0' || i === denominations.length - 1) {
- nonZeroDenomination = `${convertedValue} ${denominations[i]}`
- break
- }
- }
-
- return nonZeroDenomination
-}
-
-export function getValueFromWeiHex ({
- value,
- fromCurrency = ETH,
- toCurrency,
- conversionRate,
- numberOfDecimals,
- toDenomination,
-}) {
- return conversionUtil(value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency,
- toCurrency,
- numberOfDecimals,
- fromDenomination: WEI,
- toDenomination,
- conversionRate,
- })
-}
-
-export function getWeiHexFromDecimalValue ({
- value,
- fromCurrency,
- conversionRate,
- fromDenomination,
- invertConversionRate,
-}) {
- return conversionUtil(value, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- toCurrency: ETH,
- fromCurrency,
- conversionRate,
- invertConversionRate,
- fromDenomination,
- toDenomination: WEI,
- })
-}
-
-export function addHexWEIsToDec (aHexWEI, bHexWEI) {
- return addCurrencies(aHexWEI, bHexWEI, {
- aBase: 16,
- bBase: 16,
- fromDenomination: 'WEI',
- numberOfDecimals: 6,
- })
-}
-
-export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) {
- return conversionUtil(ethTotal, {
- fromNumericBase: 'dec',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: convertedCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
-}
-
-export function decGWEIToHexWEI (decGWEI) {
- return conversionUtil(decGWEI, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- fromDenomination: 'GWEI',
- toDenomination: 'WEI',
- })
-}
-
-export function hexWEIToDecGWEI (decGWEI) {
- return conversionUtil(decGWEI, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- toDenomination: 'GWEI',
- })
-}
diff --git a/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js b/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js
new file mode 100644
index 000000000..c195d0e21
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Redirect, Route } from 'react-router-dom'
+import { UNLOCK_ROUTE, INITIALIZE_ROUTE } from '../../constants/routes'
+
+export default function Authenticated (props) {
+ const { isUnlocked, completedOnboarding } = props
+
+ switch (true) {
+ case isUnlocked && completedOnboarding:
+ return <Route { ...props } />
+ case !completedOnboarding:
+ return <Redirect to={{ pathname: INITIALIZE_ROUTE }} />
+ default:
+ return <Redirect to={{ pathname: UNLOCK_ROUTE }} />
+ }
+}
+
+Authenticated.propTypes = {
+ isUnlocked: PropTypes.bool,
+ completedOnboarding: PropTypes.bool,
+}
diff --git a/ui/app/higher-order-components/authenticated/authenticated.container.js b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js
index 6124b0fcd..6124b0fcd 100644
--- a/ui/app/higher-order-components/authenticated/authenticated.container.js
+++ b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js
diff --git a/ui/app/higher-order-components/authenticated/index.js b/ui/app/helpers/higher-order-components/authenticated/index.js
index 05632ed21..05632ed21 100644
--- a/ui/app/higher-order-components/authenticated/index.js
+++ b/ui/app/helpers/higher-order-components/authenticated/index.js
diff --git a/ui/app/helpers/higher-order-components/i18n-provider.js b/ui/app/helpers/higher-order-components/i18n-provider.js
new file mode 100644
index 000000000..0e34e17e0
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/i18n-provider.js
@@ -0,0 +1,55 @@
+const { Component } = require('react')
+const connect = require('react-redux').connect
+const PropTypes = require('prop-types')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const t = require('../utils/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
+ return {
+ 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)
+ },
+ }
+ }
+
+ render () {
+ return this.props.children
+ }
+}
+
+I18nProvider.propTypes = {
+ localeMessages: PropTypes.object,
+ children: PropTypes.object,
+}
+
+I18nProvider.childContextTypes = {
+ t: PropTypes.func,
+ tOrDefault: PropTypes.func,
+ tOrKey: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ const { localeMessages } = state
+ return {
+ localeMessages,
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps)
+)(I18nProvider)
+
diff --git a/ui/app/higher-order-components/initialized/index.js b/ui/app/helpers/higher-order-components/initialized/index.js
index 863fcb389..863fcb389 100644
--- a/ui/app/higher-order-components/initialized/index.js
+++ b/ui/app/helpers/higher-order-components/initialized/index.js
diff --git a/ui/app/helpers/higher-order-components/initialized/initialized.component.js b/ui/app/helpers/higher-order-components/initialized/initialized.component.js
new file mode 100644
index 000000000..2042c0046
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/initialized/initialized.component.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Redirect, Route } from 'react-router-dom'
+import { INITIALIZE_ROUTE } from '../../constants/routes'
+
+export default function Initialized (props) {
+ return props.completedOnboarding
+ ? <Route { ...props } />
+ : <Redirect to={{ pathname: INITIALIZE_ROUTE }} />
+}
+
+Initialized.propTypes = {
+ completedOnboarding: PropTypes.bool,
+}
diff --git a/ui/app/higher-order-components/initialized/initialized.container.js b/ui/app/helpers/higher-order-components/initialized/initialized.container.js
index 0e7f72bcb..0e7f72bcb 100644
--- a/ui/app/higher-order-components/initialized/initialized.container.js
+++ b/ui/app/helpers/higher-order-components/initialized/initialized.container.js
diff --git a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js
new file mode 100644
index 000000000..6086e03fb
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js
@@ -0,0 +1,106 @@
+import { Component } from 'react'
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import {
+ getCurrentNetworkId,
+ getSelectedAsset,
+ getAccountType,
+ getNumberOfAccounts,
+ getNumberOfTokens,
+} from '../../../selectors/selectors'
+import {
+ txDataSelector,
+} from '../../../selectors/confirm-transaction'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import {
+ sendMetaMetricsEvent,
+ sendCountIsTrackable,
+} from '../../utils/metametrics.util'
+
+class MetaMetricsProvider extends Component {
+ static propTypes = {
+ network: PropTypes.string.isRequired,
+ environmentType: PropTypes.string.isRequired,
+ activeCurrency: PropTypes.string.isRequired,
+ accountType: PropTypes.string.isRequired,
+ metaMetricsSendCount: PropTypes.number.isRequired,
+ children: PropTypes.object.isRequired,
+ history: PropTypes.object.isRequired,
+ }
+
+ static childContextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ previousPath: '',
+ currentPath: window.location.href,
+ }
+
+ props.history.listen(locationObj => {
+ this.setState({
+ previousPath: this.state.currentPath,
+ currentPath: window.location.href,
+ })
+ })
+ }
+
+ getChildContext () {
+ const props = this.props
+ const { pathname } = location
+ const { previousPath, currentPath } = this.state
+
+ return {
+ metricsEvent: (config = {}, overrides = {}) => {
+ const { eventOpts = {} } = config
+ const { name = '' } = eventOpts
+ const { pathname: overRidePathName = '' } = overrides
+ const isSendFlow = Boolean(name.match(/^send|^confirm/) || overRidePathName.match(/send|confirm/))
+
+ if (props.participateInMetaMetrics || config.isOptIn) {
+ return sendMetaMetricsEvent({
+ ...props,
+ ...config,
+ previousPath,
+ currentPath,
+ pathname,
+ excludeMetaMetricsId: isSendFlow && !sendCountIsTrackable(props.metaMetricsSendCount + 1),
+ ...overrides,
+ })
+ }
+ },
+ }
+ }
+
+ render () {
+ return this.props.children
+ }
+}
+
+const mapStateToProps = state => {
+ const txData = txDataSelector(state) || {}
+
+ return {
+ network: getCurrentNetworkId(state),
+ environmentType: getEnvironmentType(),
+ activeCurrency: getSelectedAsset(state),
+ accountType: getAccountType(state),
+ confirmTransactionOrigin: txData.origin,
+ metaMetricsId: state.metamask.metaMetricsId,
+ participateInMetaMetrics: state.metamask.participateInMetaMetrics,
+ metaMetricsSendCount: state.metamask.metaMetricsSendCount,
+ numberOfTokens: getNumberOfTokens(state),
+ numberOfAccounts: getNumberOfAccounts(state),
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps)
+)(MetaMetricsProvider)
+
diff --git a/ui/app/higher-order-components/with-method-data/index.js b/ui/app/helpers/higher-order-components/with-method-data/index.js
index f511e1ae7..f511e1ae7 100644
--- a/ui/app/higher-order-components/with-method-data/index.js
+++ b/ui/app/helpers/higher-order-components/with-method-data/index.js
diff --git a/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js b/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js
new file mode 100644
index 000000000..efa9ad0a2
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js
@@ -0,0 +1,65 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { getMethodData, getFourBytePrefix } from '../../utils/transactions.util'
+
+export default function withMethodData (WrappedComponent) {
+ return class MethodDataWrappedComponent extends PureComponent {
+ static propTypes = {
+ transaction: PropTypes.object,
+ knownMethodData: PropTypes.object,
+ addKnownMethodData: PropTypes.func,
+ }
+
+ static defaultProps = {
+ transaction: {},
+ knownMethodData: {},
+ }
+
+ state = {
+ methodData: {},
+ done: false,
+ error: null,
+ }
+
+ componentDidMount () {
+ this.fetchMethodData()
+ }
+
+ async fetchMethodData () {
+ const { transaction, knownMethodData, addKnownMethodData } = this.props
+ const { txParams: { data = '' } = {} } = transaction
+
+ if (data) {
+ try {
+ let methodData
+ const fourBytePrefix = getFourBytePrefix(data)
+ if (fourBytePrefix in knownMethodData) {
+ methodData = knownMethodData[fourBytePrefix]
+ } else {
+ methodData = await getMethodData(data)
+ if (!Object.entries(methodData).length === 0) {
+ addKnownMethodData(fourBytePrefix, methodData)
+ }
+ }
+
+ 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/helpers/higher-order-components/with-modal-props/index.js
index e476b51d2..e476b51d2 100644
--- a/ui/app/higher-order-components/with-modal-props/index.js
+++ b/ui/app/helpers/higher-order-components/with-modal-props/index.js
diff --git a/ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js
index 654e7062a..654e7062a 100644
--- a/ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js
+++ b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js
diff --git a/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js
new file mode 100644
index 000000000..aac6b5a61
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import { hideModal } from '../../../store/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/helpers/higher-order-components/with-token-tracker/index.js
index d401e81f1..d401e81f1 100644
--- a/ui/app/higher-order-components/with-token-tracker/index.js
+++ b/ui/app/helpers/higher-order-components/with-token-tracker/index.js
diff --git a/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js
index 36f6a6efd..36f6a6efd 100644
--- a/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js
+++ b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js
diff --git a/ui/app/helpers/tests/common.util.test.js b/ui/app/helpers/tests/common.util.test.js
deleted file mode 100644
index a52b91a10..000000000
--- a/ui/app/helpers/tests/common.util.test.js
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index 838522e35..000000000
--- a/ui/app/helpers/tests/transactions.util.test.js
+++ /dev/null
@@ -1,57 +0,0 @@
-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())
- })
- })
-
- describe('getStatusKey', () => {
- it('should return the correct status', () => {
- const tests = [
- {
- transaction: {
- status: 'confirmed',
- txReceipt: {
- status: '0x0',
- },
- },
- expected: 'failed',
- },
- {
- transaction: {
- status: 'confirmed',
- txReceipt: {
- status: '0x1',
- },
- },
- expected: 'confirmed',
- },
- {
- transaction: {
- status: 'pending',
- },
- expected: 'pending',
- },
- ]
-
- tests.forEach(({ transaction, expected }) => {
- assert.equal(utils.getStatusKey(transaction), expected)
- })
- })
- })
-})
diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js
deleted file mode 100644
index d5b7f4958..000000000
--- a/ui/app/helpers/transactions.util.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import ethUtil from 'ethereumjs-util'
-import MethodRegistry from 'eth-method-registry'
-import abi from 'human-standard-token-abi'
-import abiDecoder from 'abi-decoder'
-import {
- TRANSACTION_TYPE_CANCEL,
- TRANSACTION_STATUS_CONFIRMED,
-} from '../../../app/scripts/controllers/transactions/enums'
-
-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,
- CONTRACT_INTERACTION_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 })
-
-/**
- * Attempts to return the method data from the MethodRegistry library, if the method exists in the
- * registry. Otherwise, returns an empty object.
- * @param {string} data - The hex data (@code txParams.data) of a transaction
- * @returns {Object}
- */
-export async function getMethodData (data = '') {
- const prefixedData = ethUtil.addHexPrefix(data)
- const fourBytePrefix = prefixedData.slice(0, 10)
- const sig = await registry.lookup(fourBytePrefix)
-
- if (!sig) {
- return {}
- }
-
- const parsedResult = registry.parse(sig)
-
- return {
- name: parsedResult.name,
- params: parsedResult.args,
- }
-}
-
-export function isConfirmDeployContract (txData = {}) {
- const { txParams = {} } = txData
- return !txParams.to
-}
-
-/**
- * Returns four-byte method signature from data
- *
- * @param {string} data - The hex data (@code txParams.data) of a transaction
- * @returns {string} - The four-byte method signature
- */
-export function getFourBytePrefix (data = '') {
- const prefixedData = ethUtil.addHexPrefix(data)
- const fourBytePrefix = prefixedData.slice(0, 10)
- return fourBytePrefix
-}
-
-/**
- * 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 CONTRACT_INTERACTION_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)
- // Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
- const codeIsEmpty = !code || code === '0x' || code === '0x0'
- return !codeIsEmpty
-}
-
-export function sumHexes (...args) {
- const total = args.reduce((acc, base) => {
- return addCurrencies(acc, base, {
- toNumericBase: 'hex',
- })
- })
-
- return ethUtil.addHexPrefix(total)
-}
-
-/**
- * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of
- * txMeta.status because txMeta.status does not reflect on-chain errors.
- * @param {Object} transaction - The txMeta object of a transaction.
- * @param {Object} transaction.txReceipt - The transaction receipt.
- * @returns {string}
- */
-export function getStatusKey (transaction) {
- const { txReceipt: { status: receiptStatus } = {}, type, status } = transaction
-
- // There was an on-chain failure
- if (receiptStatus === '0x0') {
- return 'failed'
- }
-
- if (status === TRANSACTION_STATUS_CONFIRMED && type === TRANSACTION_TYPE_CANCEL) {
- return 'cancelled'
- }
-
- return transaction.status
-}
diff --git a/ui/app/helpers/common.util.js b/ui/app/helpers/utils/common.util.js
index 0c02481e6..0c02481e6 100644
--- a/ui/app/helpers/common.util.js
+++ b/ui/app/helpers/utils/common.util.js
diff --git a/ui/app/helpers/utils/common.util.test.js b/ui/app/helpers/utils/common.util.test.js
new file mode 100644
index 000000000..6259f4a89
--- /dev/null
+++ b/ui/app/helpers/utils/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/utils/confirm-tx.util.js b/ui/app/helpers/utils/confirm-tx.util.js
new file mode 100644
index 000000000..224560f5a
--- /dev/null
+++ b/ui/app/helpers/utils/confirm-tx.util.js
@@ -0,0 +1,131 @@
+import currencyFormatter from 'currency-formatter'
+import currencies from 'currency-formatter/currencies'
+import ethUtil from 'ethereumjs-util'
+import BigNumber from 'bignumber.js'
+
+import {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} from './conversion-util'
+
+import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
+
+export function increaseLastGasPrice (lastGasPrice = '0x0') {
+ return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ toNumericBase: 'hex',
+ }))
+}
+
+export function hexGreaterThan (a, b) {
+ return conversionGreaterThan(
+ { value: a, fromNumericBase: 'hex' },
+ { value: b, fromNumericBase: 'hex' },
+ )
+}
+
+export function getHexGasTotal ({ gasLimit = '0x0', gasPrice = '0x0' }) {
+ return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ }))
+}
+
+export function addEth (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 6,
+ })
+ })
+}
+
+export function addFiat (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 2,
+ })
+ })
+}
+
+export function getValueFromWeiHex ({
+ value,
+ fromCurrency = 'ETH',
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+ toDenomination,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency,
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: 'WEI',
+ toDenomination,
+ conversionRate,
+ })
+}
+
+export function getTransactionFee ({
+ value,
+ fromCurrency = 'ETH',
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency,
+ toCurrency,
+ numberOfDecimals,
+ conversionRate,
+ })
+}
+
+export function formatCurrency (value, currencyCode) {
+ const upperCaseCurrencyCode = currencyCode.toUpperCase()
+
+ return currencies.find(currency => currency.code === upperCaseCurrencyCode)
+ ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode, style: 'currency' })
+ : value
+}
+
+export function convertTokenToFiat ({
+ value,
+ fromCurrency = 'ETH',
+ toCurrency,
+ conversionRate,
+ contractExchangeRate,
+}) {
+ const totalExchangeRate = conversionRate * contractExchangeRate
+
+ return conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'dec',
+ fromCurrency,
+ toCurrency,
+ numberOfDecimals: 2,
+ conversionRate: totalExchangeRate,
+ })
+}
+
+export function hasUnconfirmedTransactions (state) {
+ return unconfirmedTransactionsCountSelector(state) > 0
+}
+
+export function roundExponential (value) {
+ const PRECISION = 4
+ const bigNumberValue = new BigNumber(String(value))
+
+ // In JS, numbers with exponentials greater than 20 get displayed as an exponential.
+ return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
+}
diff --git a/ui/app/helpers/utils/confirm-tx.util.test.js b/ui/app/helpers/utils/confirm-tx.util.test.js
new file mode 100644
index 000000000..e818601ca
--- /dev/null
+++ b/ui/app/helpers/utils/confirm-tx.util.test.js
@@ -0,0 +1,137 @@
+import * as utils from './confirm-tx.util'
+import assert from 'assert'
+
+describe('Confirm Transaction utils', () => {
+ describe('increaseLastGasPrice', () => {
+ it('should increase the gasPrice by 10%', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('0xa')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+
+ it('should prefix the result with 0x', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('a')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+ })
+
+ describe('hexGreaterThan', () => {
+ it('should return true if the first value is greater than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', '0xa'),
+ true
+ )
+ })
+
+ it('should return false if the first value is less than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xb'),
+ false
+ )
+ })
+
+ it('should return false if the first value is equal to the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xa'),
+ false
+ )
+ })
+
+ it('should correctly compare prefixed and non-prefixed hex values', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', 'a'),
+ true
+ )
+ })
+ })
+
+ describe('getHexGasTotal', () => {
+ it('should multiply the hex gasLimit and hex gasPrice values together', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+
+ it('should prefix the result with 0x', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+ })
+
+ describe('addEth', () => {
+ it('should add two values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.12345678', '0'),
+ '0.123457'
+ )
+ })
+
+ it('should add any number of values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.123457'
+ )
+ })
+ })
+
+ describe('addFiat', () => {
+ it('should add two values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.12345678', '0'),
+ '0.12'
+ )
+ })
+
+ it('should add any number of values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.12'
+ )
+ })
+ })
+
+ describe('getValueFromWeiHex', () => {
+ it('should get the transaction amount in ETH', () => {
+ const ethTransactionAmount = utils.getValueFromWeiHex({
+ value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionAmount, '1')
+ })
+
+ it('should get the transaction amount in fiat', () => {
+ const fiatTransactionAmount = utils.getValueFromWeiHex({
+ value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionAmount, '468.58')
+ })
+ })
+
+ describe('getTransactionFee', () => {
+ it('should get the transaction fee in ETH', () => {
+ const ethTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionFee, '0.000021')
+ })
+
+ it('should get the transaction fee in fiat', () => {
+ const fiatTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionFee, '0.01')
+ })
+ })
+
+ describe('formatCurrency', () => {
+ it('should format USD values', () => {
+ const value = utils.formatCurrency('123.45', 'usd')
+ assert.equal(value, '$123.45')
+ })
+ })
+})
diff --git a/ui/app/conversion-util.js b/ui/app/helpers/utils/conversion-util.js
index 8cc531773..8cc531773 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/helpers/utils/conversion-util.js
diff --git a/ui/app/conversion-util.test.js b/ui/app/helpers/utils/conversion-util.test.js
index 368ce3bba..368ce3bba 100644
--- a/ui/app/conversion-util.test.js
+++ b/ui/app/helpers/utils/conversion-util.test.js
diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js
new file mode 100644
index 000000000..b4ec50626
--- /dev/null
+++ b/ui/app/helpers/utils/conversions.util.js
@@ -0,0 +1,122 @@
+import ethUtil from 'ethereumjs-util'
+import { ETH, GWEI, WEI } from '../constants/common'
+import { conversionUtil, addCurrencies } from './conversion-util'
+
+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, fromCurrency = ETH, conversionRate, numberOfDecimals = 6 }) {
+ const denominations = [fromCurrency, GWEI, WEI]
+
+ let nonZeroDenomination
+
+ for (let i = 0; i < denominations.length; i++) {
+ const convertedValue = getValueFromWeiHex({
+ value,
+ conversionRate,
+ fromCurrency,
+ toCurrency: fromCurrency,
+ numberOfDecimals,
+ toDenomination: denominations[i],
+ })
+
+ if (convertedValue !== '0' || i === denominations.length - 1) {
+ nonZeroDenomination = `${convertedValue} ${denominations[i]}`
+ break
+ }
+ }
+
+ return nonZeroDenomination
+}
+
+export function getValueFromWeiHex ({
+ value,
+ fromCurrency = ETH,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+ toDenomination,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency,
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: WEI,
+ toDenomination,
+ conversionRate,
+ })
+}
+
+export function getWeiHexFromDecimalValue ({
+ value,
+ fromCurrency,
+ conversionRate,
+ fromDenomination,
+ invertConversionRate,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ toCurrency: ETH,
+ fromCurrency,
+ conversionRate,
+ invertConversionRate,
+ fromDenomination,
+ toDenomination: WEI,
+ })
+}
+
+export function addHexWEIsToDec (aHexWEI, bHexWEI) {
+ return addCurrencies(aHexWEI, bHexWEI, {
+ aBase: 16,
+ bBase: 16,
+ fromDenomination: 'WEI',
+ numberOfDecimals: 6,
+ })
+}
+
+export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) {
+ return conversionUtil(ethTotal, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: convertedCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+}
+
+export function decGWEIToHexWEI (decGWEI) {
+ return conversionUtil(decGWEI, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ })
+}
+
+export function hexWEIToDecGWEI (decGWEI) {
+ return conversionUtil(decGWEI, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+}
diff --git a/ui/app/helpers/formatters.js b/ui/app/helpers/utils/formatters.js
index 106a2520d..106a2520d 100644
--- a/ui/app/helpers/formatters.js
+++ b/ui/app/helpers/utils/formatters.js
diff --git a/ui/i18n-helper.js b/ui/app/helpers/utils/i18n-helper.js
index db07049e1..db07049e1 100644
--- a/ui/i18n-helper.js
+++ b/ui/app/helpers/utils/i18n-helper.js
diff --git a/ui/app/metametrics/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js
index 01984bd5e..01984bd5e 100644
--- a/ui/app/metametrics/metametrics.util.js
+++ b/ui/app/helpers/utils/metametrics.util.js
diff --git a/ui/app/token-util.js b/ui/app/helpers/utils/token-util.js
index 35a19a69f..35a19a69f 100644
--- a/ui/app/token-util.js
+++ b/ui/app/helpers/utils/token-util.js
diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js
new file mode 100644
index 000000000..edf2e24f6
--- /dev/null
+++ b/ui/app/helpers/utils/transactions.util.js
@@ -0,0 +1,179 @@
+import ethUtil from 'ethereumjs-util'
+import MethodRegistry from 'eth-method-registry'
+import abi from 'human-standard-token-abi'
+import abiDecoder from 'abi-decoder'
+import {
+ TRANSACTION_TYPE_CANCEL,
+ TRANSACTION_STATUS_CONFIRMED,
+} from '../../../../app/scripts/controllers/transactions/enums'
+
+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,
+ CONTRACT_INTERACTION_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 })
+
+/**
+ * Attempts to return the method data from the MethodRegistry library, if the method exists in the
+ * registry. Otherwise, returns an empty object.
+ * @param {string} data - The hex data (@code txParams.data) of a transaction
+ * @returns {Object}
+ */
+export async function getMethodData (data = '') {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ const sig = await registry.lookup(fourBytePrefix)
+
+ if (!sig) {
+ return {}
+ }
+
+ const parsedResult = registry.parse(sig)
+
+ return {
+ name: parsedResult.name,
+ params: parsedResult.args,
+ }
+}
+
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
+
+/**
+ * Returns four-byte method signature from data
+ *
+ * @param {string} data - The hex data (@code txParams.data) of a transaction
+ * @returns {string} - The four-byte method signature
+ */
+export function getFourBytePrefix (data = '') {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ return fourBytePrefix
+}
+
+/**
+ * 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 CONTRACT_INTERACTION_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)
+ // Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
+ const codeIsEmpty = !code || code === '0x' || code === '0x0'
+ return !codeIsEmpty
+}
+
+export function sumHexes (...args) {
+ const total = args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'hex',
+ })
+ })
+
+ return ethUtil.addHexPrefix(total)
+}
+
+/**
+ * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of
+ * txMeta.status because txMeta.status does not reflect on-chain errors.
+ * @param {Object} transaction - The txMeta object of a transaction.
+ * @param {Object} transaction.txReceipt - The transaction receipt.
+ * @returns {string}
+ */
+export function getStatusKey (transaction) {
+ const { txReceipt: { status: receiptStatus } = {}, type, status } = transaction
+
+ // There was an on-chain failure
+ if (receiptStatus === '0x0') {
+ return 'failed'
+ }
+
+ if (status === TRANSACTION_STATUS_CONFIRMED && type === TRANSACTION_TYPE_CANCEL) {
+ return 'cancelled'
+ }
+
+ return transaction.status
+}
diff --git a/ui/app/helpers/utils/transactions.util.test.js b/ui/app/helpers/utils/transactions.util.test.js
new file mode 100644
index 000000000..4a8ca5c9d
--- /dev/null
+++ b/ui/app/helpers/utils/transactions.util.test.js
@@ -0,0 +1,57 @@
+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())
+ })
+ })
+
+ describe('getStatusKey', () => {
+ it('should return the correct status', () => {
+ const tests = [
+ {
+ transaction: {
+ status: 'confirmed',
+ txReceipt: {
+ status: '0x0',
+ },
+ },
+ expected: 'failed',
+ },
+ {
+ transaction: {
+ status: 'confirmed',
+ txReceipt: {
+ status: '0x1',
+ },
+ },
+ expected: 'confirmed',
+ },
+ {
+ transaction: {
+ status: 'pending',
+ },
+ expected: 'pending',
+ },
+ ]
+
+ tests.forEach(({ transaction, expected }) => {
+ assert.equal(utils.getStatusKey(transaction), expected)
+ })
+ })
+ })
+})
diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js
new file mode 100644
index 000000000..c50d7cbe5
--- /dev/null
+++ b/ui/app/helpers/utils/util.js
@@ -0,0 +1,326 @@
+const abi = require('human-standard-token-abi')
+const ethUtil = require('ethereumjs-util')
+const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
+import { DateTime } from 'luxon'
+
+const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1)
+const GWEI_FACTOR = new ethUtil.BN(1e9)
+const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
+
+// formatData :: ( date: <Unix Timestamp> ) -> String
+function formatDate (date, format = 'M/d/y \'at\' T') {
+ return DateTime.fromMillis(date).toFormat(format)
+}
+
+var valueTable = {
+ wei: '1000000000000000000',
+ kwei: '1000000000000000',
+ mwei: '1000000000000',
+ gwei: '1000000000',
+ szabo: '1000000',
+ finney: '1000',
+ ether: '1',
+ kether: '0.001',
+ mether: '0.000001',
+ gether: '0.000000001',
+ tether: '0.000000000001',
+}
+var bnTable = {}
+for (var currency in valueTable) {
+ bnTable[currency] = new ethUtil.BN(valueTable[currency], 10)
+}
+
+module.exports = {
+ valuesFor: valuesFor,
+ addressSummary: addressSummary,
+ miniAddressSummary: miniAddressSummary,
+ isAllOneCase: isAllOneCase,
+ isValidAddress: isValidAddress,
+ isValidENSAddress,
+ numericBalance: numericBalance,
+ parseBalance: parseBalance,
+ formatBalance: formatBalance,
+ generateBalanceObject: generateBalanceObject,
+ dataSize: dataSize,
+ readableDate: readableDate,
+ normalizeToWei: normalizeToWei,
+ normalizeEthStringToWei: normalizeEthStringToWei,
+ normalizeNumberToWei: normalizeNumberToWei,
+ valueTable: valueTable,
+ bnTable: bnTable,
+ isHex: isHex,
+ formatDate,
+ bnMultiplyByFraction,
+ getTxFeeBn,
+ shortenBalance,
+ getContractAtAddress,
+ exportAsFile: exportAsFile,
+ isInvalidChecksumAddress,
+ allNull,
+ getTokenAddressFromTokenObject,
+ checksumAddress,
+ addressSlicer,
+ isEthNetwork,
+}
+
+function isEthNetwork (netId) {
+ if (!netId || netId === '1' || netId === '3' || netId === '4' || netId === '42' || netId === '5777') {
+ return true
+ }
+
+ return false
+}
+
+function valuesFor (obj) {
+ if (!obj) return []
+ return Object.keys(obj)
+ .map(function (key) { return obj[key] })
+}
+
+function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) {
+ if (!address) return ''
+ let checked = checksumAddress(address)
+ if (!includeHex) {
+ checked = ethUtil.stripHexPrefix(checked)
+ }
+ return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...'
+}
+
+function miniAddressSummary (address) {
+ if (!address) return ''
+ var checked = checksumAddress(address)
+ return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...'
+}
+
+function isValidAddress (address, network) {
+ var prefixed = ethUtil.addHexPrefix(address)
+ if (address === '0x0000000000000000000000000000000000000000') return false
+ return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
+}
+
+function isValidENSAddress (address) {
+ return address.match(/^.{7,}\.(eth|test)$/)
+}
+
+function isInvalidChecksumAddress (address) {
+ var prefixed = ethUtil.addHexPrefix(address)
+ if (address === '0x0000000000000000000000000000000000000000') return false
+ return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed)
+}
+
+function isAllOneCase (address) {
+ if (!address) return true
+ var lower = address.toLowerCase()
+ var upper = address.toUpperCase()
+ return address === lower || address === upper
+}
+
+// Takes wei Hex, returns wei BN, even if input is null
+function numericBalance (balance) {
+ if (!balance) return new ethUtil.BN(0, 16)
+ var stripped = ethUtil.stripHexPrefix(balance)
+ return new ethUtil.BN(stripped, 16)
+}
+
+// Takes hex, returns [beforeDecimal, afterDecimal]
+function parseBalance (balance) {
+ var beforeDecimal, afterDecimal
+ const wei = numericBalance(balance)
+ var weiString = wei.toString()
+ const trailingZeros = /0+$/
+
+ beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0'
+ afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '')
+ if (afterDecimal === '') { afterDecimal = '0' }
+ return [beforeDecimal, afterDecimal]
+}
+
+// Takes wei hex, returns an object with three properties.
+// Its "formatted" property is what we generally use to render values.
+function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') {
+ var parsed = needsParse ? parseBalance(balance) : balance.split('.')
+ var beforeDecimal = parsed[0]
+ var afterDecimal = parsed[1]
+ var formatted = 'None'
+ if (decimalsToKeep === undefined) {
+ if (beforeDecimal === '0') {
+ if (afterDecimal !== '0') {
+ var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits
+ if (sigFigs) { afterDecimal = sigFigs[0] }
+ formatted = '0.' + afterDecimal + ` ${ticker}`
+ }
+ } else {
+ formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}`
+ }
+ } else {
+ afterDecimal += Array(decimalsToKeep).join('0')
+ formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}`
+ }
+ return formatted
+}
+
+
+function generateBalanceObject (formattedBalance, decimalsToKeep = 1) {
+ var balance = formattedBalance.split(' ')[0]
+ var label = formattedBalance.split(' ')[1]
+ var beforeDecimal = balance.split('.')[0]
+ var afterDecimal = balance.split('.')[1]
+ var shortBalance = shortenBalance(balance, decimalsToKeep)
+
+ if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') {
+ // eslint-disable-next-line eqeqeq
+ if (afterDecimal == 0) {
+ balance = '0'
+ } else {
+ balance = '<1.0e-5'
+ }
+ } else if (beforeDecimal !== '0') {
+ balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}`
+ }
+
+ return { balance, label, shortBalance }
+}
+
+function shortenBalance (balance, decimalsToKeep = 1) {
+ var truncatedValue
+ var convertedBalance = parseFloat(balance)
+ if (convertedBalance > 1000000) {
+ truncatedValue = (balance / 1000000).toFixed(decimalsToKeep)
+ return `${truncatedValue}m`
+ } else if (convertedBalance > 1000) {
+ truncatedValue = (balance / 1000).toFixed(decimalsToKeep)
+ return `${truncatedValue}k`
+ } else if (convertedBalance === 0) {
+ return '0'
+ } else if (convertedBalance < 0.001) {
+ return '<0.001'
+ } else if (convertedBalance < 1) {
+ var stringBalance = convertedBalance.toString()
+ if (stringBalance.split('.')[1].length > 3) {
+ return convertedBalance.toFixed(3)
+ } else {
+ return stringBalance
+ }
+ } else {
+ return convertedBalance.toFixed(decimalsToKeep)
+ }
+}
+
+function dataSize (data) {
+ var size = data ? ethUtil.stripHexPrefix(data).length : 0
+ return size + ' bytes'
+}
+
+// Takes a BN and an ethereum currency name,
+// returns a BN in wei
+function normalizeToWei (amount, currency) {
+ try {
+ return amount.mul(bnTable.wei).div(bnTable[currency])
+ } catch (e) {}
+ return amount
+}
+
+function normalizeEthStringToWei (str) {
+ const parts = str.split('.')
+ let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei)
+ if (parts[1]) {
+ var decimal = parts[1]
+ while (decimal.length < 18) {
+ decimal += '0'
+ }
+ if (decimal.length > 18) {
+ decimal = decimal.slice(0, 18)
+ }
+ const decimalBN = new ethUtil.BN(decimal, 10)
+ eth = eth.add(decimalBN)
+ }
+ return eth
+}
+
+var multiple = new ethUtil.BN('10000', 10)
+function normalizeNumberToWei (n, currency) {
+ var enlarged = n * 10000
+ var amount = new ethUtil.BN(String(enlarged), 10)
+ return normalizeToWei(amount, currency).div(multiple)
+}
+
+function readableDate (ms) {
+ var date = new Date(ms)
+ var month = date.getMonth()
+ var day = date.getDate()
+ var year = date.getFullYear()
+ var hours = date.getHours()
+ var minutes = '0' + date.getMinutes()
+ var seconds = '0' + date.getSeconds()
+
+ var dateStr = `${month}/${day}/${year}`
+ var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}`
+ return `${dateStr} ${time}`
+}
+
+function isHex (str) {
+ return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/))
+}
+
+function bnMultiplyByFraction (targetBN, numerator, denominator) {
+ const numBN = new ethUtil.BN(numerator)
+ const denomBN = new ethUtil.BN(denominator)
+ return targetBN.mul(numBN).div(denomBN)
+}
+
+function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) {
+ const gasBn = hexToBn(gas)
+ const gasPriceBn = hexToBn(gasPrice)
+ const txFeeBn = gasBn.mul(gasPriceBn)
+
+ return txFeeBn.toString(16)
+}
+
+function getContractAtAddress (tokenAddress) {
+ return global.eth.contract(abi).at(tokenAddress)
+}
+
+function exportAsFile (filename, data, type = 'text/csv') {
+ // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
+ const blob = new Blob([data], {type})
+ if (window.navigator.msSaveOrOpenBlob) {
+ window.navigator.msSaveBlob(blob, filename)
+ } else {
+ const elem = window.document.createElement('a')
+ elem.target = '_blank'
+ elem.href = window.URL.createObjectURL(blob)
+ elem.download = filename
+ document.body.appendChild(elem)
+ elem.click()
+ document.body.removeChild(elem)
+ }
+}
+
+function allNull (obj) {
+ return Object.entries(obj).every(([key, value]) => value === null)
+}
+
+function getTokenAddressFromTokenObject (token) {
+ return Object.values(token)[0].address.toLowerCase()
+}
+
+/**
+ * Safely checksumms a potentially-null address
+ *
+ * @param {String} [address] - address to checksum
+ * @param {String} [network] - network id
+ * @returns {String} - checksummed address
+ *
+ */
+function checksumAddress (address, network) {
+ const checksummed = address ? ethUtil.toChecksumAddress(address) : ''
+ return checksummed
+}
+
+function addressSlicer (address = '') {
+ if (address.length < 11) {
+ return address
+ }
+
+ return `${address.slice(0, 6)}...${address.slice(-4)}`
+}
diff --git a/ui/app/higher-order-components/authenticated/authenticated.component.js b/ui/app/higher-order-components/authenticated/authenticated.component.js
deleted file mode 100644
index 7b64d4895..000000000
--- a/ui/app/higher-order-components/authenticated/authenticated.component.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { Redirect, Route } from 'react-router-dom'
-import { UNLOCK_ROUTE, INITIALIZE_ROUTE } from '../../routes'
-
-export default function Authenticated (props) {
- const { isUnlocked, completedOnboarding } = props
-
- switch (true) {
- case isUnlocked && completedOnboarding:
- return <Route { ...props } />
- case !completedOnboarding:
- return <Redirect to={{ pathname: INITIALIZE_ROUTE }} />
- default:
- return <Redirect to={{ pathname: UNLOCK_ROUTE }} />
- }
-}
-
-Authenticated.propTypes = {
- isUnlocked: PropTypes.bool,
- completedOnboarding: PropTypes.bool,
-}
diff --git a/ui/app/higher-order-components/initialized/initialized.component.js b/ui/app/higher-order-components/initialized/initialized.component.js
deleted file mode 100644
index 0736ceff4..000000000
--- a/ui/app/higher-order-components/initialized/initialized.component.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { Redirect, Route } from 'react-router-dom'
-import { INITIALIZE_ROUTE } from '../../routes'
-
-export default function Initialized (props) {
- return props.completedOnboarding
- ? <Route { ...props } />
- : <Redirect to={{ pathname: INITIALIZE_ROUTE }} />
-}
-
-Initialized.propTypes = {
- completedOnboarding: PropTypes.bool,
-}
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
deleted file mode 100644
index 08b9083e1..000000000
--- a/ui/app/higher-order-components/with-method-data/with-method-data.component.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { getMethodData, getFourBytePrefix } from '../../helpers/transactions.util'
-
-export default function withMethodData (WrappedComponent) {
- return class MethodDataWrappedComponent extends PureComponent {
- static propTypes = {
- transaction: PropTypes.object,
- knownMethodData: PropTypes.object,
- addKnownMethodData: PropTypes.func,
- }
-
- static defaultProps = {
- transaction: {},
- knownMethodData: {},
- }
-
- state = {
- methodData: {},
- done: false,
- error: null,
- }
-
- componentDidMount () {
- this.fetchMethodData()
- }
-
- async fetchMethodData () {
- const { transaction, knownMethodData, addKnownMethodData } = this.props
- const { txParams: { data = '' } = {} } = transaction
-
- if (data) {
- try {
- let methodData
- const fourBytePrefix = getFourBytePrefix(data)
- if (fourBytePrefix in knownMethodData) {
- methodData = knownMethodData[fourBytePrefix]
- } else {
- methodData = await getMethodData(data)
- if (!Object.entries(methodData).length === 0) {
- addKnownMethodData(fourBytePrefix, methodData)
- }
- }
-
- 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/with-modal-props.js b/ui/app/higher-order-components/with-modal-props/with-modal-props.js
deleted file mode 100644
index 02f3855af..000000000
--- a/ui/app/higher-order-components/with-modal-props/with-modal-props.js
+++ /dev/null
@@ -1,21 +0,0 @@
-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/i18n-provider.js b/ui/app/i18n-provider.js
deleted file mode 100644
index 3419474c4..000000000
--- a/ui/app/i18n-provider.js
+++ /dev/null
@@ -1,55 +0,0 @@
-const { Component } = require('react')
-const connect = require('react-redux').connect
-const PropTypes = require('prop-types')
-const { withRouter } = require('react-router-dom')
-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
- return {
- 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)
- },
- }
- }
-
- render () {
- return this.props.children
- }
-}
-
-I18nProvider.propTypes = {
- localeMessages: PropTypes.object,
- children: PropTypes.object,
-}
-
-I18nProvider.childContextTypes = {
- t: PropTypes.func,
- tOrDefault: PropTypes.func,
- tOrKey: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const { localeMessages } = state
- return {
- localeMessages,
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(I18nProvider)
-
diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png
deleted file mode 100644
index 1742a32b8..000000000
--- a/ui/app/img/identicon-tardigrade.png
+++ /dev/null
Binary files differ
diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png
deleted file mode 100644
index d58fae912..000000000
--- a/ui/app/img/identicon-walrus.png
+++ /dev/null
Binary files differ
diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js
deleted file mode 100644
index 5ab5d4c33..000000000
--- a/ui/app/keychains/hd/create-vault-complete.js
+++ /dev/null
@@ -1,91 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const actions = require('../../actions')
-const exportAsFile = require('../../util').exportAsFile
-
-module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
-
-inherits(CreateVaultCompleteScreen, Component)
-function CreateVaultCompleteScreen () {
- Component.call(this)
-}
-
-function mapStateToProps (state) {
- return {
- seed: state.appState.currentView.seedWords,
- cachedSeed: state.metamask.seedWords,
- }
-}
-
-CreateVaultCompleteScreen.prototype.render = function () {
- var state = this.props
- var seed = state.seed || state.cachedSeed || ''
-
- return (
-
- h('.initialize-screen.flex-column.flex-center.flex-grow', [
-
- // // subtitle and nav
- // h('.section-title.flex-row.flex-center', [
- // h('h2.page-subtitle', 'Vault Created'),
- // ]),
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginTop: 36,
- marginBottom: 8,
- width: '100%',
- fontSize: '20px',
- padding: 6,
- },
- }, [
- 'Vault Created',
- ]),
-
- h('div', {
- style: {
- fontSize: '1em',
- marginTop: '10px',
- textAlign: 'center',
- },
- }, [
- h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
- ]),
-
- h('textarea.twelve-word-phrase', {
- readOnly: true,
- value: seed,
- }),
-
- h('button.primary', {
- onClick: () => this.confirmSeedWords()
- .then(account => this.showAccountDetail(account)),
- style: {
- margin: '24px',
- fontSize: '0.9em',
- marginBottom: '10px',
- },
- }, 'I\'ve copied it somewhere safe'),
-
- h('button.primary', {
- onClick: () => exportAsFile(`MetaMask Seed Words`, seed),
- style: {
- margin: '10px',
- fontSize: '0.9em',
- },
- }, 'Save Seed Words As File'),
- ])
- )
-}
-
-CreateVaultCompleteScreen.prototype.confirmSeedWords = function () {
- return this.props.dispatch(actions.confirmSeedWords())
-}
-
-CreateVaultCompleteScreen.prototype.showAccountDetail = function (account) {
- return this.props.dispatch(actions.showAccountDetail(account))
-}
diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js
deleted file mode 100644
index 913d20505..000000000
--- a/ui/app/keychains/hd/restore-vault.js
+++ /dev/null
@@ -1,181 +0,0 @@
-const inherits = require('util').inherits
-const PropTypes = require('prop-types')
-const PersistentForm = require('../../../lib/persistent-form')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const actions = require('../../actions')
-const log = require('loglevel')
-
-RestoreVaultScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(RestoreVaultScreen)
-
-
-inherits(RestoreVaultScreen, PersistentForm)
-function RestoreVaultScreen () {
- PersistentForm.call(this)
-}
-
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- forgottenPassword: state.appState.forgottenPassword,
- }
-}
-
-RestoreVaultScreen.prototype.render = function () {
- var state = this.props
- this.persistentFormParentId = 'restore-vault-form'
-
- return (
-
- h('.initialize-screen.flex-column.flex-center.flex-grow', [
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginBottom: 24,
- width: '100%',
- fontSize: '20px',
- padding: 6,
- },
- }, [
- this.context.t('restoreVault'),
- ]),
-
- // wallet seed entry
- h('h3', this.context.t('walletSeed')),
- h('textarea.twelve-word-phrase.letter-spacey', {
- dataset: {
- persistentFormId: 'wallet-seed',
- },
- placeholder: this.context.t('secretPhrase'),
- }),
-
- // password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box',
- placeholder: this.context.t('newPassword8Chars'),
- dataset: {
- persistentFormId: 'password',
- },
- style: {
- width: 260,
- marginTop: 12,
- },
- }),
-
- // confirm password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box-confirm',
- placeholder: this.context.t('confirmPassword'),
- onKeyPress: this.createOnEnter.bind(this),
- dataset: {
- persistentFormId: 'password-confirmation',
- },
- style: {
- width: 260,
- marginTop: 16,
- },
- }),
-
- (state.warning) && (
- h('span.error.in-progress-notification', state.warning)
- ),
-
- // submit
-
- h('.flex-row.flex-space-between', {
- style: {
- marginTop: 30,
- width: '50%',
- },
- }, [
-
- // cancel
- h('button.primary', {
- onClick: this.showInitializeMenu.bind(this),
- style: {
- textTransform: 'uppercase',
- },
- }, this.context.t('cancel')),
-
- // submit
- h('button.primary', {
- onClick: this.createNewVaultAndRestore.bind(this),
- style: {
- textTransform: 'uppercase',
- },
- }, this.context.t('ok')),
- ]),
- ])
- )
-}
-
-RestoreVaultScreen.prototype.showInitializeMenu = function () {
- const { dispatch, forgottenPassword } = this.props
- dispatch(actions.unMarkPasswordForgotten())
- .then(() => {
- if (forgottenPassword) {
- dispatch(actions.backToUnlockView())
- } else {
- dispatch(actions.showInitializeMenu())
- }
- })
-}
-
-RestoreVaultScreen.prototype.createOnEnter = function (event) {
- if (event.key === 'Enter') {
- this.createNewVaultAndRestore()
- }
-}
-
-RestoreVaultScreen.prototype.createNewVaultAndRestore = function () {
- // check password
- var passwordBox = document.getElementById('password-box')
- var password = passwordBox.value
- var passwordConfirmBox = document.getElementById('password-box-confirm')
- var passwordConfirm = passwordConfirmBox.value
- if (password.length < 8) {
- this.warning = this.context.t('passwordNotLongEnough')
-
- this.props.dispatch(actions.displayWarning(this.warning))
- return
- }
- if (password !== passwordConfirm) {
- this.warning = this.context.t('passwordsDontMatch')
- this.props.dispatch(actions.displayWarning(this.warning))
- return
- }
- // check seed
- var seedBox = document.querySelector('textarea.twelve-word-phrase')
- var seed = seedBox.value.trim()
-
- // true if the string has more than a space between words.
- if (seed.split(' ').length > 1) {
- this.warning = this.context.t('spaceBetween')
- this.props.dispatch(actions.displayWarning(this.warning))
- return
- }
- // true if seed contains a character that is not between a-z or a space
- if (!seed.match(/^[a-z ]+$/)) {
- this.warning = this.context.t('loweCaseWords')
- this.props.dispatch(actions.displayWarning(this.warning))
- return
- }
- if (seed.split(' ').length !== 12) {
- this.warning = this.context.t('seedPhraseReq')
- this.props.dispatch(actions.displayWarning(this.warning))
- return
- }
- // submit
- this.warning = null
- this.props.dispatch(actions.displayWarning(this.warning))
- this.props.dispatch(actions.createNewVaultAndRestore(password, seed))
- .catch(err => log.error(err.message))
-}
diff --git a/ui/app/metametrics/metametrics.provider.js b/ui/app/metametrics/metametrics.provider.js
deleted file mode 100644
index 5ff0294e5..000000000
--- a/ui/app/metametrics/metametrics.provider.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { Component } from 'react'
-import { connect } from 'react-redux'
-import PropTypes from 'prop-types'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import {
- getCurrentNetworkId,
- getSelectedAsset,
- getAccountType,
- getNumberOfAccounts,
- getNumberOfTokens,
-} from '../selectors'
-import {
- txDataSelector,
-} from '../selectors/confirm-transaction'
-import { getEnvironmentType } from '../../../app/scripts/lib/util'
-import {
- sendMetaMetricsEvent,
- sendCountIsTrackable,
-} from './metametrics.util'
-
-class MetaMetricsProvider extends Component {
- static propTypes = {
- network: PropTypes.string.isRequired,
- environmentType: PropTypes.string.isRequired,
- activeCurrency: PropTypes.string.isRequired,
- accountType: PropTypes.string.isRequired,
- metaMetricsSendCount: PropTypes.number.isRequired,
- children: PropTypes.object.isRequired,
- history: PropTypes.object.isRequired,
- }
-
- static childContextTypes = {
- metricsEvent: PropTypes.func,
- }
-
- constructor (props) {
- super(props)
-
- this.state = {
- previousPath: '',
- currentPath: window.location.href,
- }
-
- props.history.listen(locationObj => {
- this.setState({
- previousPath: this.state.currentPath,
- currentPath: window.location.href,
- })
- })
- }
-
- getChildContext () {
- const props = this.props
- const { pathname } = location
- const { previousPath, currentPath } = this.state
-
- return {
- metricsEvent: (config = {}, overrides = {}) => {
- const { eventOpts = {} } = config
- const { name = '' } = eventOpts
- const { pathname: overRidePathName = '' } = overrides
- const isSendFlow = Boolean(name.match(/^send|^confirm/) || overRidePathName.match(/send|confirm/))
-
- if (props.participateInMetaMetrics || config.isOptIn) {
- return sendMetaMetricsEvent({
- ...props,
- ...config,
- previousPath,
- currentPath,
- pathname,
- excludeMetaMetricsId: isSendFlow && !sendCountIsTrackable(props.metaMetricsSendCount + 1),
- ...overrides,
- })
- }
- },
- }
- }
-
- render () {
- return this.props.children
- }
-}
-
-const mapStateToProps = state => {
- const txData = txDataSelector(state) || {}
-
- return {
- network: getCurrentNetworkId(state),
- environmentType: getEnvironmentType(),
- activeCurrency: getSelectedAsset(state),
- accountType: getAccountType(state),
- confirmTransactionOrigin: txData.origin,
- metaMetricsId: state.metamask.metaMetricsId,
- participateInMetaMetrics: state.metamask.participateInMetaMetrics,
- metaMetricsSendCount: state.metamask.metaMetricsSendCount,
- numberOfTokens: getNumberOfTokens(state),
- numberOfAccounts: getNumberOfAccounts(state),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(MetaMetricsProvider)
-
diff --git a/ui/app/pages/add-token/add-token.component.js b/ui/app/pages/add-token/add-token.component.js
new file mode 100644
index 000000000..40c1ff7fd
--- /dev/null
+++ b/ui/app/pages/add-token/add-token.component.js
@@ -0,0 +1,335 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import { checkExistingAddresses } from './util'
+import { tokenInfoGetter } from '../../helpers/utils/token-util'
+import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
+import TextField from '../../components/ui/text-field'
+import TokenList from './token-list'
+import TokenSearch from './token-search'
+import PageContainer from '../../components/ui/page-container'
+import { Tabs, Tab } from '../../components/ui/tabs'
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+const SEARCH_TAB = 'SEARCH'
+const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
+
+class AddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ setPendingTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ tokens: PropTypes.array,
+ identities: PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: 0,
+ searchResults: [],
+ selectedTokens: {},
+ tokenSelectorError: null,
+ customAddressError: null,
+ customSymbolError: null,
+ customDecimalsError: null,
+ autoFilled: false,
+ displayedTab: SEARCH_TAB,
+ forceEditSymbol: false,
+ }
+ }
+
+ componentDidMount () {
+ this.tokenInfoGetter = tokenInfoGetter()
+ const { pendingTokens = {} } = this.props
+ const pendingTokenKeys = Object.keys(pendingTokens)
+
+ if (pendingTokenKeys.length > 0) {
+ let selectedTokens = {}
+ let customToken = {}
+
+ pendingTokenKeys.forEach(tokenAddress => {
+ const token = pendingTokens[tokenAddress]
+ const { isCustom } = token
+
+ if (isCustom) {
+ customToken = { ...token }
+ } else {
+ selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
+ }
+ })
+
+ const {
+ address: customAddress = '',
+ symbol: customSymbol = '',
+ decimals: customDecimals = 0,
+ } = customToken
+
+ const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
+ this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
+ }
+ }
+
+ handleToggleToken (token) {
+ const { address } = token
+ const { selectedTokens = {} } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ tokenSelectorError: null,
+ })
+ }
+
+ hasError () {
+ const {
+ tokenSelectorError,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ } = this.state
+
+ return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
+ }
+
+ hasSelected () {
+ const { customAddress = '', selectedTokens = {} } = this.state
+ return customAddress || Object.keys(selectedTokens).length > 0
+ }
+
+ handleNext () {
+ if (this.hasError()) {
+ return
+ }
+
+ if (!this.hasSelected()) {
+ this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
+ return
+ }
+
+ const { setPendingTokens, history } = this.props
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
+
+ const customToken = {
+ address,
+ symbol,
+ decimals,
+ }
+
+ setPendingTokens({ customToken, selectedTokens })
+ history.push(CONFIRM_ADD_TOKEN_ROUTE)
+ }
+
+ async attemptToAutoFillTokenParams (address) {
+ const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address)
+
+ const autoFilled = Boolean(symbol && decimals)
+ this.setState({ autoFilled })
+ this.handleCustomSymbolChange(symbol || '')
+ this.handleCustomDecimalsChange(decimals)
+ }
+
+ handleCustomAddressChange (value) {
+ const customAddress = value.trim()
+ this.setState({
+ customAddress,
+ customAddressError: null,
+ tokenSelectorError: null,
+ autoFilled: false,
+ })
+
+ const isValidAddress = ethUtil.isValidAddress(customAddress)
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ switch (true) {
+ case !isValidAddress:
+ this.setState({
+ customAddressError: this.context.t('invalidAddress'),
+ customSymbol: '',
+ customDecimals: 0,
+ customSymbolError: null,
+ customDecimalsError: null,
+ })
+
+ break
+ case Boolean(this.props.identities[standardAddress]):
+ this.setState({
+ customAddressError: this.context.t('personalAddressDetected'),
+ })
+
+ break
+ case checkExistingAddresses(customAddress, this.props.tokens):
+ this.setState({
+ customAddressError: this.context.t('tokenAlreadyAdded'),
+ })
+
+ break
+ default:
+ if (customAddress !== emptyAddr) {
+ this.attemptToAutoFillTokenParams(customAddress)
+ }
+ }
+ }
+
+ handleCustomSymbolChange (value) {
+ const customSymbol = value.trim()
+ const symbolLength = customSymbol.length
+ let customSymbolError = null
+
+ if (symbolLength <= 0 || symbolLength >= 12) {
+ customSymbolError = this.context.t('symbolBetweenZeroTwelve')
+ }
+
+ this.setState({ customSymbol, customSymbolError })
+ }
+
+ handleCustomDecimalsChange (value) {
+ const customDecimals = value.trim()
+ const validDecimals = customDecimals !== null &&
+ customDecimals !== '' &&
+ customDecimals >= 0 &&
+ customDecimals <= 36
+ let customDecimalsError = null
+
+ if (!validDecimals) {
+ customDecimalsError = this.context.t('decimalsMustZerotoTen')
+ }
+
+ this.setState({ customDecimals, customDecimalsError })
+ }
+
+ renderCustomTokenForm () {
+ const {
+ customAddress,
+ customSymbol,
+ customDecimals,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ autoFilled,
+ forceEditSymbol,
+ } = this.state
+
+ return (
+ <div className="add-token__custom-token-form">
+ <TextField
+ id="custom-address"
+ label={this.context.t('tokenContractAddress')}
+ type="text"
+ value={customAddress}
+ onChange={e => this.handleCustomAddressChange(e.target.value)}
+ error={customAddressError}
+ fullWidth
+ margin="normal"
+ />
+ <TextField
+ id="custom-symbol"
+ label={(
+ <div className="add-token__custom-symbol__label-wrapper">
+ <span className="add-token__custom-symbol__label">
+ {this.context.t('tokenSymbol')}
+ </span>
+ {(autoFilled && !forceEditSymbol) && (
+ <div
+ className="add-token__custom-symbol__edit"
+ onClick={() => this.setState({ forceEditSymbol: true })}
+ >
+ {this.context.t('edit')}
+ </div>
+ )}
+ </div>
+ )}
+ type="text"
+ value={customSymbol}
+ onChange={e => this.handleCustomSymbolChange(e.target.value)}
+ error={customSymbolError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled && !forceEditSymbol}
+ />
+ <TextField
+ id="custom-decimals"
+ label={this.context.t('decimal')}
+ type="number"
+ value={customDecimals}
+ onChange={e => this.handleCustomDecimalsChange(e.target.value)}
+ error={customDecimalsError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ </div>
+ )
+ }
+
+ renderSearchToken () {
+ const { tokenSelectorError, selectedTokens, searchResults } = this.state
+
+ return (
+ <div className="add-token__search-token">
+ <TokenSearch
+ onSearch={({ results = [] }) => this.setState({ searchResults: results })}
+ error={tokenSelectorError}
+ />
+ <div className="add-token__token-list">
+ <TokenList
+ results={searchResults}
+ selectedTokens={selectedTokens}
+ onToggleToken={token => this.handleToggleToken(token)}
+ />
+ </div>
+ </div>
+ )
+ }
+
+ renderTabs () {
+ return (
+ <Tabs>
+ <Tab name={this.context.t('search')}>
+ { this.renderSearchToken() }
+ </Tab>
+ <Tab name={this.context.t('customToken')}>
+ { this.renderCustomTokenForm() }
+ </Tab>
+ </Tabs>
+ )
+ }
+
+ render () {
+ const { history, clearPendingTokens } = this.props
+
+ return (
+ <PageContainer
+ title={this.context.t('addTokens')}
+ tabsComponent={this.renderTabs()}
+ onSubmit={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ onCancel={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ />
+ )
+ }
+}
+
+export default AddToken
diff --git a/ui/app/pages/add-token/add-token.container.js b/ui/app/pages/add-token/add-token.container.js
new file mode 100644
index 000000000..eee16dfc7
--- /dev/null
+++ b/ui/app/pages/add-token/add-token.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import AddToken from './add-token.component'
+
+const { setPendingTokens, clearPendingTokens } = require('../../store/actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { identities, tokens, pendingTokens } = metamask
+ return {
+ identities,
+ tokens,
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/pages/add-token/index.js
index 3666cae82..3666cae82 100644
--- a/ui/app/components/pages/add-token/index.js
+++ b/ui/app/pages/add-token/index.js
diff --git a/ui/app/pages/add-token/index.scss b/ui/app/pages/add-token/index.scss
new file mode 100644
index 000000000..ef6802f96
--- /dev/null
+++ b/ui/app/pages/add-token/index.scss
@@ -0,0 +1,45 @@
+@import 'token-list/index';
+
+.add-token {
+ &__custom-token-form {
+ padding: 8px 16px 16px;
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
+
+ &__search-token {
+ padding: 16px;
+ }
+
+ &__token-list {
+ margin-top: 16px;
+ }
+
+ &__custom-symbol {
+
+ &__label-wrapper {
+ display: flex;
+ flex-flow: row nowrap;
+ }
+
+ &__label {
+ flex: 0 0 auto;
+ }
+
+ &__edit {
+ flex: 1 1 auto;
+ text-align: right;
+ color: $curious-blue;
+ padding-right: 4px;
+ cursor: pointer;
+ }
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/pages/add-token/token-list/index.js
index 21dd5ac72..21dd5ac72 100644
--- a/ui/app/components/pages/add-token/token-list/index.js
+++ b/ui/app/pages/add-token/token-list/index.js
diff --git a/ui/app/pages/add-token/token-list/index.scss b/ui/app/pages/add-token/token-list/index.scss
new file mode 100644
index 000000000..b7787a18e
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/index.scss
@@ -0,0 +1,65 @@
+@import 'token-list-placeholder/index';
+
+.token-list {
+ &__title {
+ font-size: .75rem;
+ }
+
+ &__tokens-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__token {
+ transition: 200ms ease-in-out;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: 8px;
+ margin-top: 8px;
+ box-sizing: border-box;
+ border-radius: 10px;
+ cursor: pointer;
+ border: 2px solid transparent;
+ position: relative;
+
+ &:hover {
+ border: 2px solid rgba($malibu-blue, .5);
+ }
+
+ &--selected {
+ border: 2px solid $malibu-blue !important;
+ }
+
+ &--disabled {
+ opacity: .4;
+ pointer-events: none;
+ }
+ }
+
+ &__token-icon {
+ width: 48px;
+ height: 48px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ border-radius: 50%;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .24);
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__token-data {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__token-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/pages/add-token/token-list/token-list-placeholder/index.js
index b82f45e93..b82f45e93 100644
--- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/index.js
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss
index cc495dfb0..cc495dfb0 100644
--- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
index 20f550927..20f550927 100644
--- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/pages/add-token/token-list/token-list.component.js
index 724a68d6e..724a68d6e 100644
--- a/ui/app/components/pages/add-token/token-list/token-list.component.js
+++ b/ui/app/pages/add-token/token-list/token-list.component.js
diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/pages/add-token/token-list/token-list.container.js
index cd7b07a37..cd7b07a37 100644
--- a/ui/app/components/pages/add-token/token-list/token-list.container.js
+++ b/ui/app/pages/add-token/token-list/token-list.container.js
diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/pages/add-token/token-search/index.js
index acaa6b084..acaa6b084 100644
--- a/ui/app/components/pages/add-token/token-search/index.js
+++ b/ui/app/pages/add-token/token-search/index.js
diff --git a/ui/app/pages/add-token/token-search/token-search.component.js b/ui/app/pages/add-token/token-search/token-search.component.js
new file mode 100644
index 000000000..5542a19ff
--- /dev/null
+++ b/ui/app/pages/add-token/token-search/token-search.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import contractMap from 'eth-contract-metadata'
+import Fuse from 'fuse.js'
+import InputAdornment from '@material-ui/core/InputAdornment'
+import TextField from '../../../components/ui/text-field'
+
+const contractList = Object.entries(contractMap)
+ .map(([ _, tokenData]) => tokenData)
+ .filter(tokenData => Boolean(tokenData.erc20))
+
+const fuse = new Fuse(contractList, {
+ shouldSort: true,
+ threshold: 0.45,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ { name: 'name', weight: 0.5 },
+ { name: 'symbol', weight: 0.5 },
+ ],
+})
+
+export default class TokenSearch extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ error: null,
+ }
+
+ static propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ searchQuery: '',
+ }
+ }
+
+ handleSearch (searchQuery) {
+ this.setState({ searchQuery })
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+ this.props.onSearch({ searchQuery, results })
+ }
+
+ renderAdornment () {
+ return (
+ <InputAdornment
+ position="start"
+ style={{ marginRight: '12px' }}
+ >
+ <img src="images/search.svg" />
+ </InputAdornment>
+ )
+ }
+
+ render () {
+ const { error } = this.props
+ const { searchQuery } = this.state
+
+ return (
+ <TextField
+ id="search-tokens"
+ placeholder={this.context.t('searchTokens')}
+ type="text"
+ value={searchQuery}
+ onChange={e => this.handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ startAdornment={this.renderAdornment()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/pages/add-token/util.js
index 579c56cc0..579c56cc0 100644
--- a/ui/app/components/pages/add-token/util.js
+++ b/ui/app/pages/add-token/util.js
diff --git a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
new file mode 100644
index 000000000..7edb8f541
--- /dev/null
+++ b/ui/app/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 '../../helpers/constants/routes'
+import Button from '../../components/ui/button'
+import Identicon from '../../components/ui/identicon'
+import TokenBalance from '../../components/ui/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/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
new file mode 100644
index 000000000..a90fe148f
--- /dev/null
+++ b/ui/app/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('../../store/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/pages/confirm-add-suggested-token/index.js
index 2ca56b43c..2ca56b43c 100644
--- a/ui/app/components/pages/confirm-add-suggested-token/index.js
+++ b/ui/app/pages/confirm-add-suggested-token/index.js
diff --git a/ui/app/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/pages/confirm-add-token/confirm-add-token.component.js
new file mode 100644
index 000000000..c0ec624ac
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/confirm-add-token.component.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
+import Button from '../../components/ui/button'
+import Identicon from '../../components/ui/identicon'
+import TokenBalance from '../../components/ui/token-balance'
+
+export default class ConfirmAddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ }
+
+ 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 { history, addTokens, clearPendingTokens, pendingTokens } = this.props
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addTokens') }
+ </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 } = 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}
+ />
+ <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={() => 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/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/pages/confirm-add-token/confirm-add-token.container.js
new file mode 100644
index 000000000..961626177
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/confirm-add-token.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmAddToken from './confirm-add-token.component'
+
+const { addTokens, clearPendingTokens } = require('../../store/actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens } = metamask
+ return {
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addTokens: tokens => dispatch(addTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/pages/confirm-add-token/index.js
index b7decabec..b7decabec 100644
--- a/ui/app/components/pages/confirm-add-token/index.js
+++ b/ui/app/pages/confirm-add-token/index.js
diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/pages/confirm-add-token/index.scss
index 66146cf78..66146cf78 100644
--- a/ui/app/components/pages/confirm-add-token/index.scss
+++ b/ui/app/pages/confirm-add-token/index.scss
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/pages/confirm-approve/confirm-approve.component.js
index b71eaa1d4..b71eaa1d4 100644
--- a/ui/app/components/pages/confirm-approve/confirm-approve.component.js
+++ b/ui/app/pages/confirm-approve/confirm-approve.component.js
diff --git a/ui/app/pages/confirm-approve/confirm-approve.container.js b/ui/app/pages/confirm-approve/confirm-approve.container.js
new file mode 100644
index 000000000..5f8bb8f0b
--- /dev/null
+++ b/ui/app/pages/confirm-approve/confirm-approve.container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import ConfirmApprove from './confirm-approve.component'
+import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
+ const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ tokenSymbol,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmApprove)
diff --git a/ui/app/components/pages/confirm-approve/index.js b/ui/app/pages/confirm-approve/index.js
index 791297be7..791297be7 100644
--- a/ui/app/components/pages/confirm-approve/index.js
+++ b/ui/app/pages/confirm-approve/index.js
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
index 9bc0daab9..9bc0daab9 100644
--- a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
+++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
index 336ee83ea..336ee83ea 100644
--- a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
+++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
diff --git a/ui/app/components/pages/confirm-deploy-contract/index.js b/ui/app/pages/confirm-deploy-contract/index.js
index c4fb01b52..c4fb01b52 100644
--- a/ui/app/components/pages/confirm-deploy-contract/index.js
+++ b/ui/app/pages/confirm-deploy-contract/index.js
diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js
new file mode 100644
index 000000000..8daad675e
--- /dev/null
+++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js
@@ -0,0 +1,39 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import { SEND_ROUTE } from '../../helpers/constants/routes'
+
+export default class ConfirmSendEther extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ editTransaction: PropTypes.func,
+ history: PropTypes.object,
+ txParams: PropTypes.object,
+ }
+
+ handleEdit ({ txData }) {
+ const { editTransaction, history } = this.props
+ editTransaction(txData)
+ history.push(SEND_ROUTE)
+ }
+
+ shouldHideData () {
+ const { txParams = {} } = this.props
+ return !txParams.data
+ }
+
+ render () {
+ const hideData = this.shouldHideData()
+
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('confirm')}
+ hideData={hideData}
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js
new file mode 100644
index 000000000..713da702d
--- /dev/null
+++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import { updateSend } from '../../store/actions'
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import ConfirmSendEther from './confirm-send-ether.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData: { txParams } = {} } } = state
+
+ return {
+ txParams,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: txData => {
+ const { id, txParams } = txData
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ value: amount,
+ } = txParams
+
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ }))
+
+ dispatch(clearConfirmTransaction())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendEther)
diff --git a/ui/app/components/pages/confirm-send-ether/index.js b/ui/app/pages/confirm-send-ether/index.js
index 2d5767c39..2d5767c39 100644
--- a/ui/app/components/pages/confirm-send-ether/index.js
+++ b/ui/app/pages/confirm-send-ether/index.js
diff --git a/ui/app/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/pages/confirm-send-token/confirm-send-token.component.js
new file mode 100644
index 000000000..7f3b1c082
--- /dev/null
+++ b/ui/app/pages/confirm-send-token/confirm-send-token.component.js
@@ -0,0 +1,29 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import { SEND_ROUTE } from '../../helpers/constants/routes'
+
+export default class ConfirmSendToken extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ editTransaction: PropTypes.func,
+ tokenAmount: PropTypes.number,
+ }
+
+ handleEdit (confirmTransactionData) {
+ const { editTransaction, history } = this.props
+ editTransaction(confirmTransactionData)
+ history.push(SEND_ROUTE)
+ }
+
+ render () {
+ const { tokenAmount } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ tokenAmount={tokenAmount}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/pages/confirm-send-token/confirm-send-token.container.js
new file mode 100644
index 000000000..db9b08c48
--- /dev/null
+++ b/ui/app/pages/confirm-send-token/confirm-send-token.container.js
@@ -0,0 +1,52 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import ConfirmSendToken from './confirm-send-token.component'
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import { setSelectedToken, updateSend, showSendTokenPage } from '../../store/actions'
+import { conversionUtil } from '../../helpers/utils/conversion-util'
+import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: ({ txData, tokenData, tokenProps }) => {
+ const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
+ const { params = [] } = tokenData
+ const { value: to } = params[0] || {}
+ const { value: tokenAmountInDec } = params[1] || {}
+ const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+ dispatch(setSelectedToken(tokenAddress))
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount: tokenAmountInHex,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ token: {
+ ...tokenProps,
+ address: tokenAddress,
+ },
+ }))
+ dispatch(clearConfirmTransaction())
+ dispatch(showSendTokenPage())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendToken)
diff --git a/ui/app/components/pages/confirm-send-token/index.js b/ui/app/pages/confirm-send-token/index.js
index 409b6ef3d..409b6ef3d 100644
--- a/ui/app/components/pages/confirm-send-token/index.js
+++ b/ui/app/pages/confirm-send-token/index.js
diff --git a/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
new file mode 100644
index 000000000..dbda3c1dc
--- /dev/null
+++ b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
+import {
+ formatCurrency,
+ convertTokenToFiat,
+ addFiat,
+ roundExponential,
+} from '../../helpers/utils/confirm-tx.util'
+import { getWeiHexFromDecimalValue } from '../../helpers/utils/conversions.util'
+import { ETH, PRIMARY } from '../../helpers/constants/common'
+
+export default class ConfirmTokenTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ contractExchangeRate: PropTypes.number,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ }
+
+ getFiatTransactionAmount () {
+ const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
+
+ return convertTokenToFiat({
+ value: tokenAmount,
+ toCurrency: currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ })
+ }
+
+ renderSubtitleComponent () {
+ const { contractExchangeRate, tokenAmount } = this.props
+
+ const decimalEthValue = (tokenAmount * contractExchangeRate) || 0
+ const hexWeiValue = getWeiHexFromDecimalValue({
+ value: decimalEthValue,
+ fromCurrency: ETH,
+ fromDenomination: ETH,
+ })
+
+ return typeof contractExchangeRate === 'undefined'
+ ? (
+ <span>
+ { this.context.t('noConversionRateAvailable') }
+ </span>
+ ) : (
+ <UserPreferencedCurrencyDisplay
+ value={hexWeiValue}
+ type={PRIMARY}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+
+ renderPrimaryTotalTextOverride () {
+ const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <div>
+ <span>{ `${tokensText} + ` }</span>
+ <img
+ src="/images/eth.svg"
+ height="18"
+ />
+ <span>{ ethTransactionTotal }</span>
+ </div>
+ )
+ }
+
+ getSecondaryTotalTextOverride () {
+ const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return formatCurrency(fiatTransactionTotal, currentCurrency)
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
+ const roundedFiatTotal = roundExponential(fiatTotal)
+ return formatCurrency(roundedFiatTotal, currentCurrency)
+ }
+ }
+
+ render () {
+ const {
+ toAddress,
+ tokenAddress,
+ tokenSymbol,
+ tokenAmount,
+ ...restProps
+ } = this.props
+
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={tokensText}
+ subtitleComponent={this.renderSubtitleComponent()}
+ primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()}
+ secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()}
+ {...restProps}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
new file mode 100644
index 000000000..f5f30a460
--- /dev/null
+++ b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux'
+import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
+import {
+ tokenAmountAndToAddressSelector,
+ contractExchangeRateSelector,
+} from '../../selectors/confirm-transaction'
+
+const mapStateToProps = (state, ownProps) => {
+ const { tokenAmount: ownTokenAmount } = ownProps
+ const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
+ const {
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ tokenProps: { tokenSymbol } = {},
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ } = confirmTransaction
+
+ const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
+ const contractExchangeRate = contractExchangeRateSelector(state)
+
+ return {
+ toAddress,
+ tokenAddress,
+ tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
+ tokenSymbol,
+ currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTokenTransactionBase)
diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/pages/confirm-token-transaction-base/index.js
index e15c5d56b..e15c5d56b 100644
--- a/ui/app/components/pages/confirm-token-transaction-base/index.js
+++ b/ui/app/pages/confirm-token-transaction-base/index.js
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
new file mode 100644
index 000000000..9e749322f
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,592 @@
+import ethUtil from 'ethereumjs-util'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container'
+import { isBalanceSufficient } from '../../components/app/send/send.utils'
+import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ TRANSACTION_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
+import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../helpers/constants/common'
+import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'
+
+export default class ConfirmTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ // react-router props
+ match: PropTypes.object,
+ history: PropTypes.object,
+ // Redux props
+ balance: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ cancelAllTransactions: PropTypes.func,
+ clearConfirmTransaction: PropTypes.func,
+ clearSend: PropTypes.func,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ editTransaction: PropTypes.func,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ hexTransactionAmount: PropTypes.string,
+ hexTransactionFee: PropTypes.string,
+ hexTransactionTotal: PropTypes.string,
+ 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,
+ currentNetworkUnapprovedTxs: PropTypes.object,
+ updateGasAndCalculate: PropTypes.func,
+ customGas: PropTypes.object,
+ // Component props
+ action: PropTypes.string,
+ contentComponent: PropTypes.node,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ secondaryTotalTextOverride: PropTypes.string,
+ hideData: PropTypes.bool,
+ hideDetails: PropTypes.bool,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ onCancel: PropTypes.func,
+ onEdit: PropTypes.func,
+ onEditGas: PropTypes.func,
+ onSubmit: PropTypes.func,
+ setMetaMetricsSendCount: PropTypes.func,
+ metaMetricsSendCount: PropTypes.number,
+ subtitle: PropTypes.string,
+ subtitleComponent: PropTypes.node,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.string,
+ titleComponent: PropTypes.node,
+ valid: PropTypes.bool,
+ warning: PropTypes.string,
+ advancedInlineGasShown: PropTypes.bool,
+ insufficientBalance: PropTypes.bool,
+ hideFiatConversion: PropTypes.bool,
+ }
+
+ state = {
+ submitting: false,
+ submitError: null,
+ }
+
+ componentDidUpdate () {
+ const {
+ transactionStatus,
+ showTransactionConfirmedModal,
+ history,
+ clearConfirmTransaction,
+ } = this.props
+
+ if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
+ showTransactionConfirmedModal({
+ onSubmit: () => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+
+ return
+ }
+ }
+
+ getErrorKey () {
+ const {
+ balance,
+ conversionRate,
+ hexTransactionFee,
+ txData: {
+ simulationFails,
+ txParams: {
+ value: amount,
+ } = {},
+ } = {},
+ } = this.props
+
+ const insufficientBalance = balance && !isBalanceSufficient({
+ amount,
+ gasTotal: hexTransactionFee || '0x0',
+ balance,
+ conversionRate,
+ })
+
+ if (insufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ if (simulationFails) {
+ return {
+ valid: true,
+ errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+
+ handleEditGas () {
+ const { onEditGas, showCustomizeGasModal, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'User clicks "Edit" on gas',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ if (onEditGas) {
+ onEditGas()
+ } else {
+ showCustomizeGasModal()
+ }
+ }
+
+ renderDetails () {
+ const {
+ detailsComponent,
+ primaryTotalTextOverride,
+ secondaryTotalTextOverride,
+ hexTransactionFee,
+ hexTransactionTotal,
+ hideDetails,
+ advancedInlineGasShown,
+ customGas,
+ insufficientBalance,
+ updateGasAndCalculate,
+ hideFiatConversion,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ value={hexTransactionFee}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : ''}
+ />
+ {advancedInlineGasShown
+ ? <AdvancedGasInputs
+ updateCustomGasPrice={newGasPrice => updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })}
+ updateCustomGasLimit={newGasLimit => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit })}
+ customGasPrice={customGas.gasPrice}
+ customGasLimit={customGas.gasLimit}
+ insufficientBalance={insufficientBalance}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ />
+ : null
+ }
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ value={hexTransactionTotal}
+ primaryText={primaryTotalTextOverride}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : secondaryTotalTextOverride}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ primaryValueTextColor="#2f9ae0"
+ />
+ </div>
+ </div>
+ )
+ )
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ methodData: {
+ name,
+ params,
+ } = {},
+ hideData,
+ dataComponent,
+ } = this.props
+
+ if (hideData) {
+ return null
+ }
+
+ return dataComponent || (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('functionType')}:`}
+ <span className="confirm-page-container-content__function-type">
+ { name || t('notFound') }
+ </span>
+ </div>
+ {
+ params && (
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('parameters')}:` }
+ </div>
+ <div>
+ <pre>{ JSON.stringify(params, null, 2) }</pre>
+ </div>
+ </div>
+ )
+ }
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`}
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ handleEdit () {
+ const { txData, tokenData, tokenProps, onEdit, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Edit Transaction',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ 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 { metricsEvent } = this.context
+ const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, action, txData: { origin }, methodData = {} } = this.props
+
+ if (onCancel) {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Cancel',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+ onCancel(txData)
+ } else {
+ cancelTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ handleSubmit () {
+ const { metricsEvent } = this.context
+ const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, action, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props
+ const { submitting } = this.state
+
+ if (submitting) {
+ return
+ }
+
+ this.setState({
+ submitting: true,
+ submitError: null,
+ }, () => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Transaction Completed',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ setMetaMetricsSendCount(metaMetricsSendCount + 1)
+ .then(() => {
+ if (onSubmit) {
+ Promise.resolve(onSubmit(txData))
+ .then(() => {
+ this.setState({
+ submitting: false,
+ })
+ })
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ this.setState({
+ submitting: false,
+ }, () => {
+ history.push(DEFAULT_ROUTE)
+ })
+ })
+ .catch(error => {
+ this.setState({
+ submitting: false,
+ submitError: error.message,
+ })
+ })
+ }
+ })
+ })
+ }
+
+ renderTitleComponent () {
+ const { title, titleComponent, hexTransactionAmount } = this.props
+
+ // Title string passed in by props takes priority
+ if (title) {
+ return null
+ }
+
+ return titleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={PRIMARY}
+ showEthLogo
+ ethLogoHeight="26"
+ hideLabel
+ />
+ )
+ }
+
+ renderSubtitleComponent () {
+ const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
+
+ // Subtitle string passed in by props takes priority
+ if (subtitle) {
+ return null
+ }
+
+ return subtitleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={SECONDARY}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+
+ handleNextTx (txId) {
+ const { history, clearConfirmTransaction } = this.props
+ if (txId) {
+ clearConfirmTransaction()
+ history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`)
+ }
+ }
+
+ getNavigateTxData () {
+ const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props
+ const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs).reverse()
+ const currentPosition = enumUnapprovedTxs.indexOf(id.toString())
+
+ return {
+ totalTx: enumUnapprovedTxs.length,
+ positionOfCurrentTx: currentPosition + 1,
+ nextTxId: enumUnapprovedTxs[currentPosition + 1],
+ prevTxId: enumUnapprovedTxs[currentPosition - 1],
+ showNavigation: enumUnapprovedTxs.length > 1,
+ firstTx: enumUnapprovedTxs[0],
+ lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1],
+ ofText: this.context.t('ofTextNofM'),
+ requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'),
+ }
+ }
+
+ componentDidMount () {
+ const { txData: { origin, id } = {}, cancelTransaction } = this.props
+ const { metricsEvent } = this.context
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Confirm: Started',
+ },
+ customVariables: {
+ origin,
+ },
+ })
+
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
+ window.onbeforeunload = () => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Cancel Tx Via Notification Close',
+ },
+ customVariables: {
+ origin,
+ },
+ })
+ cancelTransaction({ id })
+ }
+ }
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ valid: propsValid = true,
+ errorMessage,
+ errorKey: propsErrorKey,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ assetImage,
+ warning,
+ unapprovedTxCount,
+ } = this.props
+ const { submitting, submitError } = this.state
+
+ const { name } = methodData
+ const { valid, errorKey } = this.getErrorKey()
+ const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
+
+ return (
+ <ConfirmPageContainer
+ fromName={fromName}
+ fromAddress={fromAddress}
+ toName={toName}
+ toAddress={toAddress}
+ showEdit={onEdit && !isTxReprice}
+ action={action || getMethodName(name) || this.context.t('contractInteraction')}
+ title={title}
+ titleComponent={this.renderTitleComponent()}
+ subtitle={subtitle}
+ subtitleComponent={this.renderSubtitleComponent()}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={this.renderDetails()}
+ dataComponent={this.renderData()}
+ contentComponent={contentComponent}
+ nonce={nonce}
+ unapprovedTxCount={unapprovedTxCount}
+ assetImage={assetImage}
+ identiconAddress={identiconAddress}
+ errorMessage={errorMessage || submitError}
+ errorKey={propsErrorKey || errorKey}
+ warning={warning}
+ totalTx={totalTx}
+ positionOfCurrentTx={positionOfCurrentTx}
+ nextTxId={nextTxId}
+ prevTxId={prevTxId}
+ showNavigation={showNavigation}
+ onNextTx={(txId) => this.handleNextTx(txId)}
+ firstTx={firstTx}
+ lastTx={lastTx}
+ ofText={ofText}
+ requestsWaitingText={requestsWaitingText}
+ disabled={!propsValid || !valid || submitting}
+ onEdit={() => this.handleEdit()}
+ onCancelAll={() => this.handleCancelAll()}
+ onCancel={() => this.handleCancel()}
+ onSubmit={() => this.handleSubmit()}
+ />
+ )
+ }
+}
+
+export function getMethodName (camelCase) {
+ if (!camelCase || typeof camelCase !== 'string') {
+ return ''
+ }
+
+ return camelCase
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ .replace(/([A-Z])([a-z])/g, ' $1$2')
+ .replace(/ +/g, ' ')
+}
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
new file mode 100644
index 000000000..83543f1a4
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,242 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+import contractMap from 'eth-contract-metadata'
+import ConfirmTransactionBase from './confirm-transaction-base.component'
+import {
+ clearConfirmTransaction,
+ updateGasAndCalculate,
+} from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../store/actions'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util'
+import { isBalanceSufficient, calcGasTotal } from '../../components/app/send/send.utils'
+import { conversionGreaterThan } from '../../helpers/utils/conversion-util'
+import { MIN_GAS_LIMIT_DEC } from '../../components/app/send/send.constants'
+import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util'
+import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../selectors/selectors'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const mapStateToProps = (state, props) => {
+ const { toAddress: propsToAddress } = props
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const { confirmTransaction, metamask, gas } = state
+ const {
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ tokenData,
+ methodData,
+ txData,
+ tokenProps,
+ nonce,
+ } = confirmTransaction
+ const { txParams = {}, lastGasPrice, id: transactionId } = txData
+ const {
+ from: fromAddress,
+ to: txParamsToAddress,
+ gasPrice,
+ gas: gasLimit,
+ value: amount,
+ } = txParams
+ const accounts = getMetaMaskAccounts(state)
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ selectedAddress,
+ selectedAddressTxList,
+ assetImages,
+ network,
+ unapprovedTxs,
+ metaMetricsSendCount,
+ } = metamask
+ const assetImage = assetImages[txParamsToAddress]
+
+ const {
+ customGasLimit,
+ customGasPrice,
+ } = gas
+
+ const { balance } = accounts[selectedAddress]
+ const { name: fromName } = identities[selectedAddress]
+ const toAddress = propsToAddress || txParamsToAddress
+ const toName = identities[toAddress]
+ ? identities[toAddress].name
+ : (
+ casedContractMap[toAddress]
+ ? casedContractMap[toAddress].name
+ : addressSlicer(checksumAddress(toAddress))
+ )
+
+ const isTxReprice = Boolean(lastGasPrice)
+
+ const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ const currentNetworkUnapprovedTxs = R.filter(
+ ({ metamaskNetworkId }) => metamaskNetworkId === network,
+ unapprovedTxs,
+ )
+ const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount,
+ gasTotal: calcGasTotal(gasLimit, gasPrice),
+ balance,
+ conversionRate,
+ })
+
+ return {
+ balance,
+ fromAddress,
+ fromName,
+ toAddress,
+ toName,
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ txData,
+ tokenData,
+ methodData,
+ tokenProps,
+ isTxReprice,
+ currentCurrency,
+ conversionRate,
+ transactionStatus,
+ nonce,
+ assetImage,
+ unapprovedTxs,
+ unapprovedTxCount,
+ currentNetworkUnapprovedTxs,
+ customGas: {
+ gasLimit: customGasLimit || gasLimit,
+ gasPrice: customGasPrice || gasPrice,
+ },
+ advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ insufficientBalance,
+ hideSubtitle: (!isMainnet && !showFiatInTestnets),
+ hideFiatConversion: (!isMainnet && !showFiatInTestnets),
+ metaMetricsSendCount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ clearSend: () => dispatch(clearSend()),
+ showTransactionConfirmedModal: ({ onSubmit }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
+ },
+ showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
+ return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }))
+ },
+ 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)),
+ setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
+ }
+}
+
+const getValidateEditGas = ({ balance, conversionRate, txData }) => {
+ const { txParams: { value: amount } = {} } = txData
+
+ return ({ gasLimit, gasPrice }) => {
+ const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
+ const hasSufficientBalance = isBalanceSufficient({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ if (!hasSufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { balance, conversionRate, txData, unapprovedTxs } = stateProps
+ const {
+ cancelAllTransactions: dispatchCancelAllTransactions,
+ showCustomizeGasModal: dispatchShowCustomizeGasModal,
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
+ txData,
+ onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas),
+ validate: validateEditGas,
+ }),
+ cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(ConfirmTransactionBase)
diff --git a/ui/app/components/pages/confirm-transaction-base/index.js b/ui/app/pages/confirm-transaction-base/index.js
index 9996e9aeb..9996e9aeb 100644
--- a/ui/app/components/pages/confirm-transaction-base/index.js
+++ b/ui/app/pages/confirm-transaction-base/index.js
diff --git a/ui/app/components/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
index 8ca7ca4e7..8ca7ca4e7 100644
--- a/ui/app/components/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
+++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
new file mode 100644
index 000000000..cd471b822
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -0,0 +1,92 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Redirect } from 'react-router-dom'
+import Loading from '../../components/ui/loading-screen'
+import {
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../helpers/constants/routes'
+import { isConfirmDeployContract } from '../../helpers/utils/transactions.util'
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+} from '../../helpers/constants/transactions'
+
+export default class ConfirmTransactionSwitch extends Component {
+ static propTypes = {
+ txData: PropTypes.object,
+ methodData: PropTypes.object,
+ fetchingData: PropTypes.bool,
+ isEtherTransaction: PropTypes.bool,
+ }
+
+ redirectToTransaction () {
+ const {
+ txData,
+ methodData: { name },
+ fetchingData,
+ isEtherTransaction,
+ } = this.props
+ const { id, txParams: { data } = {} } = txData
+
+ if (fetchingData) {
+ return <Loading />
+ }
+
+ if (isConfirmDeployContract(txData)) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (isEtherTransaction) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (data) {
+ const methodName = name && name.toLowerCase()
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_APPROVE: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_TRANSFER_FROM: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ default: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ }
+ }
+
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ render () {
+ const { txData } = this.props
+
+ if (txData.txParams) {
+ return this.redirectToTransaction()
+ } else if (txData.msgParams) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ return <Loading />
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
index 7f2c36af2..7f2c36af2 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
index 536aa5212..536aa5212 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
diff --git a/ui/app/components/pages/confirm-transaction-switch/index.js b/ui/app/pages/confirm-transaction-switch/index.js
index c288acb1a..c288acb1a 100644
--- a/ui/app/components/pages/confirm-transaction-switch/index.js
+++ b/ui/app/pages/confirm-transaction-switch/index.js
diff --git a/ui/app/pages/confirm-transaction/conf-tx.js b/ui/app/pages/confirm-transaction/conf-tx.js
new file mode 100644
index 000000000..f9af6624e
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/conf-tx.js
@@ -0,0 +1,225 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const actions = require('../../store/actions')
+const txHelper = require('../../../lib/tx-helper')
+const log = require('loglevel')
+const R = require('ramda')
+
+const SignatureRequest = require('../../components/app/signature-request')
+const Loading = require('../../components/ui/loading-screen')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../selectors/selectors')
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps)
+)(ConfirmTxScreen)
+
+function mapStateToProps (state) {
+ const { metamask } = state
+ const {
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+
+ return {
+ identities: state.metamask.identities,
+ accounts: getMetaMaskAccounts(state),
+ selectedAddress: state.metamask.selectedAddress,
+ unapprovedTxs: state.metamask.unapprovedTxs,
+ unapprovedMsgs: state.metamask.unapprovedMsgs,
+ unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
+ unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
+ index: state.appState.currentView.context,
+ warning: state.appState.warning,
+ network: state.metamask.network,
+ provider: state.metamask.provider,
+ conversionRate: state.metamask.conversionRate,
+ currentCurrency: state.metamask.currentCurrency,
+ blockGasLimit: state.metamask.currentBlockGasLimit,
+ computedBalances: state.metamask.computedBalances,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ send: state.metamask.send,
+ selectedAddressTxList: state.metamask.selectedAddressTxList,
+ }
+}
+
+inherits(ConfirmTxScreen, Component)
+function ConfirmTxScreen () {
+ Component.call(this)
+}
+
+ConfirmTxScreen.prototype.getUnapprovedMessagesTotal = function () {
+ const {
+ unapprovedMsgCount = 0,
+ unapprovedPersonalMsgCount = 0,
+ unapprovedTypedMessagesCount = 0,
+ } = this.props
+
+ return unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount
+}
+
+ConfirmTxScreen.prototype.componentDidMount = function () {
+ const {
+ unapprovedTxs = {},
+ network,
+ send,
+ } = this.props
+ const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
+
+ if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+}
+
+ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
+ const {
+ unapprovedTxs = {},
+ network,
+ selectedAddressTxList,
+ send,
+ history,
+ match: { params: { id: transactionId } = {} },
+ } = this.props
+
+ let prevTx
+
+ if (transactionId) {
+ prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList)
+ } else {
+ const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
+ const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network)
+ const prevTxData = prevUnconfTxList[prevIndex] || {}
+ prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {}
+ }
+
+ const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
+
+ if (prevTx && prevTx.status === 'dropped') {
+ this.props.dispatch(actions.showModal({
+ name: 'TRANSACTION_CONFIRMED',
+ onSubmit: () => history.push(DEFAULT_ROUTE),
+ }))
+
+ return
+ }
+
+ if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+}
+
+ConfirmTxScreen.prototype.getTxData = function () {
+ const {
+ network,
+ index,
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ match: { params: { id: transactionId } = {} },
+ } = this.props
+
+ const unconfTxList = txHelper(
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ network
+ )
+
+ log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`)
+
+ return transactionId
+ ? R.find(({ id }) => id + '' === transactionId)(unconfTxList)
+ : unconfTxList[index]
+}
+
+ConfirmTxScreen.prototype.render = function () {
+ const props = this.props
+ const {
+ currentCurrency,
+ conversionRate,
+ blockGasLimit,
+ } = props
+
+ var txData = this.getTxData() || {}
+ const { msgParams } = txData
+ log.debug('msgParams detected, rendering pending msg')
+
+ return msgParams
+ ? h(SignatureRequest, {
+ // Properties
+ txData: txData,
+ key: txData.id,
+ selectedAddress: props.selectedAddress,
+ accounts: props.accounts,
+ identities: props.identities,
+ conversionRate,
+ currentCurrency,
+ blockGasLimit,
+ // Actions
+ signMessage: this.signMessage.bind(this, txData),
+ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
+ cancelMessage: this.cancelMessage.bind(this, txData),
+ cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
+ })
+ : h(Loading)
+}
+
+ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signMsg(params))
+}
+
+ConfirmTxScreen.prototype.stopPropagation = function (event) {
+ if (event.stopPropagation) {
+ event.stopPropagation()
+ }
+}
+
+ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing personal message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signPersonalMsg(params))
+}
+
+ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing typed message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signTypedMsg(params))
+}
+
+ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
+ log.info('canceling message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelMsg(msgData))
+}
+
+ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
+ log.info('canceling personal message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelPersonalMsg(msgData))
+}
+
+ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
+ log.info('canceling typed message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelTypedMsg(msgData))
+}
diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js
new file mode 100644
index 000000000..35b8dc5aa
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js
@@ -0,0 +1,160 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import Loading from '../../components/ui/loading-screen'
+import ConfirmTransactionSwitch from '../confirm-transaction-switch'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import ConfirmSendEther from '../confirm-send-ether'
+import ConfirmSendToken from '../confirm-send-token'
+import ConfirmDeployContract from '../confirm-deploy-contract'
+import ConfirmApprove from '../confirm-approve'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import ConfTx from './conf-tx'
+import {
+ DEFAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../helpers/constants/routes'
+
+export default class ConfirmTransaction extends Component {
+ static propTypes = {
+ history: PropTypes.object.isRequired,
+ totalUnapprovedCount: PropTypes.number.isRequired,
+ match: PropTypes.object,
+ send: PropTypes.object,
+ unconfirmedTransactions: PropTypes.array,
+ setTransactionToConfirm: PropTypes.func,
+ confirmTransaction: PropTypes.object,
+ clearConfirmTransaction: PropTypes.func,
+ fetchBasicGasAndTimeEstimates: PropTypes.func,
+ }
+
+ getParamsTransactionId () {
+ const { match: { params: { id } = {} } } = this.props
+ return id || null
+ }
+
+ componentDidMount () {
+ const {
+ totalUnapprovedCount = 0,
+ send = {},
+ history,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ fetchBasicGasAndTimeEstimates,
+ } = this.props
+
+ if (!totalUnapprovedCount && !send.to) {
+ history.replace(DEFAULT_ROUTE)
+ return
+ }
+
+ if (!transactionId) {
+ fetchBasicGasAndTimeEstimates()
+ this.setTransactionToConfirm()
+ }
+ }
+
+ componentDidUpdate () {
+ const {
+ setTransactionToConfirm,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ clearConfirmTransaction,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
+ clearConfirmTransaction()
+ setTransactionToConfirm(paramsTransactionId)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ setTransactionToConfirm () {
+ const {
+ history,
+ unconfirmedTransactions,
+ setTransactionToConfirm,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId) {
+ // Check to make sure params ID is valid
+ const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
+
+ if (!tx) {
+ history.replace(DEFAULT_ROUTE)
+ } else {
+ setTransactionToConfirm(paramsTransactionId)
+ }
+ } else if (unconfirmedTransactions.length) {
+ const totalUnconfirmed = unconfirmedTransactions.length
+ const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
+ const { id: transactionId, loadingDefaults } = transaction
+
+ if (!loadingDefaults) {
+ setTransactionToConfirm(transactionId)
+ }
+ }
+ }
+
+ render () {
+ const { confirmTransaction: { txData: { id } } = {} } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ // Show routes when state.confirmTransaction has been set and when either the ID in the params
+ // isn't specified or is specified and matches the ID in state.confirmTransaction in order to
+ // support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
+ return id && (!paramsTransactionId || paramsTransactionId === id + '')
+ ? (
+ <Switch>
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
+ component={ConfirmDeployContract}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
+ component={ConfirmTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
+ component={ConfirmSendEther}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
+ component={ConfirmSendToken}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
+ component={ConfirmApprove}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
+ component={ConfirmTokenTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
+ component={ConfTx}
+ />
+ <Route path="*" component={ConfirmTransactionSwitch} />
+ </Switch>
+ )
+ : <Loading />
+ }
+}
diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js
new file mode 100644
index 000000000..2dd5e833e
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js
@@ -0,0 +1,37 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import {
+ setTransactionToConfirm,
+ clearConfirmTransaction,
+} from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import {
+ fetchBasicGasAndTimeEstimates,
+} from '../../ducks/gas/gas.duck'
+import ConfirmTransaction from './confirm-transaction.component'
+import { getTotalUnapprovedCount } from '../../selectors/selectors'
+import { unconfirmedTransactionsListSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask: { send }, confirmTransaction } = state
+
+ return {
+ totalUnapprovedCount: getTotalUnapprovedCount(state),
+ send,
+ confirmTransaction,
+ unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+)(ConfirmTransaction)
diff --git a/ui/app/components/pages/confirm-transaction/index.js b/ui/app/pages/confirm-transaction/index.js
index 4bf42d85c..4bf42d85c 100644
--- a/ui/app/components/pages/confirm-transaction/index.js
+++ b/ui/app/pages/confirm-transaction/index.js
diff --git a/ui/app/pages/create-account/connect-hardware/account-list.js b/ui/app/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..617fb8833
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,205 @@
+const { Component } = require('react')
+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 '../../../components/ui/button'
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ getHdPaths () {
+ return [
+ {
+ label: `Ledger Live`,
+ value: `m/44'/60'/0'/0/0`,
+ },
+ {
+ label: `Legacy (MEW / MyCrypto)`,
+ value: `m/44'/60'/0'`,
+ },
+ ]
+ }
+
+ goToNextPage = () => {
+ // If we have < 5 accounts, it's restricted by BIP-44
+ if (this.props.accounts.length === 5) {
+ this.props.getPage(this.props.device, 1, this.props.selectedPath)
+ } else {
+ this.props.onAccountRestriction()
+ }
+ }
+
+ goToPreviousPage = () => {
+ this.props.getPage(this.props.device, -1, this.props.selectedPath)
+ }
+
+ renderHdPathSelector () {
+ const { onPathChange, selectedPath } = this.props
+
+ const options = this.getHdPaths()
+ return h('div', [
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')),
+ h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
+ h('div.hw-connect__hdPath', [
+ h(Select, {
+ className: 'hw-connect__hdPath__select',
+ name: 'hd-path-select',
+ clearable: false,
+ value: selectedPath,
+ options,
+ onChange: (opt) => {
+ onPathChange(opt.value)
+ },
+ }),
+ ]),
+ ])
+ }
+
+ capitalizeDevice (device) {
+ return device.slice(0, 1).toUpperCase() + device.slice(1)
+ }
+
+ renderHeader () {
+ const { device } = this.props
+ return (
+ h('div.hw-connect', [
+
+ h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`),
+
+ device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
+
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')),
+ h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
+ ])
+ )
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ [
+ h('span.hw-account-list__item__index', a.index + 1),
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
+ h('span.hw-account-list__item__balance', `${a.balance}`),
+ ]),
+ ]),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: this.goToPreviousPage,
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: this.goToNextPage,
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ const disabled = this.props.selectedAccount === null
+ const buttonProps = {}
+ if (disabled) {
+ buttonProps.disabled = true
+ }
+
+ return h('div.new-account-connect-form__buttons', {}, [
+ 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: 'confirm',
+ large: true,
+ className: 'new-account-connect-form__button unlock',
+ disabled,
+ onClick: this.props.onUnlockAccount.bind(this, this.props.device),
+ }, [this.context.t('unlock')]),
+ ])
+ }
+
+ renderForgetDevice () {
+ return h('div.hw-forget-device-container', {}, [
+ h('a', {
+ onClick: this.props.onForgetDevice.bind(this, this.props.device),
+ }, this.context.t('forgetDevice')),
+ ])
+ }
+
+ render () {
+ return h('div.new-account-connect-form.account-list', {}, [
+ this.renderHeader(),
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ this.renderForgetDevice(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ onPathChange: PropTypes.func.isRequired,
+ selectedPath: PropTypes.string.isRequired,
+ device: PropTypes.string.isRequired,
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ onForgetDevice: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+ onAccountRestriction: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/pages/create-account/connect-hardware/connect-screen.js b/ui/app/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..7e9dee970
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,197 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+import Button from '../../../components/ui/button'
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ selectedDevice: null,
+ }
+ }
+
+ connect = () => {
+ if (this.state.selectedDevice) {
+ this.props.connectToHardwareWallet(this.state.selectedDevice)
+ }
+ return null
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'trezor'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/trezor-logo.svg',
+ })
+ )
+ }
+
+ renderConnectToLedgerButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'ledger'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/ledger-logo.svg',
+ })
+ )
+ }
+
+ renderButtons () {
+ return (
+ h('div', {}, [
+ h('div.hw-connect__btn-wrapper', {}, [
+ this.renderConnectToLedgerButton(),
+ this.renderConnectToTrezorButton(),
+ ]),
+ h(Button, {
+ type: 'confirm',
+ large: true,
+ className: 'hw-connect__connect-btn',
+ onClick: this.connect,
+ disabled: !this.state.selectedDevice,
+ }, this.context.t('connect')),
+ ])
+ )
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div.new-account-connect-form.unsupported-browser', {}, [
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
+ ]),
+ h(Button, {
+ type: 'primary',
+ large: true,
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ }, this.context.t('downloadGoogleChrome')),
+ ])
+ )
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect__header', {}, [
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
+ ])
+ )
+ }
+
+ getAffiliateLinks () {
+ const links = {
+ trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
+ ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
+ }
+
+ const text = this.context.t('orderOneHere')
+ const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger)
+
+ return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }})
+ }
+
+ renderTrezorAffiliateLink () {
+ return h('div.hw-connect__get-hw', {}, [
+ h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)),
+ this.getAffiliateLinks(),
+ ])
+ }
+
+
+ scrollToTutorial = (e) => {
+ if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
+ }
+
+ renderLearnMore () {
+ return (
+ h('p.hw-connect__learn-more', {
+ onClick: this.scrollToTutorial,
+ }, [
+ this.context.t('learnMore'),
+ h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
+ ])
+ )
+ }
+
+ renderTutorialSteps () {
+ const steps = [
+ {
+ asset: 'hardware-wallet-step-1',
+ dimensions: {width: '225px', height: '75px'},
+ },
+ {
+ asset: 'hardware-wallet-step-2',
+ dimensions: {width: '300px', height: '100px'},
+ },
+ {
+ asset: 'hardware-wallet-step-3',
+ dimensions: {width: '120px', height: '90px'},
+ },
+ ]
+
+ return h('.hw-tutorial', {
+ ref: node => { this.referenceNode = node },
+ },
+ steps.map((step, i) => (
+ h('div.hw-connect', {}, [
+ h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
+ h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
+ h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
+ ])
+ ))
+ )
+ }
+
+ renderFooter () {
+ return (
+ h('div.hw-connect__footer', {}, [
+ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
+ this.renderButtons(),
+ h('p.hw-connect__footer__msg', {}, [
+ this.context.t(`havingTroubleConnecting`),
+ h('a.hw-connect__footer__link', {
+ href: 'https://support.metamask.io/',
+ target: '_blank',
+ }, this.context.t('getHelp')),
+ ]),
+ ])
+ )
+ }
+
+ renderConnectScreen () {
+ return (
+ h('div.new-account-connect-form', {}, [
+ this.renderHeader(),
+ this.renderButtons(),
+ this.renderTrezorAffiliateLink(),
+ this.renderLearnMore(),
+ this.renderTutorialSteps(),
+ this.renderFooter(),
+ ])
+ )
+ }
+
+ render () {
+ if (this.props.browserSupported) {
+ return this.renderConnectScreen()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToHardwareWallet: PropTypes.func.isRequired,
+ browserSupported: PropTypes.bool.isRequired,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..1398fa680
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,293 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+const ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { formatBalance } = require('../../../helpers/utils/util')
+const { getPlatform } = require('../../../../../app/scripts/lib/util')
+const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ selectedAccount: null,
+ accounts: [],
+ browserSupported: true,
+ unlocked: false,
+ device: null,
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { accounts } = nextProps
+ const newAccounts = this.state.accounts.map(a => {
+ const normalizedAddress = a.address.toLowerCase()
+ const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
+ a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return a
+ })
+ this.setState({accounts: newAccounts})
+ }
+
+
+ componentDidMount () {
+ this.checkIfUnlocked()
+ }
+
+ async checkIfUnlocked () {
+ ['trezor', 'ledger'].forEach(async device => {
+ const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+ })
+ }
+
+ connectToHardwareWallet = (device) => {
+ // Ledger hardware wallets are not supported on firefox
+ if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') {
+ this.setState({ browserSupported: false, error: null})
+ return null
+ }
+
+ if (this.state.accounts.length) {
+ return null
+ }
+
+ // Default values
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+
+ onPathChange = (path) => {
+ this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
+ this.getPage(this.state.device, 0, path)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ onAccountRestriction = () => {
+ this.setState({error: this.context.t('ledgerAccountRestriction') })
+ }
+
+ showTemporaryAlert () {
+ this.props.showAlert(this.context.t('hardwareWalletConnected'))
+ // Autohide the alert after 5 seconds
+ setTimeout(_ => {
+ this.props.hideAlert()
+ }, 5000)
+ }
+
+ getPage = (device, page, hdPath) => {
+ this.props
+ .connectHardware(device, page, hdPath)
+ .then(accounts => {
+ if (accounts.length) {
+
+ // If we just loaded the accounts for the first time
+ // (device previously locked) show the global alert
+ if (this.state.accounts.length === 0 && !this.state.unlocked) {
+ this.showTemporaryAlert()
+ }
+
+ const newState = { unlocked: true, device, error: null }
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ accounts.forEach((a, i) => {
+ if (a.address.toLowerCase() === this.props.address) {
+ newState.selectedAccount = a.index.toString()
+ }
+ })
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
+ newState.selectedAccount = null
+ }
+
+
+ // Map accounts with balances
+ newState.accounts = accounts.map(account => {
+ const normalizedAddress = account.address.toLowerCase()
+ const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
+ account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return account
+ })
+
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ if (e === 'Window blocked') {
+ this.setState({ browserSupported: false, error: null})
+ } else if (e !== 'Window closed' && e !== 'Popup closed') {
+ this.setState({ error: e.toString() })
+ }
+ })
+ }
+
+ onForgetDevice = (device) => {
+ this.props.forgetDevice(device)
+ .then(_ => {
+ this.setState({
+ error: null,
+ selectedAccount: null,
+ accounts: [],
+ unlocked: false,
+ })
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onUnlockAccount = (device) => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
+ .then(_ => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Connected Hardware Wallet',
+ name: 'Connected Account with: ' + device,
+ },
+ })
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Connected Hardware Wallet',
+ name: 'Error connecting hardware wallet',
+ },
+ customVariables: {
+ error: e.toString(),
+ },
+ })
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToHardwareWallet: this.connectToHardwareWallet,
+ browserSupported: this.state.browserSupported,
+ })
+ }
+
+ return h(AccountList, {
+ onPathChange: this.onPathChange,
+ selectedPath: this.props.defaultHdPaths[this.state.device],
+ device: this.state.device,
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onForgetDevice: this.onForgetDevice,
+ onCancel: this.onCancel,
+ onAccountRestriction: this.onAccountRestriction,
+ })
+ }
+
+ render () {
+ return h('div', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ checkHardwareStatus: PropTypes.func,
+ forgetDevice: PropTypes.func,
+ showAlert: PropTypes.func,
+ hideAlert: PropTypes.func,
+ unlockHardwareWalletAccount: PropTypes.func,
+ setHardwareWalletDefaultHdPath: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+ address: PropTypes.string,
+ defaultHdPaths: PropTypes.object,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {} },
+ } = state
+ const accounts = getMetaMaskAccounts(state)
+ const numberOfExistingAccounts = Object.keys(identities).length
+ const {
+ appState: { defaultHdPaths },
+ } = state
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ defaultHdPaths,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setHardwareWalletDefaultHdPath: ({device, path}) => {
+ return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
+ },
+ connectHardware: (deviceName, page, hdPath) => {
+ return dispatch(actions.connectHardware(deviceName, page, hdPath))
+ },
+ checkHardwareStatus: (deviceName, hdPath) => {
+ return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
+ },
+ forgetDevice: (deviceName) => {
+ return dispatch(actions.forgetDevice(deviceName))
+ },
+ unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
+ return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ showAlert: (msg) => dispatch(actions.showAlert(msg)),
+ hideAlert: () => dispatch(actions.hideAlert()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/components/pages/create-account/import-account/index.js b/ui/app/pages/create-account/import-account/index.js
index 48d8f8838..48d8f8838 100644
--- a/ui/app/components/pages/create-account/import-account/index.js
+++ b/ui/app/pages/create-account/import-account/index.js
diff --git a/ui/app/pages/create-account/import-account/json.js b/ui/app/pages/create-account/import-account/json.js
new file mode 100644
index 000000000..17bef763c
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/json.js
@@ -0,0 +1,170 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const FileInput = require('react-simple-file-input').default
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
+import Button from '../../../components/ui/button'
+
+class JsonImportSubview extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ file: null,
+ fileContents: '',
+ }
+ }
+
+ render () {
+ const { error } = this.props
+
+ return (
+ h('div.new-account-import-form__json', [
+
+ h('p', this.context.t('usedByClients')),
+ h('a.warning', {
+ href: HELP_LINK,
+ target: '_blank',
+ }, this.context.t('fileImportFail')),
+
+ h(FileInput, {
+ readAs: 'text',
+ onLoad: this.onLoad.bind(this),
+ style: {
+ margin: '20px 0px 12px 34%',
+ fontSize: '15px',
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ }),
+
+ h('input.new-account-import-form__input-password', {
+ type: 'password',
+ placeholder: this.context.t('enterPassword'),
+ id: 'json-password-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ }),
+
+ h('div.new-account-create-form__buttons', {}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.createNewKeychain(),
+ }, [this.context.t('import')]),
+
+ ]),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+ }
+
+ onLoad (event, file) {
+ this.setState({file: file, fileContents: event.target.result})
+ }
+
+ createKeyringOnEnter (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+ }
+
+ createNewKeychain () {
+ const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress, history } = this.props
+ const state = this.state
+
+ if (!state) {
+ const message = this.context.t('validFileImport')
+ return displayWarning(message)
+ }
+
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = this.context.t('needImportFile')
+ return displayWarning(message)
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ importNewJsonAccount([ fileContents, password ])
+ .then(({ selectedAddress }) => {
+ if (selectedAddress) {
+ history.push(DEFAULT_ROUTE)
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Imported Account with JSON',
+ },
+ })
+ displayWarning(null)
+ } else {
+ displayWarning('Error importing account.')
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Error importing JSON',
+ },
+ })
+ setSelectedAddress(firstAddress)
+ }
+ })
+ .catch(err => err && displayWarning(err.message || err))
+ }
+}
+
+JsonImportSubview.propTypes = {
+ error: PropTypes.string,
+ goHome: PropTypes.func,
+ displayWarning: PropTypes.func,
+ firstAddress: PropTypes.string,
+ importNewJsonAccount: PropTypes.func,
+ history: PropTypes.object,
+ setSelectedAddress: PropTypes.func,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ return {
+ error: state.appState.warning,
+ firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ displayWarning: warning => dispatch(actions.displayWarning(warning)),
+ importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)),
+ setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
+ }
+}
+
+JsonImportSubview.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(JsonImportSubview)
diff --git a/ui/app/pages/create-account/import-account/private-key.js b/ui/app/pages/create-account/import-account/private-key.js
new file mode 100644
index 000000000..450614e87
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/private-key.js
@@ -0,0 +1,128 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+import Button from '../../../components/ui/button'
+
+PrivateKeyImportView.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(PrivateKeyImportView)
+
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ importNewAccount: (strategy, [ privateKey ]) => {
+ return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
+ },
+ displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
+ setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error, displayWarning } = this.props
+
+ return (
+ h('div.new-account-import-form__private-key', [
+
+ h('span.new-account-create-form__instruction', this.context.t('pastePrivateKey')),
+
+ h('div.new-account-import-form__private-key-password-container', [
+
+ h('input.new-account-import-form__input-password', {
+ type: 'password',
+ id: 'private-key-box',
+ onKeyPress: e => this.createKeyringOnEnter(e),
+ }),
+
+ ]),
+
+ h('div.new-account-import-form__buttons', {}, [
+
+ 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')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.createNewKeychain(),
+ }, [this.context.t('import')]),
+
+ ]),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+}
+
+PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+PrivateKeyImportView.prototype.createNewKeychain = function () {
+ const input = document.getElementById('private-key-box')
+ const privateKey = input.value
+ const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props
+
+ importNewAccount('Private Key', [ privateKey ])
+ .then(({ selectedAddress }) => {
+ if (selectedAddress) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Imported Account with Private Key',
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ displayWarning(null)
+ } else {
+ displayWarning('Error importing account.')
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Error importing with Private Key',
+ },
+ })
+ setSelectedAddress(firstAddress)
+ }
+ })
+ .catch(err => err && displayWarning(err.message || err))
+}
diff --git a/ui/app/components/pages/create-account/import-account/seed.js b/ui/app/pages/create-account/import-account/seed.js
index d98909baa..d98909baa 100644
--- a/ui/app/components/pages/create-account/import-account/seed.js
+++ b/ui/app/pages/create-account/import-account/seed.js
diff --git a/ui/app/pages/create-account/index.js b/ui/app/pages/create-account/index.js
new file mode 100644
index 000000000..ce84db028
--- /dev/null
+++ b/ui/app/pages/create-account/index.js
@@ -0,0 +1,113 @@
+const Component = require('react').Component
+const { Switch, Route, matchPath } = require('react-router-dom')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../store/actions')
+const { getCurrentViewContext } = require('../../selectors/selectors')
+const classnames = require('classnames')
+const NewAccountCreateForm = require('./new-account')
+const NewAccountImportForm = require('./import-account')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../helpers/constants/routes')
+
+class CreateAccountPage extends Component {
+ renderTabs () {
+ const { history, location } = this.props
+
+ return h('div.new-account__tabs', [
+ h('div.new-account__tabs__tab', {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: NEW_ACCOUNT_ROUTE, exact: true,
+ }),
+ }),
+ onClick: () => history.push(NEW_ACCOUNT_ROUTE),
+ }, [
+ this.context.t('create'),
+ ]),
+
+ h('div.new-account__tabs__tab', {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: IMPORT_ACCOUNT_ROUTE, exact: true,
+ }),
+ }),
+ onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
+ }, [
+ this.context.t('import'),
+ ]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
+ ])
+ }
+
+ render () {
+ return h('div.new-account', {}, [
+ h('div.new-account__header', [
+ h('div.new-account__title', this.context.t('newAccount')),
+ this.renderTabs(),
+ ]),
+ h('div.new-account__form', [
+ h(Switch, [
+ h(Route, {
+ exact: true,
+ path: NEW_ACCOUNT_ROUTE,
+ component: NewAccountCreateForm,
+ }),
+ h(Route, {
+ exact: true,
+ path: IMPORT_ACCOUNT_ROUTE,
+ component: NewAccountImportForm,
+ }),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
+ ]),
+ ]),
+ ])
+ }
+}
+
+CreateAccountPage.propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+ t: PropTypes.func,
+}
+
+CreateAccountPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => ({
+ displayedForm: getCurrentViewContext(state),
+})
+
+const mapDispatchToProps = dispatch => ({
+ displayForm: form => dispatch(actions.setNewAccountForm(form)),
+ showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
+ showExportPrivateKeyModal: () => {
+ dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
+ },
+ hideModal: () => dispatch(actions.hideModal()),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
+})
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
diff --git a/ui/app/pages/create-account/new-account.js b/ui/app/pages/create-account/new-account.js
new file mode 100644
index 000000000..316fbe6f1
--- /dev/null
+++ b/ui/app/pages/create-account/new-account.js
@@ -0,0 +1,130 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+import Button from '../../components/ui/button'
+
+class NewAccountCreateForm extends Component {
+ constructor (props, context) {
+ super(props)
+
+ const { numberOfExistingAccounts = 0 } = props
+ const newAccountNumber = numberOfExistingAccounts + 1
+
+ this.state = {
+ newAccountName: '',
+ defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]),
+ }
+ }
+
+ render () {
+ const { newAccountName, defaultAccountName } = this.state
+ const { history, createAccount } = this.props
+
+ return h('div.new-account-create-form', [
+
+ h('div.new-account-create-form__input-label', {}, [
+ this.context.t('accountName'),
+ ]),
+
+ h('div.new-account-create-form__input-wrapper', {}, [
+ h('input.new-account-create-form__input', {
+ value: newAccountName,
+ placeholder: defaultAccountName,
+ onChange: event => this.setState({ newAccountName: event.target.value }),
+ }, []),
+ ]),
+
+ h('div.new-account-create-form__buttons', {}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => {
+ createAccount(newAccountName || defaultAccountName)
+ .then(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Add New Account',
+ name: 'Added New Account',
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ })
+ .catch((e) => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Add New Account',
+ name: 'Error',
+ },
+ customVariables: {
+ errorMessage: e.message,
+ },
+ })
+ })
+ },
+ }, [this.context.t('create')]),
+
+ ]),
+
+ ])
+ }
+}
+
+NewAccountCreateForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ createAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { network, selectedAddress, identities = {} } } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
+ hideModal: () => dispatch(actions.hideModal()),
+ createAccount: newAccountName => {
+ return dispatch(actions.addNewAccount())
+ .then(newAccountAddress => {
+ if (newAccountName) {
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
+ }
+ })
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ }
+}
+
+NewAccountCreateForm.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)
+
diff --git a/ui/app/pages/first-time-flow/create-password/create-password.component.js b/ui/app/pages/first-time-flow/create-password/create-password.component.js
new file mode 100644
index 000000000..5e67a2244
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/create-password.component.js
@@ -0,0 +1,71 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import NewAccount from './new-account'
+import ImportWithSeedPhrase from './import-with-seed-phrase'
+import {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class CreatePassword extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ onCreateNewAccount: PropTypes.func,
+ onCreateNewAccountFromSeed: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { isInitialized, history } = this.props
+
+ if (isInitialized) {
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ }
+ }
+
+ render () {
+ const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
+ render={props => (
+ <ImportWithSeedPhrase
+ { ...props }
+ onSubmit={onCreateNewAccountFromSeed}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_CREATE_PASSWORD_ROUTE}
+ render={props => (
+ <NewAccount
+ { ...props }
+ onSubmit={onCreateNewAccount}
+ />
+ )}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.container.js b/ui/app/pages/first-time-flow/create-password/create-password.container.js
index 89106f016..89106f016 100644
--- a/ui/app/components/pages/first-time-flow/create-password/create-password.container.js
+++ b/ui/app/pages/first-time-flow/create-password/create-password.container.js
diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
new file mode 100644
index 000000000..433dad6e2
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
@@ -0,0 +1,256 @@
+import {validateMnemonic} from 'bip39'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TextField from '../../../../components/ui/text-field'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+} from '../../../../helpers/constants/routes'
+
+export default class ImportWithSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: PropTypes.func.isRequired,
+ }
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .trim()
+ .match(/\w+/g)
+ .join(' ')
+ }
+
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = ''
+
+ if (seedPhrase) {
+ const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase)
+ if (parsedSeedPhrase.split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ } else if (!validateMnemonic(parsedSeedPhrase)) {
+ seedPhraseError = this.context.t('invalidSeedPhrase')
+ }
+ }
+
+ this.setState({ seedPhrase, seedPhraseError })
+ }
+
+ handlePasswordChange (password) {
+ const { t } = this.context
+
+ this.setState(state => {
+ const { confirmPassword } = state
+ let confirmPasswordError = ''
+ let passwordError = ''
+
+ if (password && password.length < 8) {
+ passwordError = t('passwordNotLongEnough')
+ }
+
+ if (confirmPassword && password !== confirmPassword) {
+ confirmPasswordError = t('passwordsDontMatch')
+ }
+
+ return {
+ password,
+ passwordError,
+ confirmPasswordError,
+ }
+ })
+ }
+
+ handleConfirmPasswordChange (confirmPassword) {
+ const { t } = this.context
+
+ this.setState(state => {
+ const { password } = state
+ let confirmPasswordError = ''
+
+ if (password !== confirmPassword) {
+ confirmPasswordError = t('passwordsDontMatch')
+ }
+
+ return {
+ confirmPassword,
+ confirmPasswordError,
+ }
+ })
+ }
+
+ handleImport = async event => {
+ event.preventDefault()
+
+ if (!this.isValid()) {
+ return
+ }
+
+ const { password, seedPhrase } = this.state
+ const { history, onSubmit } = this.props
+
+ try {
+ await onSubmit(password, this.parseSeedPhrase(seedPhrase))
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Import Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ } catch (error) {
+ this.setState({ seedPhraseError: error.message })
+ }
+ }
+
+ isValid () {
+ const {
+ seedPhrase,
+ password,
+ confirmPassword,
+ passwordError,
+ confirmPasswordError,
+ seedPhraseError,
+ } = this.state
+
+ if (!password || !confirmPassword || !seedPhrase || password !== confirmPassword) {
+ return false
+ }
+
+ if (password.length < 8) {
+ return false
+ }
+
+ return !passwordError && !confirmPasswordError && !seedPhraseError
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <form
+ className="first-time-flow__form"
+ onSubmit={this.handleImport}
+ >
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Go Back from Onboarding Import',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </div>
+ <div className="first-time-flow__header">
+ { t('importAccountSeedPhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('secretPhrase') }
+ </div>
+ <div className="first-time-flow__textarea-wrapper">
+ <label>{ t('walletSeed') }</label>
+ <textarea
+ className="first-time-flow__textarea"
+ onChange={e => this.handleSeedPhraseChange(e.target.value)}
+ value={this.state.seedPhrase}
+ placeholder={t('seedPhrasePlaceholder')}
+ />
+ </div>
+ {
+ seedPhraseError && (
+ <span className="error">
+ { seedPhraseError }
+ </span>
+ )
+ }
+ <TextField
+ id="password"
+ label={t('newPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.password}
+ onChange={event => this.handlePasswordChange(event.target.value)}
+ error={passwordError}
+ autoComplete="new-password"
+ margin="normal"
+ largeLabel
+ />
+ <TextField
+ id="confirm-password"
+ label={t('confirmPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.confirmPassword}
+ onChange={event => this.handleConfirmPasswordChange(event.target.value)}
+ error={confirmPasswordError}
+ autoComplete="confirm-password"
+ margin="normal"
+ largeLabel
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleImport}
+ >
+ { t('import') }
+ </Button>
+ </form>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
index e5ff1fde5..e5ff1fde5 100644
--- a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
diff --git a/ui/app/components/pages/first-time-flow/create-password/index.js b/ui/app/pages/first-time-flow/create-password/index.js
index 42e7436f9..42e7436f9 100644
--- a/ui/app/components/pages/first-time-flow/create-password/index.js
+++ b/ui/app/pages/first-time-flow/create-password/index.js
diff --git a/ui/app/components/pages/first-time-flow/create-password/new-account/index.js b/ui/app/pages/first-time-flow/create-password/new-account/index.js
index 97db39cc3..97db39cc3 100644
--- a/ui/app/components/pages/first-time-flow/create-password/new-account/index.js
+++ b/ui/app/pages/first-time-flow/create-password/new-account/index.js
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
new file mode 100644
index 000000000..c040cff88
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
@@ -0,0 +1,225 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+} from '../../../../helpers/constants/routes'
+import TextField from '../../../../components/ui/text-field'
+
+export default class NewAccount extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onSubmit: PropTypes.func.isRequired,
+ history: PropTypes.object.isRequired,
+ }
+
+ state = {
+ password: '',
+ confirmPassword: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ isValid () {
+ const {
+ password,
+ confirmPassword,
+ passwordError,
+ confirmPasswordError,
+ } = this.state
+
+ if (!password || !confirmPassword || password !== confirmPassword) {
+ return false
+ }
+
+ if (password.length < 8) {
+ return false
+ }
+
+ return !passwordError && !confirmPasswordError
+ }
+
+ handlePasswordChange (password) {
+ const { t } = this.context
+
+ this.setState(state => {
+ const { confirmPassword } = state
+ let passwordError = ''
+ let confirmPasswordError = ''
+
+ if (password && password.length < 8) {
+ passwordError = t('passwordNotLongEnough')
+ }
+
+ if (confirmPassword && password !== confirmPassword) {
+ confirmPasswordError = t('passwordsDontMatch')
+ }
+
+ return {
+ password,
+ passwordError,
+ confirmPasswordError,
+ }
+ })
+ }
+
+ handleConfirmPasswordChange (confirmPassword) {
+ const { t } = this.context
+
+ this.setState(state => {
+ const { password } = state
+ let confirmPasswordError = ''
+
+ if (password !== confirmPassword) {
+ confirmPasswordError = t('passwordsDontMatch')
+ }
+
+ return {
+ confirmPassword,
+ confirmPasswordError,
+ }
+ })
+ }
+
+ handleCreate = async event => {
+ event.preventDefault()
+
+ if (!this.isValid()) {
+ return
+ }
+
+ const { password } = this.state
+ const { onSubmit, history } = this.props
+
+ try {
+ await onSubmit(password)
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Submit Password',
+ },
+ })
+
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ } catch (error) {
+ this.setState({ passwordError: error.message })
+ }
+ }
+
+ handleImportWithSeedPhrase = event => {
+ const { history } = this.props
+
+ event.preventDefault()
+ history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { password, confirmPassword, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <div>
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Go Back from Onboarding Create',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </div>
+ <div className="first-time-flow__header">
+ { t('createPassword') }
+ </div>
+ <form
+ className="first-time-flow__form"
+ onSubmit={this.handleCreate}
+ >
+ <TextField
+ id="create-password"
+ label={t('newPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={password}
+ onChange={event => this.handlePasswordChange(event.target.value)}
+ error={passwordError}
+ autoFocus
+ autoComplete="new-password"
+ margin="normal"
+ fullWidth
+ largeLabel
+ />
+ <TextField
+ id="confirm-password"
+ label={t('confirmPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={confirmPassword}
+ onChange={event => this.handleConfirmPasswordChange(event.target.value)}
+ error={confirmPasswordError}
+ autoComplete="confirm-password"
+ margin="normal"
+ fullWidth
+ largeLabel
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleCreate}
+ >
+ { t('create') }
+ </Button>
+ </form>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/index.js b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
index 0e97bf755..0e97bf755 100644
--- a/ui/app/components/pages/first-time-flow/create-password/unique-image/index.js
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
new file mode 100644
index 000000000..3434d117a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
@@ -0,0 +1,55 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class UniqueImageScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div>
+ <img
+ src="/images/sleuth.svg"
+ height={42}
+ width={42}
+ />
+ <div className="first-time-flow__header">
+ { t('protectYourKeys') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage1') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage2') }
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Agree to Phishing Warning',
+ name: 'Agree to Phishing Warning',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ }}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
index 34874aaec..34874aaec 100644
--- a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
new file mode 100644
index 000000000..c4292331b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
@@ -0,0 +1,93 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
+
+export default class EndOfFlowScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ completeOnboarding: PropTypes.func,
+ completionMetaMetricsName: PropTypes.string,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history, completeOnboarding, completionMetaMetricsName } = this.props
+
+ return (
+ <div className="end-of-flow">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <div className="end-of-flow__emoji">🎉</div>
+ <div className="first-time-flow__header">
+ { t('congratulations') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-1">
+ { t('endOfFlowMessage1') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-2">
+ { t('endOfFlowMessage2') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage3') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage4') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage5') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage6') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage7') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-4">
+ *MetaMask cannot recover your seedphrase. <a
+ href="https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-Tips"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ Learn More
+ </span>
+ </a>.
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={async () => {
+ await completeOnboarding()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Onboarding Complete',
+ name: completionMetaMetricsName,
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { 'All Done' }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
new file mode 100644
index 000000000..38313806c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux'
+import EndOfFlow from './end-of-flow.component'
+import { setCompletedOnboarding } from '../../../store/actions'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'New Wallet Created',
+ 'import': 'New Wallet Imported',
+}
+
+const mapStateToProps = ({ metamask }) => {
+ const { firstTimeFlowType } = metamask
+
+ return {
+ completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ }
+}
+
+
+const mapDispatchToProps = dispatch => {
+ return {
+ completeOnboarding: () => dispatch(setCompletedOnboarding()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/index.js b/ui/app/pages/first-time-flow/end-of-flow/index.js
index b0643d155..b0643d155 100644
--- a/ui/app/components/pages/first-time-flow/end-of-flow/index.js
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.js
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/index.scss b/ui/app/pages/first-time-flow/end-of-flow/index.scss
index d7eb4513b..d7eb4513b 100644
--- a/ui/app/components/pages/first-time-flow/end-of-flow/index.scss
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.scss
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
new file mode 100644
index 000000000..4fd028482
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
@@ -0,0 +1,57 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Redirect } from 'react-router-dom'
+import {
+ DEFAULT_ROUTE,
+ LOCK_ROUTE,
+ INITIALIZE_WELCOME_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class FirstTimeFlowSwitch extends PureComponent {
+ static propTypes = {
+ completedOnboarding: PropTypes.bool,
+ isInitialized: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ seedPhrase: PropTypes.string,
+ optInMetaMetrics: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ seedPhrase,
+ optInMetaMetrics,
+ } = this.props
+
+ if (completedOnboarding) {
+ return <Redirect to={{ pathname: DEFAULT_ROUTE }} />
+ }
+
+ if (isUnlocked && !seedPhrase) {
+ return <Redirect to={{ pathname: LOCK_ROUTE }} />
+ }
+
+ if (!isInitialized) {
+ return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ }
+
+ if (!isUnlocked) {
+ return <Redirect to={{ pathname: INITIALIZE_UNLOCK_ROUTE }} />
+ }
+
+ if (seedPhrase) {
+ return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
+ }
+
+ if (optInMetaMetrics === null) {
+ return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ }
+
+ return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
index d68f7a153..d68f7a153 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
index 3647756ef..3647756ef 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
diff --git a/ui/app/pages/first-time-flow/first-time-flow.component.js b/ui/app/pages/first-time-flow/first-time-flow.component.js
new file mode 100644
index 000000000..bf6e80ca9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.component.js
@@ -0,0 +1,152 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import FirstTimeFlowSwitch from './first-time-flow-switch'
+import Welcome from './welcome'
+import SelectAction from './select-action'
+import EndOfFlow from './end-of-flow'
+import Unlock from '../unlock-page'
+import CreatePassword from './create-password'
+import SeedPhrase from './seed-phrase'
+import MetaMetricsOptInScreen from './metametrics-opt-in'
+import {
+ DEFAULT_ROUTE,
+ INITIALIZE_WELCOME_ROUTE,
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../helpers/constants/routes'
+
+export default class FirstTimeFlow extends PureComponent {
+ static propTypes = {
+ completedOnboarding: PropTypes.bool,
+ createNewAccount: PropTypes.func,
+ createNewAccountFromSeed: PropTypes.func,
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ unlockAccount: PropTypes.func,
+ nextRoute: PropTypes.func,
+ }
+
+ state = {
+ seedPhrase: '',
+ isImportedKeyring: false,
+ }
+
+ componentDidMount () {
+ const { completedOnboarding, history, isInitialized, isUnlocked } = this.props
+
+ if (completedOnboarding) {
+ history.push(DEFAULT_ROUTE)
+ return
+ }
+
+ if (isInitialized && !isUnlocked) {
+ history.push(INITIALIZE_UNLOCK_ROUTE)
+ return
+ }
+ }
+
+ handleCreateNewAccount = async password => {
+ const { createNewAccount } = this.props
+
+ try {
+ const seedPhrase = await createNewAccount(password)
+ this.setState({ seedPhrase })
+ } catch (error) {
+ throw new Error(error.message)
+ }
+ }
+
+ handleImportWithSeedPhrase = async (password, seedPhrase) => {
+ const { createNewAccountFromSeed } = this.props
+
+ try {
+ await createNewAccountFromSeed(password, seedPhrase)
+ this.setState({ isImportedKeyring: true })
+ } catch (error) {
+ throw new Error(error.message)
+ }
+ }
+
+ handleUnlock = async password => {
+ const { unlockAccount, history, nextRoute } = this.props
+
+ try {
+ const seedPhrase = await unlockAccount(password)
+ this.setState({ seedPhrase }, () => {
+ history.push(nextRoute)
+ })
+ } catch (error) {
+ throw new Error(error.message)
+ }
+ }
+
+ render () {
+ const { seedPhrase, isImportedKeyring } = this.state
+
+ return (
+ <div className="first-time-flow">
+ <Switch>
+ <Route
+ path={INITIALIZE_SEED_PHRASE_ROUTE}
+ render={props => (
+ <SeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ <Route
+ path={INITIALIZE_CREATE_PASSWORD_ROUTE}
+ render={props => (
+ <CreatePassword
+ { ...props }
+ isImportedKeyring={isImportedKeyring}
+ onCreateNewAccount={this.handleCreateNewAccount}
+ onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
+ />
+ )}
+ />
+ <Route
+ path={INITIALIZE_SELECT_ACTION_ROUTE}
+ component={SelectAction}
+ />
+ <Route
+ path={INITIALIZE_UNLOCK_ROUTE}
+ render={props => (
+ <Unlock
+ { ...props }
+ onSubmit={this.handleUnlock}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_END_OF_FLOW_ROUTE}
+ component={EndOfFlow}
+ />
+ <Route
+ exact
+ path={INITIALIZE_WELCOME_ROUTE}
+ component={Welcome}
+ />
+ <Route
+ exact
+ path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
+ component={MetaMetricsOptInScreen}
+ />
+ <Route
+ exact
+ path="*"
+ component={FirstTimeFlowSwitch}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/first-time-flow.container.js b/ui/app/pages/first-time-flow/first-time-flow.container.js
new file mode 100644
index 000000000..16025a489
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import FirstTimeFlow from './first-time-flow.component'
+import { getFirstTimeFlowTypeRoute } from './first-time-flow.selectors'
+import {
+ createNewVaultAndGetSeedPhrase,
+ createNewVaultAndRestore,
+ unlockAndGetSeedPhrase,
+} from '../../store/actions'
+
+const mapStateToProps = state => {
+ const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ createNewAccount: password => dispatch(createNewVaultAndGetSeedPhrase(password)),
+ createNewAccountFromSeed: (password, seedPhrase) => {
+ return dispatch(createNewVaultAndRestore(password, seedPhrase))
+ },
+ unlockAccount: password => dispatch(unlockAndGetSeedPhrase(password)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FirstTimeFlow)
diff --git a/ui/app/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
new file mode 100644
index 000000000..e6cd5a84a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
@@ -0,0 +1,26 @@
+import {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ DEFAULT_ROUTE,
+} from '../../helpers/constants/routes'
+
+const selectors = {
+ getFirstTimeFlowTypeRoute,
+}
+
+module.exports = selectors
+
+function getFirstTimeFlowTypeRoute (state) {
+ const { firstTimeFlowType } = state.metamask
+
+ let nextRoute
+ if (firstTimeFlowType === 'create') {
+ nextRoute = INITIALIZE_CREATE_PASSWORD_ROUTE
+ } else if (firstTimeFlowType === 'import') {
+ nextRoute = INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE
+ } else {
+ nextRoute = DEFAULT_ROUTE
+ }
+
+ return nextRoute
+}
diff --git a/ui/app/components/pages/first-time-flow/index.js b/ui/app/pages/first-time-flow/index.js
index 5db42437c..5db42437c 100644
--- a/ui/app/components/pages/first-time-flow/index.js
+++ b/ui/app/pages/first-time-flow/index.js
diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss
new file mode 100644
index 000000000..6c65cfdae
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.scss
@@ -0,0 +1,159 @@
+@import 'welcome/index';
+
+@import 'select-action/index';
+
+@import 'seed-phrase/index';
+
+@import 'end-of-flow/index';
+
+@import 'metametrics-opt-in/index';
+
+
+.first-time-flow {
+ width: 100%;
+ background-color: $white;
+ display: flex;
+ justify-content: center;
+
+ &__wrapper {
+ @media screen and (min-width: $break-large) {
+ max-width: 742px;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: 2%;
+ }
+
+ .app-header__metafox-logo {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__form {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__create-back {
+ margin-bottom: 16px;
+ }
+
+ &__header {
+ font-size: 2.5rem;
+ margin-bottom: 24px;
+ color: black;
+ }
+
+ &__subheader {
+ margin-bottom: 16px;
+ }
+
+ &__input {
+ max-width: 350px;
+ }
+
+ &__textarea-wrapper {
+ margin-bottom: 8px;
+ display: inline-flex;
+ padding: 0;
+ position: relative;
+ min-width: 0;
+ flex-direction: column;
+ max-width: 350px;
+ }
+
+ &__textarea-label {
+ margin-bottom: 9px;
+ color: #1B344D;
+ font-size: 18px;
+ }
+
+ &__textarea {
+ font-size: 1rem;
+ font-family: Roboto;
+ height: 190px;
+ border: 1px solid #CDCDCD;
+ border-radius: 6px;
+ background-color: #FFFFFF;
+ padding: 16px;
+ margin-top: 8px;
+ }
+
+ &__breadcrumbs {
+ margin: 36px 0;
+ }
+
+ &__unique-image {
+ margin-bottom: 20px;
+ }
+
+ &__markdown {
+ border: 1px solid #979797;
+ border-radius: 8px;
+ background-color: $white;
+ height: 200px;
+ overflow-y: auto;
+ color: #757575;
+ font-size: .75rem;
+ line-height: 15px;
+ text-align: justify;
+ margin: 0;
+ padding: 16px 20px;
+ height: 30vh;
+ }
+
+ &__text-block {
+ margin-bottom: 24px;
+ color: black;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__button {
+ margin: 35px 0 14px;
+ width: 140px;
+ height: 44px;
+ }
+
+ &__checkbox-container {
+ display: flex;
+ align-items: center;
+ margin-top: 24px;
+ }
+
+ &__checkbox {
+ background: #FFFFFF;
+ border: 1px solid #CDCDCD;
+ box-sizing: border-box;
+ height: 34px;
+ width: 34px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ border: 1.5px solid #2f9ae0;
+ }
+
+ .fa-check {
+ color: #2f9ae0
+ }
+ }
+
+ &__checkbox-label {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 18px;
+ color: #939090;
+ margin-left: 18px;
+ }
+
+ &__link-text {
+ color: $curious-blue;
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
index 4bc2fc3a7..4bc2fc3a7 100644
--- a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
index 6c2e37785..6c2e37785 100644
--- a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
new file mode 100644
index 000000000..19c668278
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
@@ -0,0 +1,169 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'
+
+export default class MetaMetricsOptIn extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ setParticipateInMetaMetrics: PropTypes.func,
+ nextRoute: PropTypes.string,
+ firstTimeSelectionMetaMetricsName: PropTypes.string,
+ participateInMetaMetrics: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ render () {
+ const { metricsEvent } = this.context
+ const {
+ nextRoute,
+ history,
+ setParticipateInMetaMetrics,
+ firstTimeSelectionMetaMetricsName,
+ participateInMetaMetrics,
+ } = this.props
+
+ return (
+ <div className="metametrics-opt-in">
+ <div className="metametrics-opt-in__main">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <div className="metametrics-opt-in__body-graphic">
+ <img src="images/metrics-chart.svg" />
+ </div>
+ <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
+ <div className="metametrics-opt-in__body">
+ <div className="metametrics-opt-in__description">
+ MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
+ will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
+ </div>
+ <div className="metametrics-opt-in__description">
+ MetaMask will..
+ </div>
+
+ <div className="metametrics-opt-in__committments">
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Always allow you to opt-out via Settings
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Send anonymized click & pageview events
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Maintain a public aggregate dashboard to educate the community
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="metametrics-opt-in__footer">
+ <PageContainerFooter
+ onCancel={() => {
+ setParticipateInMetaMetrics(false)
+ .then(() => {
+ const promise = participateInMetaMetrics !== false
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt Out',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ cancelText={'No Thanks'}
+ hideCancel={false}
+ onSubmit={() => {
+ setParticipateInMetaMetrics(true)
+ .then(([participateStatus, metaMetricsId]) => {
+ const promise = participateInMetaMetrics !== true
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt In',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ return metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import or Create',
+ name: firstTimeSelectionMetaMetricsName,
+ },
+ isOptIn: true,
+ metaMetricsId,
+ })
+ })
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ submitText={'I agree'}
+ submitButtonType={'confirm'}
+ disabled={false}
+ />
+ <div className="metametrics-opt-in__bottom-text">
+ This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Privacy Policy here
+ </a>.
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
new file mode 100644
index 000000000..2566a2a56
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import MetaMetricsOptIn from './metametrics-opt-in.component'
+import { setParticipateInMetaMetrics } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'Selected Create New Wallet',
+ 'import': 'Selected Import Wallet',
+}
+
+const mapStateToProps = (state) => {
+ const { firstTimeFlowType, participateInMetaMetrics } = state.metamask
+
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ firstTimeSelectionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MetaMetricsOptIn)
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
new file mode 100644
index 000000000..59b4f73a6
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
@@ -0,0 +1,155 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import shuffle from 'lodash.shuffle'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
+
+export default class ConfirmSeedPhrase extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ seedPhrase: '',
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: PropTypes.func,
+ seedPhrase: PropTypes.string,
+ }
+
+ state = {
+ selectedSeedWords: [],
+ shuffledSeedWords: [],
+ // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number}
+ selectedSeedWordsHash: {},
+ }
+
+ componentDidMount () {
+ const { seedPhrase = '' } = this.props
+ const shuffledSeedWords = shuffle(seedPhrase.split(' ')) || []
+ this.setState({ shuffledSeedWords })
+ }
+
+ handleExport = () => {
+ exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
+ }
+
+ handleSubmit = async () => {
+ const { history } = this.props
+
+ if (!this.isValid()) {
+ return
+ }
+
+ try {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Verify Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ } catch (error) {
+ console.error(error.message)
+ }
+ }
+
+ handleSelectSeedWord = (word, shuffledIndex) => {
+ this.setState(selectSeedWord(word, shuffledIndex))
+ }
+
+ handleDeselectSeedWord = shuffledIndex => {
+ this.setState(deselectSeedWord(shuffledIndex))
+ }
+
+ isValid () {
+ const { seedPhrase } = this.props
+ const { selectedSeedWords } = this.state
+ return seedPhrase === selectedSeedWords.join(' ')
+ }
+
+ render () {
+ const { t } = this.context
+ const { history } = this.props
+ const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
+
+ return (
+ <div className="confirm-seed-phrase">
+ <div className="confirm-seed-phrase__back-button">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </div>
+ <div className="first-time-flow__header">
+ { t('confirmSecretBackupPhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('selectEachPhrase') }
+ </div>
+ <div className="confirm-seed-phrase__selected-seed-words">
+ {
+ selectedSeedWords.map((word, index) => (
+ <div
+ key={index}
+ className="confirm-seed-phrase__seed-word"
+ >
+ { word }
+ </div>
+ ))
+ }
+ </div>
+ <div className="confirm-seed-phrase__shuffled-seed-words">
+ {
+ shuffledSeedWords.map((word, index) => {
+ const isSelected = index in selectedSeedWordsHash
+
+ return (
+ <div
+ key={index}
+ className={classnames(
+ 'confirm-seed-phrase__seed-word',
+ 'confirm-seed-phrase__seed-word--shuffled',
+ { 'confirm-seed-phrase__seed-word--selected': isSelected }
+ )}
+ onClick={() => {
+ if (!isSelected) {
+ this.handleSelectSeedWord(word, index)
+ } else {
+ this.handleDeselectSeedWord(index)
+ }
+ }}
+ >
+ { word }
+ </div>
+ )
+ })
+ }
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleSubmit}
+ disabled={!this.isValid()}
+ >
+ { t('confirm') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
index f2476fc5c..f2476fc5c 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
index c7b511503..c7b511503 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
index 93137618c..93137618c 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/index.js
index 185b3f089..185b3f089 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/index.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.js
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/index.scss
new file mode 100644
index 000000000..24da45ded
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.scss
@@ -0,0 +1,40 @@
+@import 'confirm-seed-phrase/index';
+
+@import 'reveal-seed-phrase/index';
+
+.seed-phrase {
+
+ &__sections {
+ display: flex;
+
+ @media screen and (min-width: $break-large) {
+ flex-direction: row;
+ }
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ &__main {
+ flex: 3;
+ min-width: 0;
+ }
+
+ &__side {
+ flex: 2;
+ min-width: 0;
+
+ @media screen and (min-width: $break-large) {
+ margin-left: 81px;
+ }
+
+ @media screen and (max-width: $break-small) {
+ margin-top: 24px;
+ }
+
+ .first-time-flow__text-block {
+ color: #5A5A5A;
+ }
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
index 4a1b191b5..4a1b191b5 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
index 8a47447ed..8a47447ed 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
new file mode 100644
index 000000000..ee352d74e
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
@@ -0,0 +1,143 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import LockIcon from '../../../../components/ui/lock-icon'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+
+export default class RevealSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ seedPhrase: PropTypes.string,
+ }
+
+ state = {
+ isShowingSeedPhrase: false,
+ }
+
+ handleExport = () => {
+ exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
+ }
+
+ handleNext = event => {
+ event.preventDefault()
+ const { isShowingSeedPhrase } = this.state
+ const { history } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Advance to Verify',
+ },
+ })
+
+ if (!isShowingSeedPhrase) {
+ return
+ }
+
+ history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE)
+ }
+
+ renderSecretWordsContainer () {
+ const { t } = this.context
+ const { seedPhrase } = this.props
+ const { isShowingSeedPhrase } = this.state
+
+ return (
+ <div className="reveal-seed-phrase__secret">
+ <div className={classnames(
+ 'reveal-seed-phrase__secret-words',
+ { 'reveal-seed-phrase__secret-words--hidden': !isShowingSeedPhrase }
+ )}>
+ { seedPhrase }
+ </div>
+ {
+ !isShowingSeedPhrase && (
+ <div
+ className="reveal-seed-phrase__secret-blocker"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Revealed Words',
+ },
+ })
+ this.setState({ isShowingSeedPhrase: true })
+ }}
+ >
+ <LockIcon
+ width="28px"
+ height="35px"
+ fill="#FFFFFF"
+ />
+ <div className="reveal-seed-phrase__reveal-button">
+ { t('clickToRevealSeed') }
+ </div>
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { isShowingSeedPhrase } = this.state
+
+ return (
+ <div className="reveal-seed-phrase">
+ <div className="seed-phrase__sections">
+ <div className="seed-phrase__main">
+ <div className="first-time-flow__header">
+ { t('secretBackupPhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('secretBackupPhraseDescription') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('secretBackupPhraseWarning') }
+ </div>
+ { this.renderSecretWordsContainer() }
+ </div>
+ <div className="seed-phrase__side">
+ <div className="first-time-flow__text-block">
+ { `${t('tips')}:` }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('storePhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('writePhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('memorizePhrase') }
+ </div>
+ <div className="first-time-flow__text-block">
+ <a
+ className="reveal-seed-phrase__export-text"
+ onClick={this.handleExport}>
+ { t('downloadSecretBackup') }
+ </a>
+ </div>
+ </div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleNext}
+ disabled={!isShowingSeedPhrase}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
new file mode 100644
index 000000000..9a9f84049
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
@@ -0,0 +1,70 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import RevealSeedPhrase from './reveal-seed-phrase'
+import ConfirmSeedPhrase from './confirm-seed-phrase'
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
+ DEFAULT_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class SeedPhrase extends PureComponent {
+ static propTypes = {
+ address: PropTypes.string,
+ history: PropTypes.object,
+ seedPhrase: PropTypes.string,
+ }
+
+ componentDidMount () {
+ const { seedPhrase, history } = this.props
+
+ if (!seedPhrase) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ render () {
+ const { seedPhrase } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
+ render={props => (
+ <ConfirmSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_SEED_PHRASE_ROUTE}
+ render={props => (
+ <RevealSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/select-action/index.js b/ui/app/pages/first-time-flow/select-action/index.js
index 4fbe1823b..4fbe1823b 100644
--- a/ui/app/components/pages/first-time-flow/select-action/index.js
+++ b/ui/app/pages/first-time-flow/select-action/index.js
diff --git a/ui/app/components/pages/first-time-flow/select-action/index.scss b/ui/app/pages/first-time-flow/select-action/index.scss
index e1b22d05b..e1b22d05b 100644
--- a/ui/app/components/pages/first-time-flow/select-action/index.scss
+++ b/ui/app/pages/first-time-flow/select-action/index.scss
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.component.js b/ui/app/pages/first-time-flow/select-action/select-action.component.js
new file mode 100644
index 000000000..b25a15514
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.component.js
@@ -0,0 +1,112 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import {
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class SelectAction extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ setFirstTimeFlowType: PropTypes.func,
+ nextRoute: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { history, isInitialized, nextRoute } = this.props
+
+ if (isInitialized) {
+ history.push(nextRoute)
+ }
+ }
+
+ handleCreate = () => {
+ this.props.setFirstTimeFlowType('create')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ handleImport = () => {
+ this.props.setFirstTimeFlowType('import')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="select-action">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.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>
+
+ <div className="select-action__wrapper">
+
+
+ <div className="select-action__body">
+ <div className="select-action__body-header">
+ { t('newToMetaMask') }
+ </div>
+ <div className="select-action__select-buttons">
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/download-alt.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('noAlreadyHaveSeed') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('importYourExisting') }
+ </div>
+ </div>
+ <Button
+ type="primary"
+ className="first-time-flow__button"
+ onClick={this.handleImport}
+ >
+ { t('importWallet') }
+ </Button>
+ </div>
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/thin-plus.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('letsGoSetUp') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('thisWillCreate') }
+ </div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleCreate}
+ >
+ { t('createAWallet') }
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.container.js b/ui/app/pages/first-time-flow/select-action/select-action.container.js
new file mode 100644
index 000000000..9dc988430
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.container.js
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { setFirstTimeFlowType } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+import Welcome from './select-action.component'
+
+const mapStateToProps = (state) => {
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setFirstTimeFlowType: type => dispatch(setFirstTimeFlowType(type)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)
diff --git a/ui/app/components/pages/first-time-flow/welcome/index.js b/ui/app/pages/first-time-flow/welcome/index.js
index 8abeddaa1..8abeddaa1 100644
--- a/ui/app/components/pages/first-time-flow/welcome/index.js
+++ b/ui/app/pages/first-time-flow/welcome/index.js
diff --git a/ui/app/components/pages/first-time-flow/welcome/index.scss b/ui/app/pages/first-time-flow/welcome/index.scss
index 3b5071480..3b5071480 100644
--- a/ui/app/components/pages/first-time-flow/welcome/index.scss
+++ b/ui/app/pages/first-time-flow/welcome/index.scss
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.component.js b/ui/app/pages/first-time-flow/welcome/welcome.component.js
new file mode 100644
index 000000000..3b8d6eb17
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.component.js
@@ -0,0 +1,69 @@
+import EventEmitter from 'events'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Mascot from '../../../components/ui/mascot'
+import Button from '../../../components/ui/button'
+import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE } from '../../../helpers/constants/routes'
+
+export default class Welcome extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ participateInMetaMetrics: PropTypes.bool,
+ welcomeScreenSeen: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentDidMount () {
+ const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props
+
+ if (welcomeScreenSeen && participateInMetaMetrics !== null) {
+ history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
+ } else if (welcomeScreenSeen) {
+ history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+ }
+
+ handleContinue = () => {
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="welcome-page__wrapper">
+ <div className="welcome-page">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="125"
+ height="125"
+ />
+ <div className="welcome-page__header">
+ { t('welcome') }
+ </div>
+ <div className="welcome-page__description">
+ <div>{ t('metamaskDescription') }</div>
+ <div>{ t('happyToSeeYou') }</div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleContinue}
+ >
+ { t('getStarted') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.container.js b/ui/app/pages/first-time-flow/welcome/welcome.container.js
new file mode 100644
index 000000000..ce4b2b471
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { closeWelcomeScreen } from '../../../store/actions'
+import Welcome from './welcome.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { welcomeScreenSeen, isInitialized, participateInMetaMetrics } = metamask
+
+ return {
+ welcomeScreenSeen,
+ isInitialized,
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)
diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js
new file mode 100644
index 000000000..29d93a9fa
--- /dev/null
+++ b/ui/app/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 '../../components/app/wallet-view'
+import TransactionView from '../../components/app/transaction-view'
+import ProviderApproval from '../provider-approval'
+
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+} from '../../helpers/constants/routes'
+
+export default class Home extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ forgottenPassword: PropTypes.bool,
+ seedWords: PropTypes.string,
+ suggestedTokens: PropTypes.object,
+ unconfirmedTransactionsCount: PropTypes.number,
+ providerRequests: PropTypes.array,
+ }
+
+ 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 {
+ forgottenPassword,
+ seedWords,
+ providerRequests,
+ } = this.props
+
+ // seed words
+ if (seedWords) {
+ return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/>
+ }
+
+ if (forgottenPassword) {
+ return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
+ }
+
+ if (providerRequests && providerRequests.length > 0) {
+ return (
+ <ProviderApproval providerRequest={providerRequests[0]} />
+ )
+ }
+
+ 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/pages/home/home.container.js b/ui/app/pages/home/home.container.js
new file mode 100644
index 000000000..02ec4b9c6
--- /dev/null
+++ b/ui/app/pages/home/home.container.js
@@ -0,0 +1,32 @@
+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,
+ providerRequests,
+ } = metamask
+ const { forgottenPassword } = appState
+
+ return {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ suggestedTokens,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
+ providerRequests,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Home)
diff --git a/ui/app/components/pages/home/index.js b/ui/app/pages/home/index.js
index 4474ba5b8..4474ba5b8 100644
--- a/ui/app/components/pages/home/index.js
+++ b/ui/app/pages/home/index.js
diff --git a/ui/app/pages/index.js b/ui/app/pages/index.js
new file mode 100644
index 000000000..56fc4af04
--- /dev/null
+++ b/ui/app/pages/index.js
@@ -0,0 +1,31 @@
+import React, { Component } from 'react'
+const PropTypes = require('prop-types')
+const { Provider } = require('react-redux')
+const { HashRouter } = require('react-router-dom')
+const Routes = require('./routes')
+const I18nProvider = require('../helpers/higher-order-components/i18n-provider')
+const MetaMetricsProvider = require('../helpers/higher-order-components/metametrics/metametrics.provider')
+
+class Index extends Component {
+ render () {
+ const { store } = this.props
+
+ return (
+ <Provider store={store}>
+ <HashRouter hashType="noslash">
+ <MetaMetricsProvider>
+ <I18nProvider>
+ <Routes />
+ </I18nProvider>
+ </MetaMetricsProvider>
+ </HashRouter>
+ </Provider>
+ )
+ }
+}
+
+Index.propTypes = {
+ store: PropTypes.object,
+}
+
+module.exports = Index
diff --git a/ui/app/pages/index.scss b/ui/app/pages/index.scss
new file mode 100644
index 000000000..cb9f0d80c
--- /dev/null
+++ b/ui/app/pages/index.scss
@@ -0,0 +1,11 @@
+@import 'unlock-page/index';
+
+@import 'add-token/index';
+
+@import 'confirm-add-token/index';
+
+@import 'settings/index';
+
+@import 'first-time-flow/index';
+
+@import 'keychains/index';
diff --git a/ui/app/components/pages/keychains/index.scss b/ui/app/pages/keychains/index.scss
index 868185419..868185419 100644
--- a/ui/app/components/pages/keychains/index.scss
+++ b/ui/app/pages/keychains/index.scss
diff --git a/ui/app/pages/keychains/restore-vault.js b/ui/app/pages/keychains/restore-vault.js
new file mode 100644
index 000000000..574949258
--- /dev/null
+++ b/ui/app/pages/keychains/restore-vault.js
@@ -0,0 +1,197 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import {
+ createNewVaultAndRestore,
+ unMarkPasswordForgotten,
+} from '../../store/actions'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+import TextField from '../../components/ui/text-field'
+import Button from '../../components/ui/button'
+
+class RestoreVaultPage extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ warning: PropTypes.string,
+ createNewVaultAndRestore: PropTypes.func.isRequired,
+ leaveImportSeedScreenState: PropTypes.func,
+ history: PropTypes.object,
+ isLoading: PropTypes.bool,
+ };
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: null,
+ passwordError: null,
+ confirmPasswordError: null,
+ }
+
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .match(/\w+/g)
+ .join(' ')
+ }
+
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = null
+
+ if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ }
+
+ this.setState({ seedPhrase, seedPhraseError })
+ }
+
+ handlePasswordChange (password) {
+ const { confirmPassword } = this.state
+ let confirmPasswordError = null
+ let passwordError = null
+
+ if (password && password.length < 8) {
+ passwordError = this.context.t('passwordNotLongEnough')
+ }
+
+ if (confirmPassword && password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
+ }
+
+ this.setState({ password, passwordError, confirmPasswordError })
+ }
+
+ handleConfirmPasswordChange (confirmPassword) {
+ const { password } = this.state
+ let confirmPasswordError = null
+
+ if (password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
+ }
+
+ this.setState({ confirmPassword, confirmPasswordError })
+ }
+
+ onClick = () => {
+ const { password, seedPhrase } = this.state
+ const {
+ createNewVaultAndRestore,
+ leaveImportSeedScreenState,
+ history,
+ } = this.props
+
+ leaveImportSeedScreenState()
+ createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
+ .then(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Retention',
+ action: 'userEntersSeedPhrase',
+ name: 'onboardingRestoredVault',
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+
+ hasError () {
+ const { passwordError, confirmPasswordError, seedPhraseError } = this.state
+ return passwordError || confirmPasswordError || seedPhraseError
+ }
+
+ render () {
+ const {
+ seedPhrase,
+ password,
+ confirmPassword,
+ seedPhraseError,
+ passwordError,
+ confirmPasswordError,
+ } = this.state
+ const { t } = this.context
+ const { isLoading } = this.props
+ const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
+
+ return (
+ <div className="first-view-main-wrapper">
+ <div className="first-view-main">
+ <div className="import-account">
+ <a
+ className="import-account__back-button"
+ onClick={e => {
+ e.preventDefault()
+ this.props.history.goBack()
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ <div className="import-account__title">
+ { this.context.t('restoreAccountWithSeed') }
+ </div>
+ <div className="import-account__selector-label">
+ { this.context.t('secretPhrase') }
+ </div>
+ <div className="import-account__input-wrapper">
+ <label className="import-account__input-label">Wallet Seed</label>
+ <textarea
+ className="import-account__secret-phrase"
+ onChange={e => this.handleSeedPhraseChange(e.target.value)}
+ value={this.state.seedPhrase}
+ placeholder={this.context.t('separateEachWord')}
+ />
+ </div>
+ <span className="error">
+ { seedPhraseError }
+ </span>
+ <TextField
+ id="password"
+ label={t('newPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.password}
+ onChange={event => this.handlePasswordChange(event.target.value)}
+ error={passwordError}
+ autoComplete="new-password"
+ margin="normal"
+ largeLabel
+ />
+ <TextField
+ id="confirm-password"
+ label={t('confirmPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.confirmPassword}
+ onChange={event => this.handleConfirmPasswordChange(event.target.value)}
+ error={confirmPasswordError}
+ autoComplete="confirm-password"
+ margin="normal"
+ largeLabel
+ />
+ <Button
+ type="first-time"
+ className="first-time-flow__button"
+ onClick={() => !disabled && this.onClick()}
+ disabled={disabled}
+ >
+ {this.context.t('restore')}
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default connect(
+ ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
+ dispatch => ({
+ leaveImportSeedScreenState: () => {
+ dispatch(unMarkPasswordForgotten())
+ },
+ createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
+ })
+)(RestoreVaultPage)
diff --git a/ui/app/pages/keychains/reveal-seed.js b/ui/app/pages/keychains/reveal-seed.js
new file mode 100644
index 000000000..edc9db5a0
--- /dev/null
+++ b/ui/app/pages/keychains/reveal-seed.js
@@ -0,0 +1,177 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+
+const { requestRevealSeedWords } = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const ExportTextContainer = require('../../components/ui/export-text-container')
+
+import Button from '../../components/ui/button'
+
+const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
+const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
+
+class RevealSeedPage extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ screen: PASSWORD_PROMPT_SCREEN,
+ password: '',
+ seedWords: null,
+ error: null,
+ }
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ this.setState({ seedWords: null, error: null })
+ this.props.requestRevealSeedWords(this.state.password)
+ .then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ renderWarning () {
+ return (
+ h('.page-container__warning-container', [
+ h('img.page-container__warning-icon', {
+ src: 'images/warning.svg',
+ }),
+ h('.page-container__warning-message', [
+ h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
+ h('div', [this.context.t('revealSeedWordsWarning')]),
+ ]),
+ ])
+ )
+ }
+
+ renderContent () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptContent()
+ : this.renderRevealSeedContent()
+ }
+
+ renderPasswordPromptContent () {
+ const { t } = this.context
+
+ return (
+ h('form', {
+ onSubmit: event => this.handleSubmit(event),
+ }, [
+ h('label.input-label', {
+ htmlFor: 'password-box',
+ }, t('enterPasswordContinue')),
+ h('.input-group', [
+ h('input.form-control', {
+ type: 'password',
+ placeholder: t('password'),
+ id: 'password-box',
+ value: this.state.password,
+ onChange: event => this.setState({ password: event.target.value }),
+ className: classnames({ 'form-control--error': this.state.error }),
+ }),
+ ]),
+ this.state.error && h('.reveal-seed__error', this.state.error),
+ ])
+ )
+ }
+
+ renderRevealSeedContent () {
+ const { t } = this.context
+
+ return (
+ h('div', [
+ h('label.reveal-seed__label', t('yourPrivateSeedPhrase')),
+ h(ExportTextContainer, {
+ text: this.state.seedWords,
+ filename: t('metamaskSeedWords'),
+ }),
+ ])
+ )
+ }
+
+ renderFooter () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptFooter()
+ : this.renderRevealSeedFooter()
+ }
+
+ renderPasswordPromptFooter () {
+ return (
+ h('.page-container__footer', [
+ 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')),
+ ]),
+ ])
+ )
+ }
+
+ renderRevealSeedFooter () {
+ return (
+ h('.page-container__footer', [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('close')),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('.page-container', [
+ h('.page-container__header', [
+ h('.page-container__title', this.context.t('revealSeedWordsTitle')),
+ h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')),
+ ]),
+ h('.page-container__content', [
+ this.renderWarning(),
+ h('.reveal-seed__content', [
+ this.renderContent(),
+ ]),
+ ]),
+ this.renderFooter(),
+ ])
+ )
+ }
+}
+
+RevealSeedPage.propTypes = {
+ requestRevealSeedWords: PropTypes.func,
+ history: PropTypes.object,
+}
+
+RevealSeedPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
+ }
+}
+
+module.exports = connect(null, mapDispatchToProps)(RevealSeedPage)
diff --git a/ui/app/components/pages/lock/index.js b/ui/app/pages/lock/index.js
index 7bfe2a61f..7bfe2a61f 100644
--- a/ui/app/components/pages/lock/index.js
+++ b/ui/app/pages/lock/index.js
diff --git a/ui/app/pages/lock/lock.component.js b/ui/app/pages/lock/lock.component.js
new file mode 100644
index 000000000..1145158c5
--- /dev/null
+++ b/ui/app/pages/lock/lock.component.js
@@ -0,0 +1,26 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Loading from '../../components/ui/loading-screen'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+
+export default class Lock extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ lockMetamask: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { lockMetamask, isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ lockMetamask().then(() => history.push(DEFAULT_ROUTE))
+ } else {
+ history.replace(DEFAULT_ROUTE)
+ }
+ }
+
+ render () {
+ return <Loading />
+ }
+}
diff --git a/ui/app/pages/lock/lock.container.js b/ui/app/pages/lock/lock.container.js
new file mode 100644
index 000000000..6a20b6ed1
--- /dev/null
+++ b/ui/app/pages/lock/lock.container.js
@@ -0,0 +1,24 @@
+import Lock from './lock.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { lockMetamask } from '../../store/actions'
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ lockMetamask: () => dispatch(lockMetamask()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Lock)
diff --git a/ui/app/pages/mobile-sync/index.js b/ui/app/pages/mobile-sync/index.js
new file mode 100644
index 000000000..0938ad103
--- /dev/null
+++ b/ui/app/pages/mobile-sync/index.js
@@ -0,0 +1,387 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+const PubNub = require('pubnub')
+
+const { requestRevealSeedWords, fetchInfoToSync } = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const actions = require('../../store/actions')
+
+const qrCode = require('qrcode-generator')
+
+import Button from '../../components/ui/button'
+import LoadingScreen from '../../components/ui/loading-screen'
+
+const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
+const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
+
+class MobileSyncPage extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ selectedAddress: PropTypes.string,
+ displayWarning: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ requestRevealSeedWords: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ screen: PASSWORD_PROMPT_SCREEN,
+ password: '',
+ seedWords: null,
+ error: null,
+ syncing: false,
+ completed: false,
+ }
+
+ this.syncing = false
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ this.setState({ seedWords: null, error: null })
+ this.props.requestRevealSeedWords(this.state.password)
+ .then(seedWords => {
+ this.generateCipherKeyAndChannelName()
+ this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })
+ this.initWebsockets()
+ })
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ generateCipherKeyAndChannelName () {
+ this.cipherKey = `${this.props.selectedAddress.substr(-4)}-${PubNub.generateUUID()}`
+ this.channelName = `mm-${PubNub.generateUUID()}`
+ }
+
+ initWebsockets () {
+ this.pubnub = new PubNub({
+ subscribeKey: process.env.PUBNUB_SUB_KEY,
+ publishKey: process.env.PUBNUB_PUB_KEY,
+ cipherKey: this.cipherKey,
+ ssl: true,
+ })
+
+ this.pubnubListener = this.pubnub.addListener({
+ message: (data) => {
+ const {channel, message} = data
+ // handle message
+ if (channel !== this.channelName || !message) {
+ return false
+ }
+
+ if (message.event === 'start-sync') {
+ this.startSyncing()
+ } else if (message.event === 'end-sync') {
+ this.disconnectWebsockets()
+ this.setState({syncing: false, completed: true})
+ }
+ },
+ })
+
+ this.pubnub.subscribe({
+ channels: [this.channelName],
+ withPresence: false,
+ })
+
+ }
+
+ disconnectWebsockets () {
+ if (this.pubnub && this.pubnubListener) {
+ this.pubnub.disconnect(this.pubnubListener)
+ }
+ }
+
+ // Calculating a PubNub Message Payload Size.
+ calculatePayloadSize (channel, message) {
+ return encodeURIComponent(
+ channel + JSON.stringify(message)
+ ).length + 100
+ }
+
+ chunkString (str, size) {
+ const numChunks = Math.ceil(str.length / size)
+ const chunks = new Array(numChunks)
+ for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
+ chunks[i] = str.substr(o, size)
+ }
+ return chunks
+ }
+
+ notifyError (errorMsg) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'error-sync',
+ data: errorMsg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ })
+ })
+ }
+
+ async startSyncing () {
+ if (this.syncing) return false
+ this.syncing = true
+ this.setState({syncing: true})
+
+ const { accounts, network, preferences, transactions } = await this.props.fetchInfoToSync()
+
+ const allDataStr = JSON.stringify({
+ accounts,
+ network,
+ preferences,
+ transactions,
+ udata: {
+ pwd: this.state.password,
+ seed: this.state.seedWords,
+ },
+ })
+
+ const chunks = this.chunkString(allDataStr, 17000)
+ const totalChunks = chunks.length
+ try {
+ for (let i = 0; i < totalChunks; i++) {
+ await this.sendMessage(chunks[i], i + 1, totalChunks)
+ }
+ } catch (e) {
+ this.props.displayWarning('Sync failed :(')
+ this.setState({syncing: false})
+ this.syncing = false
+ this.notifyError(e.toString())
+ }
+ }
+
+ sendMessage (data, pkg, count) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'syncing-data',
+ data,
+ totalPkg: count,
+ currentPkg: pkg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ }
+ )
+ })
+ }
+
+
+ componentWillUnmount () {
+ this.disconnectWebsockets()
+ }
+
+ renderWarning (text) {
+ return (
+ h('.page-container__warning-container', [
+ h('.page-container__warning-message', [
+ h('div', [text]),
+ ]),
+ ])
+ )
+ }
+
+ renderContent () {
+ const { t } = this.context
+
+ if (this.state.syncing) {
+ return h(LoadingScreen, {loadingMessage: 'Sync in progress'})
+ }
+
+ if (this.state.completed) {
+ return h('div.reveal-seed__content', {},
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileComplete')),
+ )
+ }
+
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? h('div', {}, [
+ this.renderWarning(this.context.t('mobileSyncText')),
+ h('.reveal-seed__content', [
+ this.renderPasswordPromptContent(),
+ ]),
+ ])
+ : h('div', {}, [
+ this.renderWarning(this.context.t('syncWithMobileBeCareful')),
+ h('.reveal-seed__content', [ this.renderRevealSeedContent() ]),
+ ])
+ }
+
+ renderPasswordPromptContent () {
+ const { t } = this.context
+
+ return (
+ h('form', {
+ onSubmit: event => this.handleSubmit(event),
+ }, [
+ h('label.input-label', {
+ htmlFor: 'password-box',
+ }, t('enterPasswordContinue')),
+ h('.input-group', [
+ h('input.form-control', {
+ type: 'password',
+ placeholder: t('password'),
+ id: 'password-box',
+ value: this.state.password,
+ onChange: event => this.setState({ password: event.target.value }),
+ className: classnames({ 'form-control--error': this.state.error }),
+ }),
+ ]),
+ this.state.error && h('.reveal-seed__error', this.state.error),
+ ])
+ )
+ }
+
+ renderRevealSeedContent () {
+
+ const qrImage = qrCode(0, 'M')
+ qrImage.addData(`metamask-sync:${this.channelName}|@|${this.cipherKey}`)
+ qrImage.make()
+
+ const { t } = this.context
+ return (
+ h('div', [
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileScanThisCode')),
+ h('.div.qr-wrapper', {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ dangerouslySetInnerHTML: {
+ __html: qrImage.createTableTag(4),
+ },
+ }),
+ ])
+ )
+ }
+
+ renderFooter () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptFooter()
+ : this.renderRevealSeedFooter()
+ }
+
+ renderPasswordPromptFooter () {
+ return (
+ h('div.new-account-import-form__buttons', {style: {padding: 30}}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: event => this.handleSubmit(event),
+ disabled: this.state.password === '',
+ }, this.context.t('next')),
+ ])
+ )
+ }
+
+ renderRevealSeedFooter () {
+ return (
+ h('.page-container__footer', {style: {padding: 30}}, [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('close')),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('.page-container', [
+ h('.page-container__header', [
+ h('.page-container__title', this.context.t('syncWithMobileTitle')),
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDesc')) : null,
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDescNewUsers')) : null,
+ ]),
+ h('.page-container__content', [
+ this.renderContent(),
+ ]),
+ this.renderFooter(),
+ ])
+ )
+ }
+}
+
+MobileSyncPage.propTypes = {
+ requestRevealSeedWords: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ history: PropTypes.object,
+}
+
+MobileSyncPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
+ fetchInfoToSync: () => dispatch(fetchInfoToSync()),
+ displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
+ }
+
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { selectedAddress },
+ } = state
+
+ return {
+ selectedAddress,
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(MobileSyncPage)
diff --git a/ui/app/pages/notice/notice.js b/ui/app/pages/notice/notice.js
new file mode 100644
index 000000000..d8274dfcb
--- /dev/null
+++ b/ui/app/pages/notice/notice.js
@@ -0,0 +1,203 @@
+const { Component } = require('react')
+const h = require('react-hyperscript')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const ReactMarkdown = require('react-markdown')
+const linker = require('extension-link-enabler')
+const generateLostAccountsNotice = require('../../../lib/lost-accounts-notice')
+const findDOMNode = require('react-dom').findDOMNode
+const actions = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+
+class Notice extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ disclaimerDisabled: true,
+ }
+ }
+
+ componentWillMount () {
+ if (!this.props.notice) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ componentDidMount () {
+ // eslint-disable-next-line react/no-find-dom-node
+ var node = findDOMNode(this)
+ linker.setupListener(node)
+ if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
+ this.setState({ disclaimerDisabled: false })
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (!nextProps.notice) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ componentWillUnmount () {
+ // eslint-disable-next-line react/no-find-dom-node
+ var node = findDOMNode(this)
+ linker.teardownListener(node)
+ }
+
+ handleAccept () {
+ this.setState({ disclaimerDisabled: true })
+ this.props.onConfirm()
+ }
+
+ render () {
+ const { notice = {} } = this.props
+ const { title, date, body } = notice
+ const { disclaimerDisabled } = this.state
+
+ return (
+ h('.flex-column.flex-center.flex-grow', {
+ style: {
+ width: '100%',
+ },
+ }, [
+ h('h3.flex-center.text-transform-uppercase.terms-header', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ width: '100%',
+ fontSize: '20px',
+ textAlign: 'center',
+ padding: 6,
+ },
+ }, [
+ title,
+ ]),
+
+ h('h5.flex-center.text-transform-uppercase.terms-header', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginBottom: 24,
+ width: '100%',
+ fontSize: '20px',
+ textAlign: 'center',
+ padding: 6,
+ },
+ }, [
+ date,
+ ]),
+
+ h('style', `
+
+ .markdown {
+ overflow-x: hidden;
+ }
+
+ .markdown h1, .markdown h2, .markdown h3 {
+ margin: 10px 0;
+ font-weight: bold;
+ }
+
+ .markdown strong {
+ font-weight: bold;
+ }
+ .markdown em {
+ font-style: italic;
+ }
+
+ .markdown p {
+ margin: 10px 0;
+ }
+
+ .markdown a {
+ color: #df6b0e;
+ }
+
+ `),
+
+ h('div.markdown', {
+ onScroll: (e) => {
+ var object = e.currentTarget
+ if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) {
+ this.setState({ disclaimerDisabled: false })
+ }
+ },
+ style: {
+ background: 'rgb(235, 235, 235)',
+ height: '310px',
+ padding: '6px',
+ width: '90%',
+ overflowY: 'scroll',
+ scroll: 'auto',
+ },
+ }, [
+ h(ReactMarkdown, {
+ className: 'notice-box',
+ source: body,
+ skipHtml: true,
+ }),
+ ]),
+
+ h('button.primary', {
+ disabled: disclaimerDisabled,
+ onClick: () => this.handleAccept(),
+ style: {
+ marginTop: '18px',
+ },
+ }, 'Accept'),
+ ])
+ )
+ }
+
+}
+
+const mapStateToProps = state => {
+ const { metamask } = state
+ const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask
+
+ return {
+ noActiveNotices,
+ nextUnreadNotice,
+ lostAccounts,
+ }
+}
+
+Notice.propTypes = {
+ notice: PropTypes.object,
+ onConfirm: PropTypes.func,
+ history: PropTypes.object,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)),
+ markAccountsFound: () => dispatch(actions.markAccountsFound()),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps
+ const { markNoticeRead, markAccountsFound } = dispatchProps
+
+ let notice
+ let onConfirm
+
+ if (!noActiveNotices) {
+ notice = nextUnreadNotice
+ onConfirm = () => markNoticeRead(nextUnreadNotice)
+ } else if (lostAccounts && lostAccounts.length > 0) {
+ notice = generateLostAccountsNotice(lostAccounts)
+ onConfirm = () => markAccountsFound()
+ }
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ notice,
+ onConfirm,
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notice)
diff --git a/ui/app/components/pages/provider-approval/index.js b/ui/app/pages/provider-approval/index.js
index 4162f3155..4162f3155 100644
--- a/ui/app/components/pages/provider-approval/index.js
+++ b/ui/app/pages/provider-approval/index.js
diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js
new file mode 100644
index 000000000..1f1d68da7
--- /dev/null
+++ b/ui/app/pages/provider-approval/provider-approval.component.js
@@ -0,0 +1,29 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import ProviderPageContainer from '../../components/app/provider-page-container'
+
+export default class ProviderApproval extends Component {
+ static propTypes = {
+ approveProviderRequest: PropTypes.func.isRequired,
+ providerRequest: PropTypes.object.isRequired,
+ rejectProviderRequest: PropTypes.func.isRequired,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props
+ return (
+ <ProviderPageContainer
+ approveProviderRequest={approveProviderRequest}
+ origin={providerRequest.origin}
+ tabID={providerRequest.tabID}
+ rejectProviderRequest={rejectProviderRequest}
+ siteImage={providerRequest.siteImage}
+ siteTitle={providerRequest.siteTitle}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js
new file mode 100644
index 000000000..d53c0ae4d
--- /dev/null
+++ b/ui/app/pages/provider-approval/provider-approval.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ProviderApproval from './provider-approval.component'
+import { approveProviderRequest, rejectProviderRequest } from '../../store/actions'
+
+function mapDispatchToProps (dispatch) {
+ return {
+ approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)),
+ rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)),
+ }
+}
+
+export default connect(null, mapDispatchToProps)(ProviderApproval)
diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js
new file mode 100644
index 000000000..460cec958
--- /dev/null
+++ b/ui/app/pages/routes/index.js
@@ -0,0 +1,441 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { Route, Switch, withRouter, matchPath } from 'react-router-dom'
+import { compose } from 'recompose'
+import actions from '../../store/actions'
+import log from 'loglevel'
+import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors'
+
+// init
+import FirstTimeFlow from '../first-time-flow'
+// accounts
+const SendTransactionScreen = require('../../components/app/send/send.container')
+const ConfirmTransaction = require('../confirm-transaction')
+
+// slideout menu
+const Sidebar = require('../../components/app/sidebars').default
+const { WALLET_VIEW_SIDEBAR } = require('../../components/app/sidebars/sidebar.constants')
+
+// other views
+import Home from '../home'
+import Settings from '../settings'
+import Authenticated from '../../helpers/higher-order-components/authenticated'
+import Initialized from '../../helpers/higher-order-components/initialized'
+import Lock from '../lock'
+import UiMigrationAnnouncement from '../../components/app/ui-migration-annoucement'
+const RestoreVaultPage = require('../keychains/restore-vault').default
+const RevealSeedConfirmation = require('../keychains/reveal-seed')
+const MobileSyncPage = require('../mobile-sync')
+const AddTokenPage = require('../add-token')
+const ConfirmAddTokenPage = require('../confirm-add-token')
+const ConfirmAddSuggestedTokenPage = require('../confirm-add-suggested-token')
+const CreateAccountPage = require('../create-account')
+const NoticeScreen = require('../notice/notice')
+
+const Loading = require('../../components/ui/loading-screen')
+const LoadingNetwork = require('../../components/app/loading-network-screen').default
+const NetworkDropdown = require('../../components/app/dropdowns/network-dropdown')
+import AccountMenu from '../../components/app/account-menu'
+
+// Global Modals
+const Modal = require('../../components/app/modals').Modal
+// Global Alert
+const Alert = require('../../components/ui/alert')
+
+import AppHeader from '../../components/app/app-header'
+import UnlockPage from '../unlock-page'
+
+import {
+ submittedPendingTransactionsSelector,
+} from '../../selectors/transactions'
+
+// Routes
+import {
+ DEFAULT_ROUTE,
+ LOCK_ROUTE,
+ UNLOCK_ROUTE,
+ SETTINGS_ROUTE,
+ REVEAL_SEED_ROUTE,
+ MOBILE_SYNC_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+ NEW_ACCOUNT_ROUTE,
+ SEND_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ INITIALIZE_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ NOTICE_ROUTE,
+} from '../../helpers/constants/routes'
+
+// enums
+import {
+ ENVIRONMENT_TYPE_NOTIFICATION,
+ ENVIRONMENT_TYPE_POPUP,
+} from '../../../../app/scripts/lib/enums'
+
+class Routes extends Component {
+ componentWillMount () {
+ const { currentCurrency, setCurrentCurrencyToUSD } = this.props
+
+ if (!currentCurrency) {
+ setCurrentCurrencyToUSD()
+ }
+
+ this.props.history.listen((locationObj, action) => {
+ if (action === 'PUSH') {
+ const url = `&url=${encodeURIComponent('http://www.metamask.io/metametrics' + locationObj.pathname)}`
+ this.context.metricsEvent({}, {
+ currentPath: '',
+ pathname: locationObj.pathname,
+ url,
+ pageOpts: {
+ hideDimensions: true,
+ },
+ })
+ }
+ })
+ }
+
+ renderRoutes () {
+ return (
+ <Switch>
+ <Route path={LOCK_ROUTE} component={Lock} exact />
+ <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} />
+ <Initialized path={UNLOCK_ROUTE} component={UnlockPage} exact />
+ <Initialized path={RESTORE_VAULT_ROUTE} component={RestoreVaultPage} exact />
+ <Authenticated path={REVEAL_SEED_ROUTE} component={RevealSeedConfirmation} exact />
+ <Authenticated path={MOBILE_SYNC_ROUTE} component={MobileSyncPage} exact />
+ <Authenticated path={SETTINGS_ROUTE} component={Settings} />
+ <Authenticated path={NOTICE_ROUTE} component={NoticeScreen} exact />
+ <Authenticated path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransaction} />
+ <Authenticated path={SEND_ROUTE} component={SendTransactionScreen} exact />
+ <Authenticated path={ADD_TOKEN_ROUTE} component={AddTokenPage} exact />
+ <Authenticated path={CONFIRM_ADD_TOKEN_ROUTE} component={ConfirmAddTokenPage} exact />
+ <Authenticated path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE} component={ConfirmAddSuggestedTokenPage} exact />
+ <Authenticated path={NEW_ACCOUNT_ROUTE} component={CreateAccountPage} />
+ <Authenticated path={DEFAULT_ROUTE} component={Home} exact />
+ </Switch>
+ )
+ }
+
+ onInitializationUnlockPage () {
+ const { location } = this.props
+ return Boolean(matchPath(location.pathname, { path: INITIALIZE_UNLOCK_ROUTE, exact: true }))
+ }
+
+ onConfirmPage () {
+ const { location } = this.props
+ return Boolean(matchPath(location.pathname, { path: CONFIRM_TRANSACTION_ROUTE, exact: false }))
+ }
+
+ hasProviderRequests () {
+ const { providerRequests } = this.props
+ return Array.isArray(providerRequests) && providerRequests.length > 0
+ }
+
+ hideAppHeader () {
+ const { location } = this.props
+
+ const isInitializing = Boolean(matchPath(location.pathname, {
+ path: INITIALIZE_ROUTE, exact: false,
+ }))
+
+ if (isInitializing && !this.onInitializationUnlockPage()) {
+ return true
+ }
+
+ if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
+ return true
+ }
+
+ if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP) {
+ return this.onConfirmPage() || this.hasProviderRequests()
+ }
+ }
+
+ render () {
+ const {
+ isLoading,
+ alertMessage,
+ loadingMessage,
+ network,
+ provider,
+ frequentRpcListDetail,
+ currentView,
+ setMouseUserState,
+ sidebar,
+ submittedPendingTransactions,
+ } = this.props
+ const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
+ const loadMessage = loadingMessage || isLoadingNetwork ?
+ this.getConnectingLabel(loadingMessage) : null
+ log.debug('Main ui render function')
+
+ const sidebarOnOverlayClose = sidebarType === WALLET_VIEW_SIDEBAR
+ ? () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Wallet Sidebar',
+ name: 'Closed Sidebare Via Overlay',
+ },
+ })
+ }
+ : null
+
+ const {
+ isOpen: sidebarIsOpen,
+ transitionName: sidebarTransitionName,
+ type: sidebarType,
+ props,
+ } = sidebar
+ const { transaction: sidebarTransaction } = props || {}
+
+ return (
+ <div
+ className="app"
+ onClick={() => setMouseUserState(true)}
+ onKeyDown={e => {
+ if (e.keyCode === 9) {
+ setMouseUserState(false)
+ }
+ }}
+ >
+ <UiMigrationAnnouncement />
+ <Modal />
+ <Alert
+ visible={this.props.alertOpen}
+ msg={alertMessage}
+ />
+ {
+ !this.hideAppHeader() && (
+ <AppHeader
+ hideNetworkIndicator={this.onInitializationUnlockPage()}
+ disabled={this.onConfirmPage()}
+ />
+ )
+ }
+ <Sidebar
+ sidebarOpen={sidebarIsOpen}
+ sidebarShouldClose={sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)}
+ hideSidebar={this.props.hideSidebar}
+ transitionName={sidebarTransitionName}
+ type={sidebarType}
+ sidebarProps={sidebar.props}
+ onOverlayClose={sidebarOnOverlayClose}
+ />
+ <NetworkDropdown
+ provider={provider}
+ frequentRpcListDetail={frequentRpcListDetail}
+ />
+ <AccountMenu />
+ <div className="main-container-wrapper">
+ { isLoading && <Loading loadingMessage={loadMessage} /> }
+ { !isLoading && isLoadingNetwork && <LoadingNetwork /> }
+ { this.renderRoutes() }
+ </div>
+ </div>
+ )
+ }
+
+ toggleMetamaskActive () {
+ if (!this.props.isUnlocked) {
+ // currently inactive: redirect to password box
+ var passwordBox = document.querySelector('input[type=password]')
+ if (!passwordBox) return
+ passwordBox.focus()
+ } else {
+ // currently active: deactivate
+ this.props.dispatch(actions.lockMetamask(false))
+ }
+ }
+
+ getConnectingLabel = function (loadingMessage) {
+ if (loadingMessage) {
+ return loadingMessage
+ }
+ const { provider, providerId } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('connectingToMainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('connectingToRopsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('connectingToKovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('connectingToRinkeby')
+ } else {
+ name = this.context.t('connectingTo', [providerId])
+ }
+
+ return name
+ }
+
+ getNetworkName () {
+ const { provider } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('mainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('ropsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('kovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('rinkeby')
+ } else {
+ name = this.context.t('unknownNetwork')
+ }
+
+ return name
+ }
+}
+
+Routes.propTypes = {
+ currentCurrency: PropTypes.string,
+ setCurrentCurrencyToUSD: PropTypes.func,
+ isLoading: PropTypes.bool,
+ loadingMessage: PropTypes.string,
+ alertMessage: PropTypes.string,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ frequentRpcListDetail: PropTypes.array,
+ currentView: PropTypes.object,
+ sidebar: PropTypes.object,
+ alertOpen: PropTypes.bool,
+ hideSidebar: PropTypes.func,
+ isOnboarding: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ networkDropdownOpen: PropTypes.bool,
+ showNetworkDropdown: PropTypes.func,
+ hideNetworkDropdown: PropTypes.func,
+ history: PropTypes.object,
+ location: PropTypes.object,
+ dispatch: PropTypes.func,
+ toggleAccountMenu: 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,
+ submittedPendingTransactions: PropTypes.array,
+ unapprovedMsgCount: PropTypes.number,
+ unapprovedPersonalMsgCount: PropTypes.number,
+ unapprovedTypedMessagesCount: PropTypes.number,
+ welcomeScreenSeen: PropTypes.bool,
+ isPopup: PropTypes.bool,
+ isMouseUser: PropTypes.bool,
+ setMouseUserState: PropTypes.func,
+ t: PropTypes.func,
+ providerId: PropTypes.string,
+ providerRequests: PropTypes.array,
+}
+
+function mapStateToProps (state) {
+ const { appState, metamask } = state
+ const {
+ networkDropdownOpen,
+ sidebar,
+ alertOpen,
+ alertMessage,
+ isLoading,
+ loadingMessage,
+ } = appState
+
+ const accounts = getMetaMaskAccounts(state)
+
+ const {
+ identities,
+ address,
+ keyrings,
+ isInitialized,
+ noActiveNotices,
+ seedWords,
+ unapprovedTxs,
+ nextUnreadNotice,
+ lostAccounts,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ providerRequests,
+ } = metamask
+ const selected = address || Object.keys(accounts)[0]
+
+ return {
+ // state from plugin
+ networkDropdownOpen,
+ sidebar,
+ alertOpen,
+ alertMessage,
+ isLoading,
+ loadingMessage,
+ noActiveNotices,
+ isInitialized,
+ isUnlocked: state.metamask.isUnlocked,
+ selectedAddress: state.metamask.selectedAddress,
+ currentView: state.appState.currentView,
+ activeAddress: state.appState.activeAddress,
+ transForward: state.appState.transForward,
+ isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
+ isPopup: state.metamask.isPopup,
+ seedWords: state.metamask.seedWords,
+ submittedPendingTransactions: submittedPendingTransactionsSelector(state),
+ 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,
+ frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
+ currentCurrency: state.metamask.currentCurrency,
+ isMouseUser: state.appState.isMouseUser,
+ isRevealingSeedWords: state.metamask.isRevealingSeedWords,
+ Qr: state.appState.Qr,
+ welcomeScreenSeen: state.metamask.welcomeScreenSeen,
+ providerId: getNetworkIdentifier(state),
+
+ // state needed to get account dropdown temporarily rendering from app bar
+ identities,
+ selected,
+ keyrings,
+ providerRequests,
+ }
+}
+
+function mapDispatchToProps (dispatch, ownProps) {
+ return {
+ dispatch,
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
+ setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
+ toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
+ setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
+ }
+}
+
+Routes.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Routes)
diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
new file mode 100644
index 000000000..d1cad1746
--- /dev/null
+++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
@@ -0,0 +1,378 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import validUrl from 'valid-url'
+import { exportAsFile } from '../../../helpers/utils/util'
+import ToggleButton from 'react-toggle-button'
+import TextField from '../../../components/ui/text-field'
+import Button from '../../../components/ui/button'
+import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'
+
+export default class AdvancedTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ setHexDataFeatureFlag: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ showResetAccountConfirmationModal: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ sendHexData: PropTypes.bool,
+ setAdvancedInlineGasFeatureFlag: PropTypes.func,
+ advancedInlineGas: PropTypes.bool,
+ showFiatInTestnets: PropTypes.bool,
+ setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
+ }
+
+ state = {
+ newRpc: '',
+ chainId: '',
+ showOptions: false,
+ ticker: '',
+ nickname: '',
+ }
+
+ renderNewRpcUrl () {
+ const { t } = this.context
+ const { newRpc, chainId, ticker, nickname } = this.state
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('newNetwork') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <TextField
+ type="text"
+ id="new-rpc"
+ placeholder={t('rpcURL')}
+ value={newRpc}
+ onChange={e => this.setState({ newRpc: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="chainid"
+ placeholder={t('optionalChainId')}
+ value={chainId}
+ onChange={e => this.setState({ chainId: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="ticker"
+ placeholder={t('optionalSymbol')}
+ value={ticker}
+ onChange={e => this.setState({ ticker: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="nickname"
+ placeholder={t('optionalNickname')}
+ value={nickname}
+ onChange={e => this.setState({ nickname: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <div className="flex-row flex-align-center space-between">
+ <span className="settings-tab__advanced-link"
+ onClick={e => {
+ e.preventDefault()
+ this.setState({ showOptions: !this.state.showOptions })
+ }}
+ >
+ { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') }
+ </span>
+ <button
+ className="button btn-primary settings-tab__rpc-save-button"
+ onClick={e => {
+ e.preventDefault()
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }}
+ >
+ { t('save') }
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ validateRpc (newRpc, chainId, ticker = 'ETH', nickname) {
+ const { setRpcTarget, displayWarning } = this.props
+ if (validUrl.isWebUri(newRpc)) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Custom RPC',
+ name: 'Success',
+ },
+ customVariables: {
+ networkId: newRpc,
+ chainId,
+ },
+ })
+ if (!!chainId && Number.isNaN(parseInt(chainId))) {
+ return displayWarning(`${this.context.t('invalidInput')} chainId`)
+ }
+
+ setRpcTarget(newRpc, chainId, ticker, nickname)
+ } else {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Custom RPC',
+ name: 'Error',
+ },
+ customVariables: {
+ networkId: newRpc,
+ chainId,
+ },
+ })
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning(this.context.t('uriErrorMsg'))
+ } else {
+ displayWarning(this.context.t('invalidRPC'))
+ }
+ }
+ }
+
+ renderMobileSync () {
+ const { t } = this.context
+ const { history } = this.props
+//
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('syncWithMobile') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="primary"
+ large
+ onClick={event => {
+ event.preventDefault()
+ history.push(MOBILE_SYNC_ROUTE)
+ }}
+ >
+ { t('syncWithMobile') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ 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>
+ )
+ }
+
+ 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()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Reset Account',
+ name: 'Reset Account',
+ },
+ })
+ showResetAccountConfirmationModal()
+ }}
+ >
+ { t('resetAccount') }
+ </Button>
+ </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>
+ )
+ }
+
+ renderAdvancedGasInputInline () {
+ const { t } = this.context
+ const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showAdvancedGasInline') }</span>
+ <div className="settings-page__content-description">
+ { t('showAdvancedGasInlineDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={advancedInlineGas}
+ onToggle={value => setAdvancedInlineGasFeatureFlag(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderShowConversionInTestnets () {
+ const { t } = this.context
+ const {
+ showFiatInTestnets,
+ setShowFiatConversionOnTestnetsPreference,
+ } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showFiatConversionInTestnets') }</span>
+ <div className="settings-page__content-description">
+ { t('showFiatConversionInTestnetsDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={showFiatInTestnets}
+ onToggle={value => setShowFiatConversionOnTestnetsPreference(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderContent () {
+ const { warning } = this.props
+
+ return (
+ <div className="settings-page__body">
+ { warning && <div className="settings-tab__error">{ warning }</div> }
+ { this.renderStateLogs() }
+ { this.renderMobileSync() }
+ { this.renderNewRpcUrl() }
+ { this.renderResetAccount() }
+ { this.renderAdvancedGasInputInline() }
+ { this.renderHexDataOptIn() }
+ { this.renderShowConversionInTestnets() }
+ </div>
+ )
+ }
+
+ render () {
+ return this.renderContent()
+ }
+}
diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js
new file mode 100644
index 000000000..69d7e07e6
--- /dev/null
+++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js
@@ -0,0 +1,48 @@
+import AdvancedTab from './advanced-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ updateAndSetCustomRpc,
+ displayWarning,
+ setFeatureFlag,
+ showModal,
+ setShowFiatConversionOnTestnetsPreference,
+} from '../../../store/actions'
+import {preferencesSelector} from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask } = state
+ const {
+ featureFlags: {
+ sendHexData,
+ advancedInlineGas,
+ } = {},
+ } = metamask
+ const { showFiatInTestnets } = preferencesSelector(state)
+
+ return {
+ warning,
+ sendHexData,
+ advancedInlineGas,
+ showFiatInTestnets,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
+ setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)),
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
+ setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
+ setShowFiatConversionOnTestnetsPreference: value => {
+ return dispatch(setShowFiatConversionOnTestnetsPreference(value))
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(AdvancedTab)
diff --git a/ui/app/pages/settings/advanced-tab/index.js b/ui/app/pages/settings/advanced-tab/index.js
new file mode 100644
index 000000000..85955174e
--- /dev/null
+++ b/ui/app/pages/settings/advanced-tab/index.js
@@ -0,0 +1 @@
+export { default } from './advanced-tab.container'
diff --git a/ui/app/components/pages/settings/index.js b/ui/app/pages/settings/index.js
index 44a9ffa63..44a9ffa63 100644
--- a/ui/app/components/pages/settings/index.js
+++ b/ui/app/pages/settings/index.js
diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss
new file mode 100644
index 000000000..52208dc85
--- /dev/null
+++ b/ui/app/pages/settings/index.scss
@@ -0,0 +1,143 @@
+@import 'info-tab/index';
+
+@import 'settings-tab/index';
+
+.settings-page {
+ position: relative;
+ background: $white;
+ display: flex;
+ flex-flow: column nowrap;
+
+ &__header {
+ display: flex;
+ flex-flow: row nowrap;
+ padding: 12px 24px;
+ align-items: center;
+ border-bottom: 1px solid $alto;
+ flex: 0 0 auto;
+
+ &__title {
+ flex: 1 0 auto;
+ font-size: 24px;
+ }
+ }
+
+ &__back-button {
+ display: none;
+
+ @media screen and (max-width: 575px) {
+ display: block;
+ background-image: url('/images/caret-left-black.svg');
+ width: 18px;
+ height: 18px;
+ opacity: .5;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ margin-right: 16px;
+ cursor: pointer;
+ }
+ }
+
+ &__close-button::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ cursor: pointer;
+ }
+
+ &__content {
+ display: flex;
+ flex-flow: row nowrap;
+ height: auto;
+ overflow: auto;
+
+ &__tabs {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+
+ @media screen and (min-width: 576px) {
+ flex: 0 0 32%;
+ max-width: 210px;
+ border-right: 1px solid $alto;
+ }
+ }
+
+ &__modules {
+ overflow-y: auto;
+ flex: 1 1 auto;
+
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+ }
+ }
+
+ &__body {
+ padding: 12px 24px;
+ }
+
+ &__content-row {
+ display: flex;
+ flex-direction: column;
+ padding: 10px 0 20px;
+ }
+
+ &__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%;
+ }
+ }
+
+ &--selected {
+ .settings-page {
+ &__content {
+ &__tabs {
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+ }
+
+ &__modules {
+ @media screen and (max-width: 575px) {
+ display: block;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/app/components/pages/settings/info-tab/index.js b/ui/app/pages/settings/info-tab/index.js
index 7556a258d..7556a258d 100644
--- a/ui/app/components/pages/settings/info-tab/index.js
+++ b/ui/app/pages/settings/info-tab/index.js
diff --git a/ui/app/components/pages/settings/info-tab/index.scss b/ui/app/pages/settings/info-tab/index.scss
index 43ad6f652..43ad6f652 100644
--- a/ui/app/components/pages/settings/info-tab/index.scss
+++ b/ui/app/pages/settings/info-tab/index.scss
diff --git a/ui/app/pages/settings/info-tab/info-tab.component.js b/ui/app/pages/settings/info-tab/info-tab.component.js
new file mode 100644
index 000000000..552dd156e
--- /dev/null
+++ b/ui/app/pages/settings/info-tab/info-tab.component.js
@@ -0,0 +1,140 @@
+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>
+ )
+ }
+
+ renderContent () {
+ const { t } = this.context
+
+ return (
+ <div className="settings-page__body">
+ <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>
+ )
+ }
+
+ render () {
+ return this.renderContent()
+ }
+}
diff --git a/ui/app/pages/settings/security-tab/index.js b/ui/app/pages/settings/security-tab/index.js
new file mode 100644
index 000000000..7ffc291a2
--- /dev/null
+++ b/ui/app/pages/settings/security-tab/index.js
@@ -0,0 +1 @@
+export { default } from './security-tab.container'
diff --git a/ui/app/pages/settings/security-tab/security-tab.component.js b/ui/app/pages/settings/security-tab/security-tab.component.js
new file mode 100644
index 000000000..233561115
--- /dev/null
+++ b/ui/app/pages/settings/security-tab/security-tab.component.js
@@ -0,0 +1,195 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { exportAsFile } from '../../../helpers/utils/util'
+import ToggleButton from 'react-toggle-button'
+import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes'
+import Button from '../../../components/ui/button'
+
+export default class SecurityTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ setPrivacyMode: PropTypes.func,
+ privacyMode: PropTypes.bool,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ showClearApprovalModal: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ mobileSync: PropTypes.bool,
+ participateInMetaMetrics: PropTypes.bool,
+ setParticipateInMetaMetrics: PropTypes.func,
+ }
+
+ 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>
+ )
+ }
+
+ renderClearApproval () {
+ const { t } = this.context
+ const { showClearApprovalModal } = this.props
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('approvalData') }</span>
+ <span className="settings-page__content-description">
+ { t('approvalDataDescription') }
+ </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()
+ showClearApprovalModal()
+ }}
+ >
+ { t('clearApprovalData') }
+ </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()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Reveal Seed Phrase',
+ name: 'Reveal Seed Phrase',
+ },
+ })
+ history.push(REVEAL_SEED_ROUTE)
+ }}
+ >
+ { t('revealSeedWords') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderPrivacyOptIn () {
+ const { t } = this.context
+ const { privacyMode, setPrivacyMode } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('privacyMode') }</span>
+ <div className="settings-page__content-description">
+ { t('privacyModeDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={privacyMode}
+ onToggle={value => setPrivacyMode(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderMetaMetricsOptIn () {
+ const { t } = this.context
+ const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('participateInMetaMetrics') }</span>
+ <div className="settings-page__content-description">
+ <span>{ t('participateInMetaMetricsDescription') }</span>
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={participateInMetaMetrics}
+ onToggle={value => setParticipateInMetaMetrics(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderContent () {
+ const { warning } = this.props
+
+ return (
+ <div className="settings-page__body">
+ { warning && <div className="settings-tab__error">{ warning }</div> }
+ { this.renderPrivacyOptIn() }
+ { this.renderClearApproval() }
+ { this.renderSeedWords() }
+ { this.renderMetaMetricsOptIn() }
+ </div>
+ )
+ }
+
+ render () {
+ return this.renderContent()
+ }
+}
diff --git a/ui/app/pages/settings/security-tab/security-tab.container.js b/ui/app/pages/settings/security-tab/security-tab.container.js
new file mode 100644
index 000000000..6036f4eda
--- /dev/null
+++ b/ui/app/pages/settings/security-tab/security-tab.container.js
@@ -0,0 +1,42 @@
+import SecurityTab from './security-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ displayWarning,
+ revealSeedConfirmation,
+ setFeatureFlag,
+ showModal,
+ setParticipateInMetaMetrics,
+} from '../../../store/actions'
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask } = state
+ const {
+ featureFlags: {
+ privacyMode,
+ } = {},
+ participateInMetaMetrics,
+ } = metamask
+
+ return {
+ warning,
+ privacyMode,
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ revealSeedConfirmation: () => dispatch(revealSeedConfirmation()),
+ setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)),
+ showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })),
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SecurityTab)
diff --git a/ui/app/components/pages/settings/settings-tab/index.js b/ui/app/pages/settings/settings-tab/index.js
index 9fdaafd3f..9fdaafd3f 100644
--- a/ui/app/components/pages/settings/settings-tab/index.js
+++ b/ui/app/pages/settings/settings-tab/index.js
diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/pages/settings/settings-tab/index.scss
index ef32b0e4c..ef32b0e4c 100644
--- a/ui/app/components/pages/settings/settings-tab/index.scss
+++ b/ui/app/pages/settings/settings-tab/index.scss
diff --git a/ui/app/pages/settings/settings-tab/settings-tab.component.js b/ui/app/pages/settings/settings-tab/settings-tab.component.js
new file mode 100644
index 000000000..57e80be0d
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/settings-tab.component.js
@@ -0,0 +1,200 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import infuraCurrencies from '../../../helpers/constants/infura-conversion.json'
+import SimpleDropdown from '../../../components/app/dropdowns/simple-dropdown'
+import ToggleButton from 'react-toggle-button'
+import locales from '../../../../../app/_locales/index.json'
+
+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,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ setUseBlockie: PropTypes.func,
+ setCurrentCurrency: PropTypes.func,
+ displayWarning: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ updateCurrentLocale: PropTypes.func,
+ currentLocale: PropTypes.string,
+ useBlockie: PropTypes.bool,
+ currentCurrency: PropTypes.string,
+ conversionDate: PropTypes.number,
+ nativeCurrency: PropTypes.string,
+ useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
+ }
+
+ 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('currencyConversion') }</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>
+ )
+ }
+
+
+ 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>
+ )
+ }
+
+ renderUsePrimaryCurrencyOptions () {
+ const { t } = this.context
+ const {
+ nativeCurrency,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference,
+ useNativeCurrencyAsPrimaryCurrency,
+ } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('primaryCurrencySetting') }</span>
+ <div className="settings-page__content-description">
+ { t('primaryCurrencySettingDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <div className="settings-tab__radio-buttons">
+ <div className="settings-tab__radio-button">
+ <input
+ type="radio"
+ id="native-primary-currency"
+ onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)}
+ checked={Boolean(useNativeCurrencyAsPrimaryCurrency)}
+ />
+ <label
+ htmlFor="native-primary-currency"
+ className="settings-tab__radio-label"
+ >
+ { nativeCurrency }
+ </label>
+ </div>
+ <div className="settings-tab__radio-button">
+ <input
+ type="radio"
+ id="fiat-primary-currency"
+ onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)}
+ checked={!useNativeCurrencyAsPrimaryCurrency}
+ />
+ <label
+ htmlFor="fiat-primary-currency"
+ className="settings-tab__radio-label"
+ >
+ { t('fiat') }
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderContent () {
+ const { warning } = this.props
+
+ return (
+ <div className="settings-page__body">
+ { warning && <div className="settings-tab__error">{ warning }</div> }
+ { this.renderCurrentConversion() }
+ { this.renderUsePrimaryCurrencyOptions() }
+ { this.renderCurrentLocale() }
+ { this.renderBlockieOptIn() }
+ </div>
+ )
+ }
+
+ render () {
+ return this.renderContent()
+ }
+}
diff --git a/ui/app/pages/settings/settings-tab/settings-tab.container.js b/ui/app/pages/settings/settings-tab/settings-tab.container.js
new file mode 100644
index 000000000..d3d8457f0
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/settings-tab.container.js
@@ -0,0 +1,53 @@
+import SettingsTab from './settings-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ setCurrentCurrency,
+ displayWarning,
+ setUseBlockie,
+ updateCurrentLocale,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference,
+ setParticipateInMetaMetrics,
+} from '../../../store/actions'
+import { preferencesSelector } from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask } = state
+ const {
+ currentCurrency,
+ conversionDate,
+ nativeCurrency,
+ useBlockie,
+ currentLocale,
+ } = metamask
+ const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
+
+ return {
+ warning,
+ currentLocale,
+ currentCurrency,
+ conversionDate,
+ nativeCurrency,
+ useBlockie,
+ useNativeCurrencyAsPrimaryCurrency,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)),
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ setUseBlockie: value => dispatch(setUseBlockie(value)),
+ updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
+ setUseNativeCurrencyAsPrimaryCurrencyPreference: value => {
+ return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value))
+ },
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SettingsTab)
diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js
new file mode 100644
index 000000000..3d415c6b8
--- /dev/null
+++ b/ui/app/pages/settings/settings.component.js
@@ -0,0 +1,137 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route, matchPath } from 'react-router-dom'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+import TabBar from '../../components/app/tab-bar'
+import c from 'classnames'
+import SettingsTab from './settings-tab'
+import AdvancedTab from './advanced-tab'
+import InfoTab from './info-tab'
+import SecurityTab from './security-tab'
+import {
+ DEFAULT_ROUTE,
+ ADVANCED_ROUTE,
+ SECURITY_ROUTE,
+ GENERAL_ROUTE,
+ ABOUT_US_ROUTE,
+ SETTINGS_ROUTE,
+} from '../../helpers/constants/routes'
+
+const ROUTES_TO_I18N_KEYS = {
+ [GENERAL_ROUTE]: 'general',
+ [ADVANCED_ROUTE]: 'advanced',
+ [SECURITY_ROUTE]: 'securityAndPrivacy',
+ [ABOUT_US_ROUTE]: 'aboutUs',
+}
+
+export default class SettingsPage extends PureComponent {
+ static propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ isCurrentPath (pathname) {
+ return this.props.location.pathname === pathname
+ }
+
+ render () {
+ const { t } = this.context
+ const { history, location } = this.props
+
+ const pathnameI18nKey = ROUTES_TO_I18N_KEYS[location.pathname]
+ const isPopupView = getEnvironmentType(location.href) === ENVIRONMENT_TYPE_POPUP
+
+ return (
+ <div
+ className={c('main-container settings-page', {
+ 'settings-page--selected': !this.isCurrentPath(SETTINGS_ROUTE),
+ })}
+ >
+ <div className="settings-page__header">
+ {
+ !this.isCurrentPath(SETTINGS_ROUTE) && (
+ <div
+ className="settings-page__back-button"
+ onClick={() => history.push(SETTINGS_ROUTE)}
+ />
+ )
+ }
+ <div className="settings-page__header__title">
+ {t(pathnameI18nKey && isPopupView ? pathnameI18nKey : 'settings')}
+ </div>
+ <div
+ className="settings-page__close-button"
+ onClick={() => history.push(DEFAULT_ROUTE)}
+ />
+ </div>
+ <div className="settings-page__content">
+ <div className="settings-page__content__tabs">
+ { this.renderTabs() }
+ </div>
+ <div className="settings-page__content__modules">
+ { this.renderContent() }
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderTabs () {
+ const { history, location } = this.props
+ const { t } = this.context
+
+ return (
+ <TabBar
+ tabs={[
+ { content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE },
+ { content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE },
+ { content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE },
+ { content: t('aboutUs'), key: ABOUT_US_ROUTE },
+ ]}
+ isActive={key => {
+ if (key === GENERAL_ROUTE && this.isCurrentPath(SETTINGS_ROUTE)) {
+ return true
+ }
+ return matchPath(location.pathname, { path: key, exact: true })
+ }}
+ onSelect={key => history.push(key)}
+ />
+ )
+ }
+
+ renderContent () {
+ return (
+ <Switch>
+ <Route
+ exact
+ path={GENERAL_ROUTE}
+ component={SettingsTab}
+ />
+ <Route
+ exact
+ path={ABOUT_US_ROUTE}
+ component={InfoTab}
+ />
+ <Route
+ exact
+ path={ADVANCED_ROUTE}
+ component={AdvancedTab}
+ />
+ <Route
+ exact
+ path={SECURITY_ROUTE}
+ component={SecurityTab}
+ />
+ <Route
+ component={SettingsTab}
+ />
+ </Switch>
+ )
+ }
+}
diff --git a/ui/app/components/pages/unlock-page/index.js b/ui/app/pages/unlock-page/index.js
index be80cde4f..be80cde4f 100644
--- a/ui/app/components/pages/unlock-page/index.js
+++ b/ui/app/pages/unlock-page/index.js
diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/pages/unlock-page/index.scss
index 3d44bd037..3d44bd037 100644
--- a/ui/app/components/pages/unlock-page/index.scss
+++ b/ui/app/pages/unlock-page/index.scss
diff --git a/ui/app/pages/unlock-page/unlock-page.component.js b/ui/app/pages/unlock-page/unlock-page.component.js
new file mode 100644
index 000000000..3aeb2a59b
--- /dev/null
+++ b/ui/app/pages/unlock-page/unlock-page.component.js
@@ -0,0 +1,191 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '@material-ui/core/Button'
+import TextField from '../../components/ui/text-field'
+import getCaretCoordinates from 'textarea-caret'
+import { EventEmitter } from 'events'
+import Mascot from '../../components/ui/mascot'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+
+export default class UnlockPage extends Component {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ onImport: PropTypes.func,
+ onRestore: PropTypes.func,
+ onSubmit: PropTypes.func,
+ forceUpdateMetamaskState: PropTypes.func,
+ showOptInModal: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ password: '',
+ error: null,
+ }
+
+ this.submitting = false
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentWillMount () {
+ const { isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ handleSubmit = async event => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const { password } = this.state
+ const { onSubmit, forceUpdateMetamaskState, showOptInModal } = this.props
+
+ if (password === '' || this.submitting) {
+ return
+ }
+
+ this.setState({ error: null })
+ this.submitting = true
+
+ try {
+ await onSubmit(password)
+ const newState = await forceUpdateMetamaskState()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Unlock',
+ name: 'Success',
+ },
+ isNewVisit: true,
+ })
+
+ if (newState.participateInMetaMetrics === null || newState.participateInMetaMetrics === undefined) {
+ showOptInModal()
+ }
+ } catch ({ message }) {
+ if (message === 'Incorrect password') {
+ const newState = await forceUpdateMetamaskState()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Unlock',
+ name: 'Incorrect Passowrd',
+ },
+ customVariables: {
+ numberOfTokens: newState.tokens.length,
+ numberOfAccounts: Object.keys(newState.accounts).length,
+ },
+ })
+ }
+
+ this.setState({ error: message })
+ this.submitting = false
+ }
+ }
+
+ handleInputChange ({ target }) {
+ this.setState({ password: target.value, error: null })
+
+ // tell mascot to look at page action
+ const element = target
+ const boundingRect = element.getBoundingClientRect()
+ const coordinates = getCaretCoordinates(element, element.selectionEnd)
+ this.animationEventEmitter.emit('point', {
+ x: boundingRect.left + coordinates.left - element.scrollLeft,
+ y: boundingRect.top + coordinates.top - element.scrollTop,
+ })
+ }
+
+ renderSubmitButton () {
+ const style = {
+ backgroundColor: '#f7861c',
+ color: 'white',
+ marginTop: '20px',
+ height: '60px',
+ fontWeight: '400',
+ boxShadow: 'none',
+ borderRadius: '4px',
+ }
+
+ return (
+ <Button
+ type="submit"
+ style={style}
+ disabled={!this.state.password}
+ fullWidth
+ variant="raised"
+ size="large"
+ onClick={this.handleSubmit}
+ disableRipple
+ >
+ { this.context.t('login') }
+ </Button>
+ )
+ }
+
+ render () {
+ const { password, error } = this.state
+ const { t } = this.context
+ const { onImport, onRestore } = this.props
+
+ return (
+ <div className="unlock-page__container">
+ <div className="unlock-page">
+ <div className="unlock-page__mascot-container">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="120"
+ height="120"
+ />
+ </div>
+ <h1 className="unlock-page__title">
+ { t('welcomeBack') }
+ </h1>
+ <div>{ t('unlockMessage') }</div>
+ <form
+ className="unlock-page__form"
+ onSubmit={this.handleSubmit}
+ >
+ <TextField
+ id="password"
+ label={t('password')}
+ type="password"
+ value={password}
+ onChange={event => this.handleInputChange(event)}
+ error={error}
+ autoFocus
+ autoComplete="current-password"
+ material
+ fullWidth
+ />
+ </form>
+ { this.renderSubmitButton() }
+ <div className="unlock-page__links">
+ <div
+ className="unlock-page__link"
+ onClick={() => onRestore()}
+ >
+ { t('restoreFromSeed') }
+ </div>
+ <div
+ className="unlock-page__link unlock-page__link--import"
+ onClick={() => onImport()}
+ >
+ { t('importUsingSeed') }
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/unlock-page/unlock-page.container.js b/ui/app/pages/unlock-page/unlock-page.container.js
new file mode 100644
index 000000000..b89392ab5
--- /dev/null
+++ b/ui/app/pages/unlock-page/unlock-page.container.js
@@ -0,0 +1,64 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
+import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../helpers/constants/routes'
+import {
+ tryUnlockMetamask,
+ forgotPassword,
+ markPasswordForgotten,
+ forceUpdateMetamaskState,
+ showModal,
+} from '../../store/actions'
+import UnlockPage from './unlock-page.component'
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ forgotPassword: () => dispatch(forgotPassword()),
+ tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
+ markPasswordForgotten: () => dispatch(markPasswordForgotten()),
+ forceUpdateMetamaskState: () => forceUpdateMetamaskState(dispatch),
+ showOptInModal: () => dispatch(showModal({ name: 'METAMETRICS_OPT_IN_MODAL' })),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { markPasswordForgotten, tryUnlockMetamask, ...restDispatchProps } = dispatchProps
+ const { history, onSubmit: ownPropsSubmit, ...restOwnProps } = ownProps
+
+ const onImport = () => {
+ markPasswordForgotten()
+ history.push(RESTORE_VAULT_ROUTE)
+
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(RESTORE_VAULT_ROUTE)
+ }
+ }
+
+ const onSubmit = async password => {
+ await tryUnlockMetamask(password)
+ history.push(DEFAULT_ROUTE)
+ }
+
+ return {
+ ...stateProps,
+ ...restDispatchProps,
+ ...restOwnProps,
+ onImport,
+ onRestore: onImport,
+ onSubmit: ownPropsSubmit || onSubmit,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(UnlockPage)
diff --git a/ui/app/reducers.js b/ui/app/reducers.js
deleted file mode 100644
index 786af853d..000000000
--- a/ui/app/reducers.js
+++ /dev/null
@@ -1,95 +0,0 @@
-const clone = require('clone')
-const extend = require('xtend')
-const copyToClipboard = require('copy-to-clipboard')
-
-//
-// Sub-Reducers take in the complete state and return their sub-state
-//
-const reduceMetamask = require('./reducers/metamask')
-const reduceApp = require('./reducers/app')
-const reduceLocale = require('./reducers/locale')
-const reduceSend = require('./ducks/send.duck').default
-import reduceConfirmTransaction from './ducks/confirm-transaction.duck'
-import reduceGas from './ducks/gas.duck'
-
-window.METAMASK_CACHED_LOG_STATE = null
-
-module.exports = rootReducer
-
-function rootReducer (state, action) {
- // clone
- state = extend(state)
-
- if (action.type === 'GLOBAL_FORCE_UPDATE') {
- return action.value
- }
-
- //
- // MetaMask
- //
-
- state.metamask = reduceMetamask(state, action)
-
- //
- // AppState
- //
-
- state.appState = reduceApp(state, action)
-
- //
- // LocaleMessages
- //
-
- state.localeMessages = reduceLocale(state, action)
-
- //
- // Send
- //
-
- state.send = reduceSend(state, action)
-
- state.confirmTransaction = reduceConfirmTransaction(state, action)
-
- state.gas = reduceGas(state, action)
-
- window.METAMASK_CACHED_LOG_STATE = state
- return state
-}
-
-window.getCleanAppState = function () {
- const state = clone(window.METAMASK_CACHED_LOG_STATE)
- // append additional information
- state.version = global.platform.getVersion()
- state.browser = window.navigator.userAgent
- // ensure seedWords are not included
- if (state.metamask) delete state.metamask.seedWords
- if (state.appState.currentView) delete state.appState.currentView.seedWords
- return state
-}
-
-window.logStateString = function (cb) {
- const state = window.getCleanAppState()
- global.platform.getPlatformInfo((err, platform) => {
- if (err) return cb(err)
- state.platform = platform
- const stateString = JSON.stringify(state, removeSeedWords, 2)
- cb(null, stateString)
- })
-}
-
-window.logState = function (toClipboard) {
- return window.logStateString((err, result) => {
- if (err) {
- console.error(err.message)
- } else if (toClipboard) {
- copyToClipboard(result)
- console.log('State log copied')
- } else {
- console.log(result)
- }
- })
-}
-
-function removeSeedWords (key, value) {
- return key === 'seedWords' ? undefined : value
-}
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
deleted file mode 100644
index 22cfe7f8d..000000000
--- a/ui/app/reducers/app.js
+++ /dev/null
@@ -1,788 +0,0 @@
-const extend = require('xtend')
-const actions = require('../actions')
-const txHelper = require('../../lib/tx-helper')
-const log = require('loglevel')
-
-module.exports = reduceApp
-
-
-function reduceApp (state, action) {
- log.debug('App Reducer got ' + action.type)
- // clone and defaults
- const selectedAddress = state.metamask.selectedAddress
- const hasUnconfActions = checkUnconfActions(state)
- let name = 'accounts'
- if (selectedAddress) {
- name = 'accountDetail'
- }
-
- if (hasUnconfActions) {
- log.debug('pending txs detected, defaulting to conf-tx view.')
- name = 'confTx'
- }
-
- var defaultView = {
- name,
- detailView: null,
- context: selectedAddress,
- }
-
- // confirm seed words
- var seedWords = state.metamask.seedWords
- var seedConfView = {
- name: 'createVaultComplete',
- seedWords,
- }
-
- // default state
- var appState = extend({
- shouldClose: false,
- menuOpen: false,
- modal: {
- open: false,
- modalState: {
- name: null,
- props: {},
- },
- previousModalState: {
- name: null,
- },
- },
- sidebar: {
- isOpen: false,
- transitionName: '',
- type: '',
- props: {},
- },
- alertOpen: false,
- alertMessage: null,
- qrCodeData: null,
- networkDropdownOpen: false,
- currentView: seedWords ? seedConfView : defaultView,
- accountDetail: {
- subview: 'transactions',
- },
- // Used to render transition direction
- transForward: true,
- // Used to display loading indicator
- isLoading: false,
- // Used to display error text
- warning: null,
- buyView: {},
- isMouseUser: false,
- gasIsLoading: false,
- networkNonce: null,
- defaultHdPaths: {
- trezor: `m/44'/60'/0'/0`,
- ledger: `m/44'/60'/0'/0/0`,
- },
- lastSelectedProvider: null,
- }, state.appState)
-
- switch (action.type) {
- // dropdown methods
- case actions.NETWORK_DROPDOWN_OPEN:
- return extend(appState, {
- networkDropdownOpen: true,
- })
-
- case actions.NETWORK_DROPDOWN_CLOSE:
- return extend(appState, {
- networkDropdownOpen: false,
- })
-
- // sidebar methods
- case actions.SIDEBAR_OPEN:
- return extend(appState, {
- sidebar: {
- ...action.value,
- isOpen: true,
- },
- })
-
- case actions.SIDEBAR_CLOSE:
- return extend(appState, {
- sidebar: {
- ...appState.sidebar,
- isOpen: false,
- },
- })
-
- // alert methods
- case actions.ALERT_OPEN:
- return extend(appState, {
- alertOpen: true,
- alertMessage: action.value,
- })
-
- case actions.ALERT_CLOSE:
- return extend(appState, {
- alertOpen: false,
- alertMessage: null,
- })
-
- // qr scanner methods
- case actions.QR_CODE_DETECTED:
- return extend(appState, {
- qrCodeData: action.value,
- })
-
-
- // modal methods:
- case actions.MODAL_OPEN:
- const { name, ...modalProps } = action.payload
-
- return extend(appState, {
- modal: {
- open: true,
- modalState: {
- name: name,
- props: { ...modalProps },
- },
- previousModalState: { ...appState.modal.modalState },
- },
- })
-
- case actions.MODAL_CLOSE:
- return extend(appState, {
- modal: Object.assign(
- state.appState.modal,
- { open: false },
- { modalState: { name: null, props: {} } },
- { previousModalState: appState.modal.modalState},
- ),
- })
-
- // transition methods
- case actions.TRANSITION_FORWARD:
- return extend(appState, {
- transForward: true,
- })
-
- case actions.TRANSITION_BACKWARD:
- return extend(appState, {
- transForward: false,
- })
-
- // intialize
-
- case actions.SHOW_CREATE_VAULT:
- return extend(appState, {
- currentView: {
- name: 'createVault',
- },
- transForward: true,
- warning: null,
- })
-
- case actions.SHOW_RESTORE_VAULT:
- return extend(appState, {
- currentView: {
- name: 'restoreVault',
- },
- transForward: true,
- forgottenPassword: true,
- })
-
- case actions.FORGOT_PASSWORD:
- const newState = extend(appState, {
- forgottenPassword: action.value,
- })
-
- if (action.value) {
- newState.currentView = {
- name: 'restoreVault',
- }
- }
-
- return newState
-
- case actions.SHOW_INIT_MENU:
- return extend(appState, {
- currentView: defaultView,
- transForward: false,
- })
-
- case actions.SHOW_CONFIG_PAGE:
- return extend(appState, {
- currentView: {
- name: 'config',
- context: appState.currentView.context,
- },
- transForward: action.value,
- })
-
- case actions.SHOW_ADD_TOKEN_PAGE:
- return extend(appState, {
- currentView: {
- name: 'add-token',
- context: appState.currentView.context,
- },
- 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: {
- name: 'import-menu',
- },
- transForward: true,
- warning: null,
- })
-
- case actions.SHOW_NEW_ACCOUNT_PAGE:
- return extend(appState, {
- currentView: {
- name: 'new-account-page',
- context: action.formToSelect,
- },
- transForward: true,
- warning: null,
- })
-
- case actions.SET_NEW_ACCOUNT_FORM:
- return extend(appState, {
- currentView: {
- name: appState.currentView.name,
- context: action.formToSelect,
- },
- })
-
- case actions.SHOW_INFO_PAGE:
- return extend(appState, {
- currentView: {
- name: 'info',
- context: appState.currentView.context,
- },
- transForward: true,
- })
-
- case actions.CREATE_NEW_VAULT_IN_PROGRESS:
- return extend(appState, {
- currentView: {
- name: 'createVault',
- inProgress: true,
- },
- transForward: true,
- isLoading: true,
- })
-
- case actions.SHOW_NEW_VAULT_SEED:
- return extend(appState, {
- currentView: {
- name: 'createVaultComplete',
- seedWords: action.value,
- },
- transForward: true,
- isLoading: false,
- })
-
- case actions.NEW_ACCOUNT_SCREEN:
- return extend(appState, {
- currentView: {
- name: 'new-account',
- context: appState.currentView.context,
- },
- transForward: true,
- })
-
- case actions.SHOW_SEND_PAGE:
- return extend(appState, {
- currentView: {
- name: 'sendTransaction',
- context: appState.currentView.context,
- },
- transForward: true,
- warning: null,
- })
-
- case actions.SHOW_SEND_TOKEN_PAGE:
- return extend(appState, {
- currentView: {
- name: 'sendToken',
- context: appState.currentView.context,
- },
- transForward: true,
- warning: null,
- })
-
- case actions.SHOW_NEW_KEYCHAIN:
- return extend(appState, {
- currentView: {
- name: 'newKeychain',
- context: appState.currentView.context,
- },
- transForward: true,
- })
-
- // unlock
-
- case actions.UNLOCK_METAMASK:
- return extend(appState, {
- forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
- detailView: {},
- transForward: true,
- isLoading: false,
- warning: null,
- })
-
- case actions.LOCK_METAMASK:
- return extend(appState, {
- currentView: defaultView,
- transForward: false,
- warning: null,
- })
-
- case actions.BACK_TO_INIT_MENU:
- return extend(appState, {
- warning: null,
- transForward: false,
- forgottenPassword: true,
- currentView: {
- name: 'InitMenu',
- },
- })
-
- case actions.BACK_TO_UNLOCK_VIEW:
- return extend(appState, {
- warning: null,
- transForward: true,
- forgottenPassword: false,
- currentView: {
- name: 'UnlockScreen',
- },
- })
- // reveal seed words
-
- case actions.REVEAL_SEED_CONFIRMATION:
- return extend(appState, {
- currentView: {
- name: 'reveal-seed-conf',
- },
- transForward: true,
- warning: null,
- })
-
- // accounts
-
- case actions.SET_SELECTED_ACCOUNT:
- return extend(appState, {
- activeAddress: action.value,
- })
-
- case actions.GO_HOME:
- return extend(appState, {
- currentView: extend(appState.currentView, {
- name: 'accountDetail',
- }),
- accountDetail: {
- subview: 'transactions',
- accountExport: 'none',
- privateKey: '',
- },
- transForward: false,
- warning: null,
- })
-
- case actions.SHOW_ACCOUNT_DETAIL:
- return extend(appState, {
- forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
- currentView: {
- name: 'accountDetail',
- context: action.value,
- },
- accountDetail: {
- subview: 'transactions',
- accountExport: 'none',
- privateKey: '',
- },
- transForward: false,
- })
-
- case actions.BACK_TO_ACCOUNT_DETAIL:
- return extend(appState, {
- currentView: {
- name: 'accountDetail',
- context: action.value,
- },
- accountDetail: {
- subview: 'transactions',
- accountExport: 'none',
- privateKey: '',
- },
- transForward: false,
- })
-
- case actions.SHOW_ACCOUNTS_PAGE:
- return extend(appState, {
- currentView: {
- name: seedWords ? 'createVaultComplete' : 'accounts',
- seedWords,
- },
- transForward: true,
- isLoading: false,
- warning: null,
- scrollToBottom: false,
- forgottenPassword: false,
- })
-
- case actions.SHOW_NOTICE:
- return extend(appState, {
- transForward: true,
- isLoading: false,
- })
-
- case actions.REVEAL_ACCOUNT:
- return extend(appState, {
- scrollToBottom: true,
- })
-
- case actions.SHOW_CONF_TX_PAGE:
- return extend(appState, {
- currentView: {
- name: 'confTx',
- context: action.id ? indexForPending(state, action.id) : 0,
- },
- transForward: action.transForward,
- warning: null,
- isLoading: false,
- })
-
- case actions.SHOW_CONF_MSG_PAGE:
- return extend(appState, {
- currentView: {
- name: hasUnconfActions ? 'confTx' : 'account-detail',
- context: 0,
- },
- transForward: true,
- warning: null,
- isLoading: false,
- })
-
- case actions.COMPLETED_TX:
- log.debug('reducing COMPLETED_TX for tx ' + action.value)
- const otherUnconfActions = getUnconfActionList(state)
- .filter(tx => tx.id !== action.value)
- const hasOtherUnconfActions = otherUnconfActions.length > 0
-
- if (hasOtherUnconfActions) {
- log.debug('reducer detected txs - rendering confTx view')
- return extend(appState, {
- transForward: false,
- currentView: {
- name: 'confTx',
- context: 0,
- },
- warning: null,
- })
- } else {
- log.debug('attempting to close popup')
- return extend(appState, {
- // indicate notification should close
- shouldClose: true,
- transForward: false,
- warning: null,
- currentView: {
- name: 'accountDetail',
- context: state.metamask.selectedAddress,
- },
- accountDetail: {
- subview: 'transactions',
- },
- })
- }
-
- case actions.NEXT_TX:
- return extend(appState, {
- transForward: true,
- currentView: {
- name: 'confTx',
- context: ++appState.currentView.context,
- warning: null,
- },
- })
-
- case actions.VIEW_PENDING_TX:
- const context = indexForPending(state, action.value)
- return extend(appState, {
- transForward: true,
- currentView: {
- name: 'confTx',
- context,
- warning: null,
- },
- })
-
- case actions.PREVIOUS_TX:
- return extend(appState, {
- transForward: false,
- currentView: {
- name: 'confTx',
- context: --appState.currentView.context,
- warning: null,
- },
- })
-
- case actions.TRANSACTION_ERROR:
- return extend(appState, {
- currentView: {
- name: 'confTx',
- errorMessage: 'There was a problem submitting this transaction.',
- },
- })
-
- case actions.UNLOCK_FAILED:
- return extend(appState, {
- warning: action.value || 'Incorrect password. Try again.',
- })
-
- case actions.UNLOCK_SUCCEEDED:
- return extend(appState, {
- warning: '',
- })
-
- case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH:
- const { device, path } = action.value
- const newDefaults = {...appState.defaultHdPaths}
- newDefaults[device] = path
-
- return extend(appState, {
- defaultHdPaths: newDefaults,
- })
-
- case actions.SHOW_LOADING:
- return extend(appState, {
- isLoading: true,
- loadingMessage: action.value,
- })
-
- case actions.HIDE_LOADING:
- return extend(appState, {
- isLoading: false,
- })
-
- case actions.SHOW_SUB_LOADING_INDICATION:
- return extend(appState, {
- isSubLoading: true,
- })
-
- case actions.HIDE_SUB_LOADING_INDICATION:
- return extend(appState, {
- isSubLoading: false,
- })
- case actions.CLEAR_SEED_WORD_CACHE:
- return extend(appState, {
- transForward: true,
- currentView: {},
- isLoading: false,
- accountDetail: {
- subview: 'transactions',
- accountExport: 'none',
- privateKey: '',
- },
- })
-
- case actions.DISPLAY_WARNING:
- return extend(appState, {
- warning: action.value,
- isLoading: false,
- })
-
- case actions.HIDE_WARNING:
- return extend(appState, {
- warning: undefined,
- })
-
- case actions.REQUEST_ACCOUNT_EXPORT:
- return extend(appState, {
- transForward: true,
- currentView: {
- name: 'accountDetail',
- context: appState.currentView.context,
- },
- accountDetail: {
- subview: 'export',
- accountExport: 'requested',
- },
- })
-
- case actions.EXPORT_ACCOUNT:
- return extend(appState, {
- accountDetail: {
- subview: 'export',
- accountExport: 'completed',
- },
- })
-
- case actions.SHOW_PRIVATE_KEY:
- return extend(appState, {
- accountDetail: {
- subview: 'export',
- accountExport: 'completed',
- privateKey: action.value,
- },
- })
-
- case actions.BUY_ETH_VIEW:
- return extend(appState, {
- transForward: true,
- currentView: {
- name: 'buyEth',
- context: appState.currentView.name,
- },
- identity: state.metamask.identities[action.value],
- buyView: {
- subview: 'Coinbase',
- amount: '15.00',
- buyAddress: action.value,
- formView: {
- coinbase: true,
- shapeshift: false,
- },
- },
- })
-
- case actions.ONBOARDING_BUY_ETH_VIEW:
- return extend(appState, {
- transForward: true,
- currentView: {
- name: 'onboardingBuyEth',
- context: appState.currentView.name,
- },
- identity: state.metamask.identities[action.value],
- })
-
- case actions.COINBASE_SUBVIEW:
- return extend(appState, {
- buyView: {
- subview: 'Coinbase',
- formView: {
- coinbase: true,
- shapeshift: false,
- },
- buyAddress: appState.buyView.buyAddress,
- amount: appState.buyView.amount,
- },
- })
-
- case actions.SHAPESHIFT_SUBVIEW:
- return extend(appState, {
- buyView: {
- subview: 'ShapeShift',
- formView: {
- coinbase: false,
- shapeshift: true,
- marketinfo: action.value.marketinfo,
- coinOptions: action.value.coinOptions,
- },
- buyAddress: action.value.buyAddress || appState.buyView.buyAddress,
- amount: appState.buyView.amount || 0,
- },
- })
-
- case actions.PAIR_UPDATE:
- return extend(appState, {
- buyView: {
- subview: 'ShapeShift',
- formView: {
- coinbase: false,
- shapeshift: true,
- marketinfo: action.value.marketinfo,
- coinOptions: appState.buyView.formView.coinOptions,
- },
- buyAddress: appState.buyView.buyAddress,
- amount: appState.buyView.amount,
- warning: null,
- },
- })
-
- case actions.SHOW_QR:
- return extend(appState, {
- qrRequested: true,
- transForward: true,
-
- Qr: {
- message: action.value.message,
- data: action.value.data,
- },
- })
-
- case actions.SHOW_QR_VIEW:
- return extend(appState, {
- currentView: {
- name: 'qr',
- context: appState.currentView.context,
- },
- transForward: true,
- Qr: {
- message: action.value.message,
- data: action.value.data,
- },
- })
-
- case actions.SET_MOUSE_USER_STATE:
- return extend(appState, {
- isMouseUser: action.value,
- })
-
- case actions.GAS_LOADING_STARTED:
- return extend(appState, {
- gasIsLoading: true,
- })
-
- case actions.GAS_LOADING_FINISHED:
- return extend(appState, {
- gasIsLoading: false,
- })
-
- case actions.SET_NETWORK_NONCE:
- return extend(appState, {
- networkNonce: action.value,
- })
-
- case actions.SET_PREVIOUS_PROVIDER:
- if (action.value === 'loading') {
- return appState
- }
- return extend(appState, {
- lastSelectedProvider: action.value,
- })
-
- default:
- return appState
- }
-}
-
-function checkUnconfActions (state) {
- const unconfActionList = getUnconfActionList(state)
- const hasUnconfActions = unconfActionList.length > 0
- return hasUnconfActions
-}
-
-function getUnconfActionList (state) {
- const { unapprovedTxs, unapprovedMsgs,
- unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask
-
- const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
- return unconfActionList
-}
-
-function indexForPending (state, txId) {
- const unconfTxList = getUnconfActionList(state)
- const match = unconfTxList.find((tx) => tx.id === txId)
- const index = unconfTxList.indexOf(match)
- return index
-}
-
-// function indexForLastPending (state) {
-// return getUnconfActionList(state).length
-// }
diff --git a/ui/app/reducers/locale.js b/ui/app/reducers/locale.js
deleted file mode 100644
index bdd97acb4..000000000
--- a/ui/app/reducers/locale.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const extend = require('xtend')
-const actions = require('../actions')
-
-module.exports = reduceMetamask
-
-function reduceMetamask (state, action) {
- const localeMessagesState = extend({}, state.localeMessages)
-
- switch (action.type) {
- case actions.SET_LOCALE_MESSAGES:
- return extend(localeMessagesState, {
- current: action.value,
- })
- default:
- return localeMessagesState
- }
-}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
deleted file mode 100644
index d4b920748..000000000
--- a/ui/app/reducers/metamask.js
+++ /dev/null
@@ -1,419 +0,0 @@
-const extend = require('xtend')
-const actions = require('../actions')
-const { getEnvironmentType } = require('../../../app/scripts/lib/util')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums')
-const { OLD_UI_NETWORK_TYPE } = require('../../../app/scripts/controllers/network/enums')
-
-module.exports = reduceMetamask
-
-function reduceMetamask (state, action) {
- let newState
-
- // clone + defaults
- var metamaskState = extend({
- isInitialized: false,
- isUnlocked: false,
- isAccountMenuOpen: false,
- isPopup: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP,
- rpcTarget: 'https://rawtestrpc.metamask.io/',
- identities: {},
- unapprovedTxs: {},
- noActiveNotices: true,
- nextUnreadNotice: undefined,
- frequentRpcList: [],
- addressBook: [],
- selectedTokenAddress: null,
- contractExchangeRates: {},
- tokenExchangeRates: {},
- tokens: [],
- pendingTokens: {},
- send: {
- gasLimit: null,
- gasPrice: null,
- gasTotal: null,
- tokenBalance: '0x0',
- from: '',
- to: '',
- amount: '0',
- memo: '',
- errors: {},
- maxModeOn: false,
- editingTransactionId: null,
- forceGasMin: null,
- toNickname: '',
- },
- coinOptions: {},
- useBlockie: false,
- featureFlags: {},
- networkEndpointType: OLD_UI_NETWORK_TYPE,
- isRevealingSeedWords: false,
- welcomeScreenSeen: false,
- currentLocale: '',
- preferences: {
- useNativeCurrencyAsPrimaryCurrency: true,
- showFiatInTestnets: false,
- },
- firstTimeFlowType: null,
- completedOnboarding: false,
- knownMethodData: {},
- participateInMetaMetrics: null,
- metaMetricsSendCount: 0,
- }, state.metamask)
-
- switch (action.type) {
-
- case actions.SHOW_ACCOUNTS_PAGE:
- newState = extend(metamaskState, {
- isRevealingSeedWords: false,
- })
- delete newState.seedWords
- return newState
-
- case actions.SHOW_NOTICE:
- return extend(metamaskState, {
- noActiveNotices: false,
- nextUnreadNotice: action.value,
- })
-
- case actions.CLEAR_NOTICES:
- return extend(metamaskState, {
- noActiveNotices: true,
- nextUnreadNotice: undefined,
- })
-
- case actions.UPDATE_METAMASK_STATE:
- return extend(metamaskState, action.value)
-
- case actions.UNLOCK_METAMASK:
- return extend(metamaskState, {
- isUnlocked: true,
- isInitialized: true,
- selectedAddress: action.value,
- })
-
- case actions.LOCK_METAMASK:
- return extend(metamaskState, {
- isUnlocked: false,
- })
-
- case actions.SET_RPC_LIST:
- return extend(metamaskState, {
- frequentRpcList: action.value,
- })
-
- case actions.SET_RPC_TARGET:
- return extend(metamaskState, {
- provider: {
- type: 'rpc',
- rpcTarget: action.value,
- },
- })
-
- case actions.SET_PROVIDER_TYPE:
- return extend(metamaskState, {
- provider: {
- type: action.value,
- },
- })
-
- case actions.COMPLETED_TX:
- var stringId = String(action.id)
- newState = extend(metamaskState, {
- unapprovedTxs: {},
- unapprovedMsgs: {},
- })
- for (const id in metamaskState.unapprovedTxs) {
- if (id !== stringId) {
- newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id]
- }
- }
- for (const id in metamaskState.unapprovedMsgs) {
- if (id !== stringId) {
- newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id]
- }
- }
- return newState
-
- case actions.EDIT_TX:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- editingTransactionId: action.value,
- },
- })
-
-
- case actions.SHOW_NEW_VAULT_SEED:
- return extend(metamaskState, {
- isRevealingSeedWords: true,
- seedWords: action.value,
- })
-
- case actions.CLEAR_SEED_WORD_CACHE:
- newState = extend(metamaskState, {
- isUnlocked: true,
- isInitialized: true,
- selectedAddress: action.value,
- })
- delete newState.seedWords
- return newState
-
- case actions.SHOW_ACCOUNT_DETAIL:
- newState = extend(metamaskState, {
- isUnlocked: true,
- isInitialized: true,
- selectedAddress: action.value,
- })
- delete newState.seedWords
- return newState
-
- case actions.SET_SELECTED_TOKEN:
- return extend(metamaskState, {
- selectedTokenAddress: action.value,
- })
-
- case actions.SET_ACCOUNT_LABEL:
- const account = action.value.account
- const name = action.value.label
- const id = {}
- id[account] = extend(metamaskState.identities[account], { name })
- const identities = extend(metamaskState.identities, id)
- return extend(metamaskState, { identities })
-
- case actions.SET_CURRENT_FIAT:
- return extend(metamaskState, {
- currentCurrency: action.value.currentCurrency,
- conversionRate: action.value.conversionRate,
- conversionDate: action.value.conversionDate,
- })
-
- case actions.UPDATE_TOKENS:
- return extend(metamaskState, {
- tokens: action.newTokens,
- })
-
- // metamask.send
- case actions.UPDATE_GAS_LIMIT:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- gasLimit: action.value,
- },
- })
-
- case actions.UPDATE_GAS_PRICE:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- gasPrice: action.value,
- },
- })
-
- case actions.TOGGLE_ACCOUNT_MENU:
- return extend(metamaskState, {
- isAccountMenuOpen: !metamaskState.isAccountMenuOpen,
- })
-
- case actions.UPDATE_GAS_TOTAL:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- gasTotal: action.value,
- },
- })
-
- case actions.UPDATE_SEND_TOKEN_BALANCE:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- tokenBalance: action.value,
- },
- })
-
- case actions.UPDATE_SEND_HEX_DATA:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- data: action.value,
- },
- })
-
- case actions.UPDATE_SEND_FROM:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- from: action.value,
- },
- })
-
- case actions.UPDATE_SEND_TO:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- to: action.value.to,
- toNickname: action.value.nickname,
- },
- })
-
- case actions.UPDATE_SEND_AMOUNT:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- amount: action.value,
- },
- })
-
- case actions.UPDATE_SEND_MEMO:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- memo: action.value,
- },
- })
-
- case actions.UPDATE_MAX_MODE:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- maxModeOn: action.value,
- },
- })
-
- case actions.UPDATE_SEND:
- return extend(metamaskState, {
- send: {
- ...metamaskState.send,
- ...action.value,
- },
- })
-
- case actions.CLEAR_SEND:
- return extend(metamaskState, {
- send: {
- gasLimit: null,
- gasPrice: null,
- gasTotal: null,
- tokenBalance: null,
- from: '',
- to: '',
- amount: '0x0',
- memo: '',
- errors: {},
- maxModeOn: false,
- editingTransactionId: null,
- forceGasMin: null,
- toNickname: '',
- },
- })
-
- case actions.UPDATE_TRANSACTION_PARAMS:
- const { id: txId, value } = action
- let { selectedAddressTxList } = metamaskState
- selectedAddressTxList = selectedAddressTxList.map(tx => {
- if (tx.id === txId) {
- tx.txParams = value
- }
- return tx
- })
-
- return extend(metamaskState, {
- selectedAddressTxList,
- })
-
- case actions.PAIR_UPDATE:
- const { value: { marketinfo: pairMarketInfo } } = action
- return extend(metamaskState, {
- tokenExchangeRates: {
- ...metamaskState.tokenExchangeRates,
- [pairMarketInfo.pair]: pairMarketInfo,
- },
- })
-
- case actions.SHAPESHIFT_SUBVIEW:
- const { value: { marketinfo: ssMarketInfo, coinOptions } } = action
- return extend(metamaskState, {
- tokenExchangeRates: {
- ...metamaskState.tokenExchangeRates,
- [ssMarketInfo.pair]: ssMarketInfo,
- },
- coinOptions,
- })
-
- case actions.SET_PARTICIPATE_IN_METAMETRICS:
- return extend(metamaskState, {
- participateInMetaMetrics: action.value,
- })
-
- case actions.SET_METAMETRICS_SEND_COUNT:
- return extend(metamaskState, {
- metaMetricsSendCount: action.value,
- })
-
- case actions.SET_USE_BLOCKIE:
- return extend(metamaskState, {
- useBlockie: action.value,
- })
-
- case actions.UPDATE_FEATURE_FLAGS:
- return extend(metamaskState, {
- featureFlags: action.value,
- })
-
- case actions.UPDATE_NETWORK_ENDPOINT_TYPE:
- return extend(metamaskState, {
- networkEndpointType: action.value,
- })
-
- case actions.CLOSE_WELCOME_SCREEN:
- return extend(metamaskState, {
- welcomeScreenSeen: true,
- })
-
- case actions.SET_CURRENT_LOCALE:
- return extend(metamaskState, {
- currentLocale: action.value,
- })
-
- case actions.SET_PENDING_TOKENS:
- return extend(metamaskState, {
- pendingTokens: { ...action.payload },
- })
-
- case actions.CLEAR_PENDING_TOKENS: {
- return extend(metamaskState, {
- pendingTokens: {},
- })
- }
-
- case actions.UPDATE_PREFERENCES: {
- return extend(metamaskState, {
- preferences: {
- ...metamaskState.preferences,
- ...action.payload,
- },
- })
- }
-
- case actions.COMPLETE_ONBOARDING: {
- return extend(metamaskState, {
- completedOnboarding: true,
- })
- }
-
- case actions.COMPLETE_UI_MIGRATION: {
- return extend(metamaskState, {
- completedUiMigration: true,
- })
- }
-
- case actions.SET_FIRST_TIME_FLOW_TYPE: {
- return extend(metamaskState, {
- firstTimeFlowType: action.value,
- })
- }
-
- default:
- return metamaskState
-
- }
-}
diff --git a/ui/app/root.js b/ui/app/root.js
deleted file mode 100644
index c95c56581..000000000
--- a/ui/app/root.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const { Provider } = require('react-redux')
-const h = require('react-hyperscript')
-const { HashRouter } = require('react-router-dom')
-const App = require('./app')
-const I18nProvider = require('./i18n-provider')
-const MetaMetricsProvider = require('./metametrics/metametrics.provider')
-
-class Root extends Component {
- render () {
- const { store } = this.props
-
- return (
- h(Provider, { store }, [
- h(HashRouter, {
- hashType: 'noslash',
- }, [
- h(MetaMetricsProvider, [
- h(I18nProvider, [
- h(App),
- ]),
- ]),
- ]),
- ])
- )
- }
-}
-
-Root.propTypes = {
- store: PropTypes.object,
-}
-
-module.exports = Root
diff --git a/ui/app/routes.js b/ui/app/routes.js
deleted file mode 100644
index 932dfa7df..000000000
--- a/ui/app/routes.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const DEFAULT_ROUTE = '/'
-const UNLOCK_ROUTE = '/unlock'
-const LOCK_ROUTE = '/lock'
-const SETTINGS_ROUTE = '/settings'
-const INFO_ROUTE = '/settings/info'
-const REVEAL_SEED_ROUTE = '/seed'
-const MOBILE_SYNC_ROUTE = '/mobile-sync'
-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'
-const SEND_ROUTE = '/send'
-const NOTICE_ROUTE = '/notice'
-const WELCOME_ROUTE = '/welcome'
-
-const INITIALIZE_ROUTE = '/initialize'
-const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'
-const INITIALIZE_UNLOCK_ROUTE = '/initialize/unlock'
-const INITIALIZE_CREATE_PASSWORD_ROUTE = '/initialize/create-password'
-const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/create-password/import-account'
-const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/create-password/import-with-seed-phrase'
-const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/create-password/unique-image'
-const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
-const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action'
-const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase'
-const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow'
-const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm'
-const INITIALIZE_METAMETRICS_OPT_IN_ROUTE = '/initialize/metametrics-opt-in'
-
-const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
-const CONFIRM_SEND_ETHER_PATH = '/send-ether'
-const CONFIRM_SEND_TOKEN_PATH = '/send-token'
-const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'
-const CONFIRM_APPROVE_PATH = '/approve'
-const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
-const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
-const SIGNATURE_REQUEST_PATH = '/signature-request'
-
-module.exports = {
- DEFAULT_ROUTE,
- UNLOCK_ROUTE,
- LOCK_ROUTE,
- SETTINGS_ROUTE,
- INFO_ROUTE,
- REVEAL_SEED_ROUTE,
- MOBILE_SYNC_ROUTE,
- CONFIRM_SEED_ROUTE,
- RESTORE_VAULT_ROUTE,
- ADD_TOKEN_ROUTE,
- CONFIRM_ADD_TOKEN_ROUTE,
- CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
- NEW_ACCOUNT_ROUTE,
- IMPORT_ACCOUNT_ROUTE,
- CONNECT_HARDWARE_ROUTE,
- SEND_ROUTE,
- NOTICE_ROUTE,
- WELCOME_ROUTE,
- INITIALIZE_ROUTE,
- INITIALIZE_WELCOME_ROUTE,
- INITIALIZE_UNLOCK_ROUTE,
- INITIALIZE_CREATE_PASSWORD_ROUTE,
- INITIALIZE_IMPORT_ACCOUNT_ROUTE,
- INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
- INITIALIZE_UNIQUE_IMAGE_ROUTE,
- INITIALIZE_NOTICE_ROUTE,
- INITIALIZE_SELECT_ACTION_ROUTE,
- INITIALIZE_SEED_PHRASE_ROUTE,
- INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
- INITIALIZE_END_OF_FLOW_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- CONFIRM_SEND_ETHER_PATH,
- CONFIRM_SEND_TOKEN_PATH,
- CONFIRM_DEPLOY_CONTRACT_PATH,
- CONFIRM_APPROVE_PATH,
- CONFIRM_TRANSFER_FROM_PATH,
- CONFIRM_TOKEN_METHOD_PATH,
- SIGNATURE_REQUEST_PATH,
- INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
-}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
deleted file mode 100644
index 663c3f12b..000000000
--- a/ui/app/selectors.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import {NETWORK_TYPES} from './constants/common'
-import { stripHexPrefix } from 'ethereumjs-util'
-
-const abi = require('human-standard-token-abi')
-import {
- transactionsSelector,
-} from './selectors/transactions'
-const {
- multiplyCurrencies,
-} = require('./conversion-util')
-
-const selectors = {
- getSelectedAddress,
- getSelectedIdentity,
- getSelectedAccount,
- getSelectedToken,
- getSelectedTokenExchangeRate,
- getSelectedTokenAssetImage,
- getAssetImages,
- getTokenExchangeRate,
- conversionRateSelector,
- transactionsSelector,
- accountsWithSendEtherInfoSelector,
- getCurrentAccountWithSendEtherInfo,
- getGasIsLoading,
- getForceGasMin,
- getAddressBook,
- getSendFrom,
- getCurrentCurrency,
- getNativeCurrency,
- getSendAmount,
- getSelectedTokenToFiatRate,
- getSelectedTokenContract,
- getSendMaxModeState,
- getCurrentViewContext,
- getTotalUnapprovedCount,
- preferencesSelector,
- getMetaMaskAccounts,
- getCurrentEthBalance,
- getNetworkIdentifier,
- isBalanceCached,
- getAdvancedInlineGasShown,
- getIsMainnet,
- getCurrentNetworkId,
- getSelectedAsset,
- getCurrentKeyring,
- getAccountType,
- getNumberOfAccounts,
- getNumberOfTokens,
-}
-
-module.exports = selectors
-
-function getNetworkIdentifier (state) {
- const { metamask: { provider: { type, nickname, rpcTarget } } } = state
-
- return nickname || rpcTarget || type
-}
-
-function getCurrentKeyring (state) {
- const identity = getSelectedIdentity(state)
-
- if (!identity) {
- return null
- }
-
- const simpleAddress = stripHexPrefix(identity.address).toLowerCase()
-
- const keyring = state.metamask.keyrings.find((kr) => {
- return kr.accounts.includes(simpleAddress) ||
- kr.accounts.includes(identity.address)
- })
-
- return keyring
-}
-
-function getAccountType (state) {
- const currentKeyring = getCurrentKeyring(state)
- const type = currentKeyring && currentKeyring.type
-
- switch (type) {
- case 'Trezor Hardware':
- case 'Ledger Hardware':
- return 'hardware'
- case 'Simple Key Pair':
- return 'imported'
- default:
- return 'default'
- }
-}
-
-function getSelectedAsset (state) {
- return getSelectedToken(state) || 'ETH'
-}
-
-function getCurrentNetworkId (state) {
- return state.metamask.network
-}
-
-function getSelectedAddress (state) {
- const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
-
- return selectedAddress
-}
-
-function getSelectedIdentity (state) {
- const selectedAddress = getSelectedAddress(state)
- const identities = state.metamask.identities
-
- return identities[selectedAddress]
-}
-
-function getNumberOfAccounts (state) {
- return Object.keys(state.metamask.accounts).length
-}
-
-function getNumberOfTokens (state) {
- const tokens = state.metamask.tokens
- return tokens ? tokens.length : 0
-}
-
-function getMetaMaskAccounts (state) {
- const currentAccounts = state.metamask.accounts
- const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
- const selectedAccounts = {}
-
- Object.keys(currentAccounts).forEach(accountID => {
- const account = currentAccounts[accountID]
- if (account && account.balance === null || account.balance === undefined) {
- selectedAccounts[accountID] = {
- ...account,
- balance: cachedBalances && cachedBalances[accountID],
- }
- } else {
- selectedAccounts[accountID] = account
- }
- })
- return selectedAccounts
-}
-
-function isBalanceCached (state) {
- const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
- const cachedBalance = getSelectedAccountCachedBalance(state)
-
- return Boolean(!selectedAccountBalance && cachedBalance)
-}
-
-function getSelectedAccountCachedBalance (state) {
- const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
- const selectedAddress = getSelectedAddress(state)
-
- return cachedBalances && cachedBalances[selectedAddress]
-}
-
-function getSelectedAccount (state) {
- const accounts = getMetaMaskAccounts(state)
- const selectedAddress = getSelectedAddress(state)
-
- return accounts[selectedAddress]
-}
-
-function getSelectedToken (state) {
- const tokens = state.metamask.tokens || []
- const selectedTokenAddress = state.metamask.selectedTokenAddress
- const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
- const sendToken = state.metamask.send.token
-
- return selectedToken || sendToken || null
-}
-
-function getSelectedTokenExchangeRate (state) {
- const contractExchangeRates = state.metamask.contractExchangeRates
- const selectedToken = getSelectedToken(state) || {}
- const { address } = selectedToken
- 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
-}
-
-function conversionRateSelector (state) {
- return state.metamask.conversionRate
-}
-
-function getAddressBook (state) {
- return state.metamask.addressBook
-}
-
-function accountsWithSendEtherInfoSelector (state) {
- const accounts = getMetaMaskAccounts(state)
- const { identities } = state.metamask
-
- const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
- return Object.assign({}, account, identities[key])
- })
-
- return accountsWithSendEtherInfo
-}
-
-function getCurrentAccountWithSendEtherInfo (state) {
- const currentAddress = getSelectedAddress(state)
- const accounts = accountsWithSendEtherInfoSelector(state)
-
- return accounts.find(({ address }) => address === currentAddress)
-}
-
-function getCurrentEthBalance (state) {
- return getCurrentAccountWithSendEtherInfo(state).balance
-}
-
-function getGasIsLoading (state) {
- return state.appState.gasIsLoading
-}
-
-function getForceGasMin (state) {
- return state.metamask.send.forceGasMin
-}
-
-function getSendFrom (state) {
- return state.metamask.send.from
-}
-
-function getSendAmount (state) {
- return state.metamask.send.amount
-}
-
-function getSendMaxModeState (state) {
- return state.metamask.send.maxModeOn
-}
-
-function getCurrentCurrency (state) {
- return state.metamask.currentCurrency
-}
-
-function getNativeCurrency (state) {
- return state.metamask.nativeCurrency
-}
-
-function getSelectedTokenToFiatRate (state) {
- const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
- const conversionRate = conversionRateSelector(state)
-
- const tokenToFiatRate = multiplyCurrencies(
- conversionRate,
- selectedTokenExchangeRate,
- { toNumericBase: 'dec' }
- )
-
- return tokenToFiatRate
-}
-
-function getSelectedTokenContract (state) {
- const selectedToken = getSelectedToken(state)
- return selectedToken
- ? global.eth.contract(abi).at(selectedToken.address)
- : null
-}
-
-function getCurrentViewContext (state) {
- const { currentView = {} } = state.appState
- return currentView.context
-}
-
-function getTotalUnapprovedCount ({ metamask }) {
- const {
- unapprovedTxs = {},
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = metamask
-
- return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
- unapprovedTypedMessagesCount
-}
-
-function getIsMainnet (state) {
- const networkType = getNetworkIdentifier(state)
- return networkType === NETWORK_TYPES.MAINNET
-}
-
-function preferencesSelector ({ metamask }) {
- return metamask.preferences
-}
-
-function getAdvancedInlineGasShown (state) {
- return Boolean(state.metamask.featureFlags.advancedInlineGas)
-}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index ccd16fadd..9b5eda82f 100644
--- a/ui/app/selectors/confirm-transaction.js
+++ b/ui/app/selectors/confirm-transaction.js
@@ -1,7 +1,7 @@
import { createSelector } from 'reselect'
import txHelper from '../../lib/tx-helper'
-import { calcTokenAmount } from '../token-util'
-import { roundExponential } from '../helpers/confirm-transaction/util'
+import { calcTokenAmount } from '../helpers/utils/token-util'
+import { roundExponential } from '../helpers/utils/confirm-tx.util'
const unapprovedTxsSelector = state => state.metamask.unapprovedTxs
const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js
index 8039c0746..ecffb37ca 100644
--- a/ui/app/selectors/custom-gas.js
+++ b/ui/app/selectors/custom-gas.js
@@ -3,22 +3,22 @@ import {
conversionUtil,
multiplyCurrencies,
conversionGreaterThan,
-} from '../conversion-util'
+} from '../helpers/utils/conversion-util'
import {
getCurrentCurrency, getIsMainnet, preferencesSelector,
-} from '../selectors'
+} from './selectors'
import {
formatCurrency,
-} from '../helpers/confirm-transaction/util'
+} from '../helpers/utils/confirm-tx.util'
import {
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
-} from '../helpers/conversions.util'
+} from '../helpers/utils/conversions.util'
import {
formatETHFee,
-} from '../helpers/formatters'
+} from '../helpers/utils/formatters'
import {
calcGasTotal,
-} from '../components/send/send.utils'
+} from '../components/app/send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
const selectors = {
diff --git a/ui/app/selectors/custom-gas.test.js b/ui/app/selectors/custom-gas.test.js
new file mode 100644
index 000000000..6df4a60c7
--- /dev/null
+++ b/ui/app/selectors/custom-gas.test.js
@@ -0,0 +1,595 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+const {
+ getCustomGasErrors,
+ getCustomGasLimit,
+ getCustomGasPrice,
+ getCustomGasTotal,
+ getEstimatedGasPrices,
+ getEstimatedGasTimes,
+ getPriceAndTimeEstimates,
+ getRenderableBasicEstimateData,
+ getRenderableEstimateDataForSmallButtonsFromGWEI,
+} = proxyquire('./custom-gas', {})
+
+describe('custom-gas selectors', () => {
+
+ describe('getCustomGasPrice()', () => {
+ it('should return gas.customData.price', () => {
+ const mockState = { gas: { customData: { price: 'mockPrice' } } }
+ assert.equal(getCustomGasPrice(mockState), 'mockPrice')
+ })
+ })
+
+ describe('getCustomGasLimit()', () => {
+ it('should return gas.customData.limit', () => {
+ const mockState = { gas: { customData: { limit: 'mockLimit' } } }
+ assert.equal(getCustomGasLimit(mockState), 'mockLimit')
+ })
+ })
+
+ describe('getCustomGasTotal()', () => {
+ it('should return gas.customData.total', () => {
+ const mockState = { gas: { customData: { total: 'mockTotal' } } }
+ assert.equal(getCustomGasTotal(mockState), 'mockTotal')
+ })
+ })
+
+ describe('getCustomGasErrors()', () => {
+ it('should return gas.errors', () => {
+ const mockState = { gas: { errors: 'mockErrors' } }
+ assert.equal(getCustomGasErrors(mockState), 'mockErrors')
+ })
+ })
+
+ describe('getPriceAndTimeEstimates', () => {
+ it('should return price and time estimates', () => {
+ const mockState = { gas: { priceAndTimeEstimates: 'mockPriceAndTimeEstimates' } }
+ assert.equal(getPriceAndTimeEstimates(mockState), 'mockPriceAndTimeEstimates')
+ })
+ })
+
+ describe('getEstimatedGasPrices', () => {
+ it('should return price and time estimates', () => {
+ const mockState = { gas: { priceAndTimeEstimates: [
+ { gasprice: 12, somethingElse: 20 },
+ { gasprice: 22, expectedTime: 30 },
+ { gasprice: 32, somethingElse: 40 },
+ ] } }
+ assert.deepEqual(getEstimatedGasPrices(mockState), [12, 22, 32])
+ })
+ })
+
+ describe('getEstimatedGasTimes', () => {
+ it('should return price and time estimates', () => {
+ const mockState = { gas: { priceAndTimeEstimates: [
+ { somethingElse: 12, expectedTime: 20 },
+ { gasPrice: 22, expectedTime: 30 },
+ { somethingElse: 32, expectedTime: 40 },
+ ] } }
+ assert.deepEqual(getEstimatedGasTimes(mockState), [20, 30, 40])
+ })
+ })
+
+ describe('getRenderableBasicEstimateData()', () => {
+ const tests = [
+ {
+ expectedResult: [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: '$0.01',
+ feeInPrimaryCurrency: '0.0000525 ETH',
+ timeEstimate: '~6 min 36 sec',
+ priceInHexWei: '0x9502f900',
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: '$0.03',
+ feeInPrimaryCurrency: '0.000105 ETH',
+ timeEstimate: '~3 min 18 sec',
+ priceInHexWei: '0x12a05f200',
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: '$0.05',
+ feeInPrimaryCurrency: '0.00021 ETH',
+ timeEstimate: '~30 sec',
+ priceInHexWei: '0x2540be400',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 255.71,
+ currentCurrency: 'usd',
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 2.5,
+ safeLowWait: 6.6,
+ fast: 5,
+ fastWait: 3.3,
+ fastest: 10,
+ fastestWait: 0.5,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: '$0.27',
+ feeInPrimaryCurrency: '0.000105 ETH',
+ timeEstimate: '~13 min 12 sec',
+ priceInHexWei: '0x12a05f200',
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: '$0.54',
+ feeInPrimaryCurrency: '0.00021 ETH',
+ timeEstimate: '~6 min 36 sec',
+ priceInHexWei: '0x2540be400',
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: '$1.07',
+ feeInPrimaryCurrency: '0.00042 ETH',
+ timeEstimate: '~1 min',
+ priceInHexWei: '0x4a817c800',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 5,
+ safeLowWait: 13.2,
+ fast: 10,
+ fastWait: 6.6,
+ fastest: 20,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.000105 ETH',
+ timeEstimate: '~13 min 12 sec',
+ priceInHexWei: '0x12a05f200',
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.00021 ETH',
+ timeEstimate: '~6 min 36 sec',
+ priceInHexWei: '0x2540be400',
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.00042 ETH',
+ timeEstimate: '~1 min',
+ priceInHexWei: '0x4a817c800',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 5,
+ safeLowWait: 13.2,
+ fast: 10,
+ fastWait: 6.6,
+ fastest: 20,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: '$0.27',
+ feeInPrimaryCurrency: '0.000105 ETH',
+ timeEstimate: '~13 min 12 sec',
+ priceInHexWei: '0x12a05f200',
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: '$0.54',
+ feeInPrimaryCurrency: '0.00021 ETH',
+ timeEstimate: '~6 min 36 sec',
+ priceInHexWei: '0x2540be400',
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: '$1.07',
+ feeInPrimaryCurrency: '0.00042 ETH',
+ timeEstimate: '~1 min',
+ priceInHexWei: '0x4a817c800',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 5,
+ safeLowWait: 13.2,
+ fast: 10,
+ fastWait: 6.6,
+ fastest: 20,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: '$0.27',
+ feeInPrimaryCurrency: '0.000105 ETH',
+ timeEstimate: '~13 min 12 sec',
+ priceInHexWei: '0x12a05f200',
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: '$0.54',
+ feeInPrimaryCurrency: '0.00021 ETH',
+ timeEstimate: '~6 min 36 sec',
+ priceInHexWei: '0x2540be400',
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: '$1.07',
+ feeInPrimaryCurrency: '0.00042 ETH',
+ timeEstimate: '~1 min',
+ priceInHexWei: '0x4a817c800',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 5,
+ safeLowWait: 13.2,
+ fast: 10,
+ fastWait: 6.6,
+ fastest: 20,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ ]
+ it('should return renderable data about basic estimates', () => {
+ tests.forEach(test => {
+ assert.deepEqual(
+ getRenderableBasicEstimateData(test.mockState, '0x5208'),
+ test.expectedResult
+ )
+ })
+ })
+
+ })
+
+ describe('getRenderableEstimateDataForSmallButtonsFromGWEI()', () => {
+ const tests = [
+ {
+ expectedResult: [
+ {
+ feeInSecondaryCurrency: '$0.13',
+ feeInPrimaryCurrency: '0.00052 ETH',
+ labelKey: 'slow',
+ priceInHexWei: '0x5d21dba00',
+ },
+ {
+ feeInSecondaryCurrency: '$0.27',
+ feeInPrimaryCurrency: '0.00105 ETH',
+ labelKey: 'average',
+ priceInHexWei: '0xba43b7400',
+ },
+ {
+ feeInSecondaryCurrency: '$0.54',
+ feeInPrimaryCurrency: '0.0021 ETH',
+ labelKey: 'fast',
+ priceInHexWei: '0x174876e800',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 255.71,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 25,
+ safeLowWait: 6.6,
+ fast: 50,
+ fastWait: 3.3,
+ fastest: 100,
+ fastestWait: 0.5,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ feeInSecondaryCurrency: '$2.68',
+ feeInPrimaryCurrency: '0.00105 ETH',
+ labelKey: 'slow',
+ priceInHexWei: '0xba43b7400',
+ },
+ {
+ feeInSecondaryCurrency: '$5.37',
+ feeInPrimaryCurrency: '0.0021 ETH',
+ labelKey: 'average',
+ priceInHexWei: '0x174876e800',
+ },
+ {
+ feeInSecondaryCurrency: '$10.74',
+ feeInPrimaryCurrency: '0.0042 ETH',
+ labelKey: 'fast',
+ priceInHexWei: '0x2e90edd000',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 50,
+ safeLowWait: 13.2,
+ fast: 100,
+ fastWait: 6.6,
+ fastest: 200,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.00105 ETH',
+ labelKey: 'slow',
+ priceInHexWei: '0xba43b7400',
+ },
+ {
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.0021 ETH',
+ labelKey: 'average',
+ priceInHexWei: '0x174876e800',
+ },
+ {
+ feeInSecondaryCurrency: '',
+ feeInPrimaryCurrency: '0.0042 ETH',
+ labelKey: 'fast',
+ priceInHexWei: '0x2e90edd000',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 50,
+ safeLowWait: 13.2,
+ fast: 100,
+ fastWait: 6.6,
+ fastest: 200,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ feeInSecondaryCurrency: '$2.68',
+ feeInPrimaryCurrency: '0.00105 ETH',
+ labelKey: 'slow',
+ priceInHexWei: '0xba43b7400',
+ },
+ {
+ feeInSecondaryCurrency: '$5.37',
+ feeInPrimaryCurrency: '0.0021 ETH',
+ labelKey: 'average',
+ priceInHexWei: '0x174876e800',
+ },
+ {
+ feeInSecondaryCurrency: '$10.74',
+ feeInPrimaryCurrency: '0.0042 ETH',
+ labelKey: 'fast',
+ priceInHexWei: '0x2e90edd000',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 50,
+ safeLowWait: 13.2,
+ fast: 100,
+ fastWait: 6.6,
+ fastest: 200,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ {
+ expectedResult: [
+ {
+ feeInSecondaryCurrency: '$2.68',
+ feeInPrimaryCurrency: '0.00105 ETH',
+ labelKey: 'slow',
+ priceInHexWei: '0xba43b7400',
+ },
+ {
+ feeInSecondaryCurrency: '$5.37',
+ feeInPrimaryCurrency: '0.0021 ETH',
+ labelKey: 'average',
+ priceInHexWei: '0x174876e800',
+ },
+ {
+ feeInSecondaryCurrency: '$10.74',
+ feeInPrimaryCurrency: '0.0042 ETH',
+ labelKey: 'fast',
+ priceInHexWei: '0x2e90edd000',
+ },
+ ],
+ mockState: {
+ metamask: {
+ conversionRate: 2557.1,
+ currentCurrency: 'usd',
+ send: {
+ gasLimit: '0x5208',
+ },
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ gas: {
+ basicEstimates: {
+ blockTime: 14.16326530612245,
+ safeLow: 50,
+ safeLowWait: 13.2,
+ fast: 100,
+ fastWait: 6.6,
+ fastest: 200,
+ fastestWait: 1.0,
+ },
+ },
+ },
+ },
+ ]
+ it('should return renderable data about basic estimates appropriate for buttons with less info', () => {
+ tests.forEach(test => {
+ assert.deepEqual(
+ getRenderableEstimateDataForSmallButtonsFromGWEI(test.mockState),
+ test.expectedResult
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js
new file mode 100644
index 000000000..bea2cea33
--- /dev/null
+++ b/ui/app/selectors/selectors.js
@@ -0,0 +1,313 @@
+import { NETWORK_TYPES } from '../helpers/constants/common'
+import { stripHexPrefix } from 'ethereumjs-util'
+
+const abi = require('human-standard-token-abi')
+import {
+ transactionsSelector,
+} from './transactions'
+const {
+ multiplyCurrencies,
+} = require('../helpers/utils/conversion-util')
+
+const selectors = {
+ getSelectedAddress,
+ getSelectedIdentity,
+ getSelectedAccount,
+ getSelectedToken,
+ getSelectedTokenExchangeRate,
+ getSelectedTokenAssetImage,
+ getAssetImages,
+ getTokenExchangeRate,
+ conversionRateSelector,
+ transactionsSelector,
+ accountsWithSendEtherInfoSelector,
+ getCurrentAccountWithSendEtherInfo,
+ getGasIsLoading,
+ getForceGasMin,
+ getAddressBook,
+ getSendFrom,
+ getCurrentCurrency,
+ getNativeCurrency,
+ getSendAmount,
+ getSelectedTokenToFiatRate,
+ getSelectedTokenContract,
+ getSendMaxModeState,
+ getCurrentViewContext,
+ getTotalUnapprovedCount,
+ preferencesSelector,
+ getMetaMaskAccounts,
+ getCurrentEthBalance,
+ getNetworkIdentifier,
+ isBalanceCached,
+ getAdvancedInlineGasShown,
+ getIsMainnet,
+ getCurrentNetworkId,
+ getSelectedAsset,
+ getCurrentKeyring,
+ getAccountType,
+ getNumberOfAccounts,
+ getNumberOfTokens,
+ isEthereumNetwork,
+}
+
+module.exports = selectors
+
+function getNetworkIdentifier (state) {
+ const { metamask: { provider: { type, nickname, rpcTarget } } } = state
+
+ return nickname || rpcTarget || type
+}
+
+function getCurrentKeyring (state) {
+ const identity = getSelectedIdentity(state)
+
+ if (!identity) {
+ return null
+ }
+
+ const simpleAddress = stripHexPrefix(identity.address).toLowerCase()
+
+ const keyring = state.metamask.keyrings.find((kr) => {
+ return kr.accounts.includes(simpleAddress) ||
+ kr.accounts.includes(identity.address)
+ })
+
+ return keyring
+}
+
+function getAccountType (state) {
+ const currentKeyring = getCurrentKeyring(state)
+ const type = currentKeyring && currentKeyring.type
+
+ switch (type) {
+ case 'Trezor Hardware':
+ case 'Ledger Hardware':
+ return 'hardware'
+ case 'Simple Key Pair':
+ return 'imported'
+ default:
+ return 'default'
+ }
+}
+
+function getSelectedAsset (state) {
+ return getSelectedToken(state) || 'ETH'
+}
+
+function getCurrentNetworkId (state) {
+ return state.metamask.network
+}
+
+function getSelectedAddress (state) {
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
+
+ return selectedAddress
+}
+
+function getSelectedIdentity (state) {
+ const selectedAddress = getSelectedAddress(state)
+ const identities = state.metamask.identities
+
+ return identities[selectedAddress]
+}
+
+function getNumberOfAccounts (state) {
+ return Object.keys(state.metamask.accounts).length
+}
+
+function getNumberOfTokens (state) {
+ const tokens = state.metamask.tokens
+ return tokens ? tokens.length : 0
+}
+
+function getMetaMaskAccounts (state) {
+ const currentAccounts = state.metamask.accounts
+ const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
+ const selectedAccounts = {}
+
+ Object.keys(currentAccounts).forEach(accountID => {
+ const account = currentAccounts[accountID]
+ if (account && account.balance === null || account.balance === undefined) {
+ selectedAccounts[accountID] = {
+ ...account,
+ balance: cachedBalances && cachedBalances[accountID],
+ }
+ } else {
+ selectedAccounts[accountID] = account
+ }
+ })
+ return selectedAccounts
+}
+
+function isBalanceCached (state) {
+ const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
+ const cachedBalance = getSelectedAccountCachedBalance(state)
+
+ return Boolean(!selectedAccountBalance && cachedBalance)
+}
+
+function getSelectedAccountCachedBalance (state) {
+ const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
+ const selectedAddress = getSelectedAddress(state)
+
+ return cachedBalances && cachedBalances[selectedAddress]
+}
+
+function getSelectedAccount (state) {
+ const accounts = getMetaMaskAccounts(state)
+ const selectedAddress = getSelectedAddress(state)
+
+ return accounts[selectedAddress]
+}
+
+function getSelectedToken (state) {
+ const tokens = state.metamask.tokens || []
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
+ const sendToken = state.metamask.send.token
+
+ return selectedToken || sendToken || null
+}
+
+function getSelectedTokenExchangeRate (state) {
+ const contractExchangeRates = state.metamask.contractExchangeRates
+ const selectedToken = getSelectedToken(state) || {}
+ const { address } = selectedToken
+ 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
+}
+
+function conversionRateSelector (state) {
+ return state.metamask.conversionRate
+}
+
+function getAddressBook (state) {
+ return state.metamask.addressBook
+}
+
+function accountsWithSendEtherInfoSelector (state) {
+ const accounts = getMetaMaskAccounts(state)
+ const { identities } = state.metamask
+
+ const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
+ return Object.assign({}, account, identities[key])
+ })
+
+ return accountsWithSendEtherInfo
+}
+
+function getCurrentAccountWithSendEtherInfo (state) {
+ const currentAddress = getSelectedAddress(state)
+ const accounts = accountsWithSendEtherInfoSelector(state)
+
+ return accounts.find(({ address }) => address === currentAddress)
+}
+
+function getCurrentEthBalance (state) {
+ return getCurrentAccountWithSendEtherInfo(state).balance
+}
+
+function getGasIsLoading (state) {
+ return state.appState.gasIsLoading
+}
+
+function getForceGasMin (state) {
+ return state.metamask.send.forceGasMin
+}
+
+function getSendFrom (state) {
+ return state.metamask.send.from
+}
+
+function getSendAmount (state) {
+ return state.metamask.send.amount
+}
+
+function getSendMaxModeState (state) {
+ return state.metamask.send.maxModeOn
+}
+
+function getCurrentCurrency (state) {
+ return state.metamask.currentCurrency
+}
+
+function getNativeCurrency (state) {
+ return state.metamask.nativeCurrency
+}
+
+function getSelectedTokenToFiatRate (state) {
+ const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
+ const conversionRate = conversionRateSelector(state)
+
+ const tokenToFiatRate = multiplyCurrencies(
+ conversionRate,
+ selectedTokenExchangeRate,
+ { toNumericBase: 'dec' }
+ )
+
+ return tokenToFiatRate
+}
+
+function getSelectedTokenContract (state) {
+ const selectedToken = getSelectedToken(state)
+ return selectedToken
+ ? global.eth.contract(abi).at(selectedToken.address)
+ : null
+}
+
+function getCurrentViewContext (state) {
+ const { currentView = {} } = state.appState
+ return currentView.context
+}
+
+function getTotalUnapprovedCount ({ metamask }) {
+ const {
+ unapprovedTxs = {},
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+
+ return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
+ unapprovedTypedMessagesCount
+}
+
+function getIsMainnet (state) {
+ const networkType = getNetworkIdentifier(state)
+ return networkType === NETWORK_TYPES.MAINNET
+}
+
+function isEthereumNetwork (state) {
+ const networkType = getNetworkIdentifier(state)
+ const {
+ KOVAN,
+ MAINNET,
+ RINKEBY,
+ ROPSTEN,
+ } = NETWORK_TYPES
+ return [ KOVAN, MAINNET, RINKEBY, ROPSTEN].includes(type => type === networkType)
+}
+
+function preferencesSelector ({ metamask }) {
+ return metamask.preferences
+}
+
+function getAdvancedInlineGasShown (state) {
+ return Boolean(state.metamask.featureFlags.advancedInlineGas)
+}
diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js
deleted file mode 100644
index 73240d997..000000000
--- a/ui/app/selectors/tests/custom-gas.test.js
+++ /dev/null
@@ -1,595 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-const {
- getCustomGasErrors,
- getCustomGasLimit,
- getCustomGasPrice,
- getCustomGasTotal,
- getEstimatedGasPrices,
- getEstimatedGasTimes,
- getPriceAndTimeEstimates,
- getRenderableBasicEstimateData,
- getRenderableEstimateDataForSmallButtonsFromGWEI,
-} = proxyquire('../custom-gas', {})
-
-describe('custom-gas selectors', () => {
-
- describe('getCustomGasPrice()', () => {
- it('should return gas.customData.price', () => {
- const mockState = { gas: { customData: { price: 'mockPrice' } } }
- assert.equal(getCustomGasPrice(mockState), 'mockPrice')
- })
- })
-
- describe('getCustomGasLimit()', () => {
- it('should return gas.customData.limit', () => {
- const mockState = { gas: { customData: { limit: 'mockLimit' } } }
- assert.equal(getCustomGasLimit(mockState), 'mockLimit')
- })
- })
-
- describe('getCustomGasTotal()', () => {
- it('should return gas.customData.total', () => {
- const mockState = { gas: { customData: { total: 'mockTotal' } } }
- assert.equal(getCustomGasTotal(mockState), 'mockTotal')
- })
- })
-
- describe('getCustomGasErrors()', () => {
- it('should return gas.errors', () => {
- const mockState = { gas: { errors: 'mockErrors' } }
- assert.equal(getCustomGasErrors(mockState), 'mockErrors')
- })
- })
-
- describe('getPriceAndTimeEstimates', () => {
- it('should return price and time estimates', () => {
- const mockState = { gas: { priceAndTimeEstimates: 'mockPriceAndTimeEstimates' } }
- assert.equal(getPriceAndTimeEstimates(mockState), 'mockPriceAndTimeEstimates')
- })
- })
-
- describe('getEstimatedGasPrices', () => {
- it('should return price and time estimates', () => {
- const mockState = { gas: { priceAndTimeEstimates: [
- { gasprice: 12, somethingElse: 20 },
- { gasprice: 22, expectedTime: 30 },
- { gasprice: 32, somethingElse: 40 },
- ] } }
- assert.deepEqual(getEstimatedGasPrices(mockState), [12, 22, 32])
- })
- })
-
- describe('getEstimatedGasTimes', () => {
- it('should return price and time estimates', () => {
- const mockState = { gas: { priceAndTimeEstimates: [
- { somethingElse: 12, expectedTime: 20 },
- { gasPrice: 22, expectedTime: 30 },
- { somethingElse: 32, expectedTime: 40 },
- ] } }
- assert.deepEqual(getEstimatedGasTimes(mockState), [20, 30, 40])
- })
- })
-
- describe('getRenderableBasicEstimateData()', () => {
- const tests = [
- {
- expectedResult: [
- {
- labelKey: 'slow',
- feeInSecondaryCurrency: '$0.01',
- feeInPrimaryCurrency: '0.0000525 ETH',
- timeEstimate: '~6 min 36 sec',
- priceInHexWei: '0x9502f900',
- },
- {
- labelKey: 'average',
- feeInSecondaryCurrency: '$0.03',
- feeInPrimaryCurrency: '0.000105 ETH',
- timeEstimate: '~3 min 18 sec',
- priceInHexWei: '0x12a05f200',
- },
- {
- labelKey: 'fast',
- feeInSecondaryCurrency: '$0.05',
- feeInPrimaryCurrency: '0.00021 ETH',
- timeEstimate: '~30 sec',
- priceInHexWei: '0x2540be400',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 255.71,
- currentCurrency: 'usd',
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 2.5,
- safeLowWait: 6.6,
- fast: 5,
- fastWait: 3.3,
- fastest: 10,
- fastestWait: 0.5,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- labelKey: 'slow',
- feeInSecondaryCurrency: '$0.27',
- feeInPrimaryCurrency: '0.000105 ETH',
- timeEstimate: '~13 min 12 sec',
- priceInHexWei: '0x12a05f200',
- },
- {
- labelKey: 'average',
- feeInSecondaryCurrency: '$0.54',
- feeInPrimaryCurrency: '0.00021 ETH',
- timeEstimate: '~6 min 36 sec',
- priceInHexWei: '0x2540be400',
- },
- {
- labelKey: 'fast',
- feeInSecondaryCurrency: '$1.07',
- feeInPrimaryCurrency: '0.00042 ETH',
- timeEstimate: '~1 min',
- priceInHexWei: '0x4a817c800',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 5,
- safeLowWait: 13.2,
- fast: 10,
- fastWait: 6.6,
- fastest: 20,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- labelKey: 'slow',
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.000105 ETH',
- timeEstimate: '~13 min 12 sec',
- priceInHexWei: '0x12a05f200',
- },
- {
- labelKey: 'average',
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.00021 ETH',
- timeEstimate: '~6 min 36 sec',
- priceInHexWei: '0x2540be400',
- },
- {
- labelKey: 'fast',
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.00042 ETH',
- timeEstimate: '~1 min',
- priceInHexWei: '0x4a817c800',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'rinkeby',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 5,
- safeLowWait: 13.2,
- fast: 10,
- fastWait: 6.6,
- fastest: 20,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- labelKey: 'slow',
- feeInSecondaryCurrency: '$0.27',
- feeInPrimaryCurrency: '0.000105 ETH',
- timeEstimate: '~13 min 12 sec',
- priceInHexWei: '0x12a05f200',
- },
- {
- labelKey: 'average',
- feeInSecondaryCurrency: '$0.54',
- feeInPrimaryCurrency: '0.00021 ETH',
- timeEstimate: '~6 min 36 sec',
- priceInHexWei: '0x2540be400',
- },
- {
- labelKey: 'fast',
- feeInSecondaryCurrency: '$1.07',
- feeInPrimaryCurrency: '0.00042 ETH',
- timeEstimate: '~1 min',
- priceInHexWei: '0x4a817c800',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: true,
- },
- provider: {
- type: 'rinkeby',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 5,
- safeLowWait: 13.2,
- fast: 10,
- fastWait: 6.6,
- fastest: 20,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- labelKey: 'slow',
- feeInSecondaryCurrency: '$0.27',
- feeInPrimaryCurrency: '0.000105 ETH',
- timeEstimate: '~13 min 12 sec',
- priceInHexWei: '0x12a05f200',
- },
- {
- labelKey: 'average',
- feeInSecondaryCurrency: '$0.54',
- feeInPrimaryCurrency: '0.00021 ETH',
- timeEstimate: '~6 min 36 sec',
- priceInHexWei: '0x2540be400',
- },
- {
- labelKey: 'fast',
- feeInSecondaryCurrency: '$1.07',
- feeInPrimaryCurrency: '0.00042 ETH',
- timeEstimate: '~1 min',
- priceInHexWei: '0x4a817c800',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: true,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 5,
- safeLowWait: 13.2,
- fast: 10,
- fastWait: 6.6,
- fastest: 20,
- fastestWait: 1.0,
- },
- },
- },
- },
- ]
- it('should return renderable data about basic estimates', () => {
- tests.forEach(test => {
- assert.deepEqual(
- getRenderableBasicEstimateData(test.mockState, '0x5208'),
- test.expectedResult
- )
- })
- })
-
- })
-
- describe('getRenderableEstimateDataForSmallButtonsFromGWEI()', () => {
- const tests = [
- {
- expectedResult: [
- {
- feeInSecondaryCurrency: '$0.13',
- feeInPrimaryCurrency: '0.00052 ETH',
- labelKey: 'slow',
- priceInHexWei: '0x5d21dba00',
- },
- {
- feeInSecondaryCurrency: '$0.27',
- feeInPrimaryCurrency: '0.00105 ETH',
- labelKey: 'average',
- priceInHexWei: '0xba43b7400',
- },
- {
- feeInSecondaryCurrency: '$0.54',
- feeInPrimaryCurrency: '0.0021 ETH',
- labelKey: 'fast',
- priceInHexWei: '0x174876e800',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 255.71,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 25,
- safeLowWait: 6.6,
- fast: 50,
- fastWait: 3.3,
- fastest: 100,
- fastestWait: 0.5,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- feeInSecondaryCurrency: '$2.68',
- feeInPrimaryCurrency: '0.00105 ETH',
- labelKey: 'slow',
- priceInHexWei: '0xba43b7400',
- },
- {
- feeInSecondaryCurrency: '$5.37',
- feeInPrimaryCurrency: '0.0021 ETH',
- labelKey: 'average',
- priceInHexWei: '0x174876e800',
- },
- {
- feeInSecondaryCurrency: '$10.74',
- feeInPrimaryCurrency: '0.0042 ETH',
- labelKey: 'fast',
- priceInHexWei: '0x2e90edd000',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 50,
- safeLowWait: 13.2,
- fast: 100,
- fastWait: 6.6,
- fastest: 200,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.00105 ETH',
- labelKey: 'slow',
- priceInHexWei: '0xba43b7400',
- },
- {
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.0021 ETH',
- labelKey: 'average',
- priceInHexWei: '0x174876e800',
- },
- {
- feeInSecondaryCurrency: '',
- feeInPrimaryCurrency: '0.0042 ETH',
- labelKey: 'fast',
- priceInHexWei: '0x2e90edd000',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: false,
- },
- provider: {
- type: 'rinkeby',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 50,
- safeLowWait: 13.2,
- fast: 100,
- fastWait: 6.6,
- fastest: 200,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- feeInSecondaryCurrency: '$2.68',
- feeInPrimaryCurrency: '0.00105 ETH',
- labelKey: 'slow',
- priceInHexWei: '0xba43b7400',
- },
- {
- feeInSecondaryCurrency: '$5.37',
- feeInPrimaryCurrency: '0.0021 ETH',
- labelKey: 'average',
- priceInHexWei: '0x174876e800',
- },
- {
- feeInSecondaryCurrency: '$10.74',
- feeInPrimaryCurrency: '0.0042 ETH',
- labelKey: 'fast',
- priceInHexWei: '0x2e90edd000',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: true,
- },
- provider: {
- type: 'rinkeby',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 50,
- safeLowWait: 13.2,
- fast: 100,
- fastWait: 6.6,
- fastest: 200,
- fastestWait: 1.0,
- },
- },
- },
- },
- {
- expectedResult: [
- {
- feeInSecondaryCurrency: '$2.68',
- feeInPrimaryCurrency: '0.00105 ETH',
- labelKey: 'slow',
- priceInHexWei: '0xba43b7400',
- },
- {
- feeInSecondaryCurrency: '$5.37',
- feeInPrimaryCurrency: '0.0021 ETH',
- labelKey: 'average',
- priceInHexWei: '0x174876e800',
- },
- {
- feeInSecondaryCurrency: '$10.74',
- feeInPrimaryCurrency: '0.0042 ETH',
- labelKey: 'fast',
- priceInHexWei: '0x2e90edd000',
- },
- ],
- mockState: {
- metamask: {
- conversionRate: 2557.1,
- currentCurrency: 'usd',
- send: {
- gasLimit: '0x5208',
- },
- preferences: {
- showFiatInTestnets: true,
- },
- provider: {
- type: 'mainnet',
- },
- },
- gas: {
- basicEstimates: {
- blockTime: 14.16326530612245,
- safeLow: 50,
- safeLowWait: 13.2,
- fast: 100,
- fastWait: 6.6,
- fastest: 200,
- fastestWait: 1.0,
- },
- },
- },
- },
- ]
- it('should return renderable data about basic estimates appropriate for buttons with less info', () => {
- tests.forEach(test => {
- assert.deepEqual(
- getRenderableEstimateDataForSmallButtonsFromGWEI(test.mockState),
- test.expectedResult
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js
index fc1271c59..b1d27b333 100644
--- a/ui/app/selectors/transactions.js
+++ b/ui/app/selectors/transactions.js
@@ -4,12 +4,12 @@ import {
APPROVED_STATUS,
SUBMITTED_STATUS,
CONFIRMED_STATUS,
-} from '../constants/transactions'
+} from '../helpers/constants/transactions'
import {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
} from '../../../app/scripts/controllers/transactions/enums'
-import { hexToDecimal } from '../helpers/conversions.util'
+import { hexToDecimal } from '../helpers/utils/conversions.util'
import { selectedTokenAddressSelector } from './tokens'
import txHelper from '../../lib/tx-helper'
diff --git a/ui/app/store.js b/ui/app/store.js
deleted file mode 100644
index feebbabc0..000000000
--- a/ui/app/store.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const createStore = require('redux').createStore
-const applyMiddleware = require('redux').applyMiddleware
-const thunkMiddleware = require('redux-thunk').default
-const rootReducer = require('./reducers')
-const createLogger = require('redux-logger').createLogger
-
-global.METAMASK_DEBUG = process.env.METAMASK_DEBUG
-
-module.exports = configureStore
-
-const loggerMiddleware = createLogger({
- predicate: () => global.METAMASK_DEBUG,
-})
-
-const middlewares = [thunkMiddleware, loggerMiddleware]
-
-const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)
-
-function configureStore (initialState) {
- return createStoreWithMiddleware(rootReducer, initialState)
-}
diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js
new file mode 100644
index 000000000..785cadc3c
--- /dev/null
+++ b/ui/app/store/actions.js
@@ -0,0 +1,2778 @@
+const abi = require('human-standard-token-abi')
+const pify = require('pify')
+const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url')
+const { getTokenAddressFromTokenObject } = require('../helpers/utils/util')
+const {
+ calcTokenBalance,
+ estimateGas,
+} = require('../components/app/send/send.utils')
+const ethUtil = require('ethereumjs-util')
+const { fetchLocale } = require('../helpers/utils/i18n-helper')
+const log = require('loglevel')
+const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../app/scripts/lib/enums')
+const { hasUnconfirmedTransactions } = require('../helpers/utils/confirm-tx.util')
+const gasDuck = require('../ducks/gas/gas.duck')
+const WebcamUtils = require('../../lib/webcam-utils')
+
+var actions = {
+ _setBackgroundConnection: _setBackgroundConnection,
+
+ GO_HOME: 'GO_HOME',
+ goHome: goHome,
+ // modal state
+ MODAL_OPEN: 'UI_MODAL_OPEN',
+ MODAL_CLOSE: 'UI_MODAL_CLOSE',
+ showModal: showModal,
+ hideModal: hideModal,
+ // sidebar state
+ SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN',
+ SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE',
+ showSidebar: showSidebar,
+ hideSidebar: hideSidebar,
+ // sidebar state
+ ALERT_OPEN: 'UI_ALERT_OPEN',
+ ALERT_CLOSE: 'UI_ALERT_CLOSE',
+ showAlert: showAlert,
+ hideAlert: hideAlert,
+ QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED',
+ qrCodeDetected,
+ // network dropdown open
+ NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
+ NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
+ showNetworkDropdown: showNetworkDropdown,
+ hideNetworkDropdown: hideNetworkDropdown,
+ // menu state/
+ getNetworkStatus: 'getNetworkStatus',
+ // transition state
+ TRANSITION_FORWARD: 'TRANSITION_FORWARD',
+ TRANSITION_BACKWARD: 'TRANSITION_BACKWARD',
+ transitionForward,
+ transitionBackward,
+ // remote state
+ UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
+ updateMetamaskState: updateMetamaskState,
+ // notices
+ MARK_NOTICE_READ: 'MARK_NOTICE_READ',
+ markNoticeRead: markNoticeRead,
+ SHOW_NOTICE: 'SHOW_NOTICE',
+ showNotice: showNotice,
+ CLEAR_NOTICES: 'CLEAR_NOTICES',
+ clearNotices: clearNotices,
+ markAccountsFound,
+ // intialize screen
+ CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
+ SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
+ SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
+ fetchInfoToSync,
+ FORGOT_PASSWORD: 'FORGOT_PASSWORD',
+ forgotPassword: forgotPassword,
+ markPasswordForgotten,
+ unMarkPasswordForgotten,
+ SHOW_INIT_MENU: 'SHOW_INIT_MENU',
+ SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
+ SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
+ SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE',
+ SHOW_NEW_ACCOUNT_PAGE: 'SHOW_NEW_ACCOUNT_PAGE',
+ SET_NEW_ACCOUNT_FORM: 'SET_NEW_ACCOUNT_FORM',
+ unlockMetamask: unlockMetamask,
+ unlockFailed: unlockFailed,
+ unlockSucceeded,
+ showCreateVault: showCreateVault,
+ showRestoreVault: showRestoreVault,
+ showInitializeMenu: showInitializeMenu,
+ showImportPage,
+ showNewAccountPage,
+ setNewAccountForm,
+ createNewVaultAndKeychain: createNewVaultAndKeychain,
+ createNewVaultAndRestore: createNewVaultAndRestore,
+ createNewVaultInProgress: createNewVaultInProgress,
+ createNewVaultAndGetSeedPhrase,
+ unlockAndGetSeedPhrase,
+ addNewKeyring,
+ importNewAccount,
+ addNewAccount,
+ connectHardware,
+ checkHardwareStatus,
+ forgetDevice,
+ unlockHardwareWalletAccount,
+ NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
+ navigateToNewAccountScreen,
+ resetAccount,
+ removeAccount,
+ showNewVaultSeed: showNewVaultSeed,
+ showInfoPage: showInfoPage,
+ CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN',
+ closeWelcomeScreen,
+ // seed recovery actions
+ REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
+ revealSeedConfirmation: revealSeedConfirmation,
+ requestRevealSeed: requestRevealSeed,
+ requestRevealSeedWords,
+ // unlock screen
+ UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
+ UNLOCK_FAILED: 'UNLOCK_FAILED',
+ UNLOCK_SUCCEEDED: 'UNLOCK_SUCCEEDED',
+ UNLOCK_METAMASK: 'UNLOCK_METAMASK',
+ LOCK_METAMASK: 'LOCK_METAMASK',
+ tryUnlockMetamask: tryUnlockMetamask,
+ lockMetamask: lockMetamask,
+ unlockInProgress: unlockInProgress,
+ // error handling
+ displayWarning: displayWarning,
+ DISPLAY_WARNING: 'DISPLAY_WARNING',
+ HIDE_WARNING: 'HIDE_WARNING',
+ hideWarning: hideWarning,
+ // accounts screen
+ SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT',
+ SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN',
+ setSelectedToken,
+ SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
+ SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
+ SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
+ SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
+ SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
+ showQrScanner,
+ setCurrentCurrency,
+ setCurrentAccountTab,
+ // account detail screen
+ SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
+ showSendPage: showSendPage,
+ SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE',
+ showSendTokenPage,
+ ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
+ addToAddressBook: addToAddressBook,
+ REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
+ requestExportAccount: requestExportAccount,
+ EXPORT_ACCOUNT: 'EXPORT_ACCOUNT',
+ exportAccount: exportAccount,
+ SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
+ showPrivateKey: showPrivateKey,
+ exportAccountComplete,
+ SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
+ setAccountLabel,
+ updateNetworkNonce,
+ SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
+ // tx conf screen
+ COMPLETED_TX: 'COMPLETED_TX',
+ TRANSACTION_ERROR: 'TRANSACTION_ERROR',
+ NEXT_TX: 'NEXT_TX',
+ PREVIOUS_TX: 'PREV_TX',
+ EDIT_TX: 'EDIT_TX',
+ signMsg: signMsg,
+ cancelMsg: cancelMsg,
+ signPersonalMsg,
+ cancelPersonalMsg,
+ signTypedMsg,
+ cancelTypedMsg,
+ sendTx: sendTx,
+ signTx: signTx,
+ signTokenTx: signTokenTx,
+ updateTransaction,
+ updateAndApproveTx,
+ cancelTx: cancelTx,
+ cancelTxs,
+ completedTx: completedTx,
+ txError: txError,
+ nextTx: nextTx,
+ editTx,
+ previousTx: previousTx,
+ cancelAllTx: cancelAllTx,
+ viewPendingTx: viewPendingTx,
+ VIEW_PENDING_TX: 'VIEW_PENDING_TX',
+ updateTransactionParams,
+ UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
+ // send screen
+ UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
+ UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
+ UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
+ UPDATE_SEND_FROM: 'UPDATE_SEND_FROM',
+ UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA',
+ UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE',
+ UPDATE_SEND_TO: 'UPDATE_SEND_TO',
+ UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
+ UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO',
+ UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
+ UPDATE_SEND_WARNINGS: 'UPDATE_SEND_WARNINGS',
+ UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
+ UPDATE_SEND: 'UPDATE_SEND',
+ CLEAR_SEND: 'CLEAR_SEND',
+ OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
+ CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
+ GAS_LOADING_STARTED: 'GAS_LOADING_STARTED',
+ GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED',
+ setGasLimit,
+ setGasPrice,
+ updateGasData,
+ setGasTotal,
+ setSendTokenBalance,
+ updateSendTokenBalance,
+ updateSendHexData,
+ updateSendTo,
+ updateSendAmount,
+ updateSendMemo,
+ setMaxModeTo,
+ updateSend,
+ updateSendErrors,
+ updateSendWarnings,
+ clearSend,
+ setSelectedAddress,
+ gasLoadingStarted,
+ gasLoadingFinished,
+ // app messages
+ confirmSeedWords: confirmSeedWords,
+ showAccountDetail: showAccountDetail,
+ BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL',
+ backToAccountDetail: backToAccountDetail,
+ showAccountsPage: showAccountsPage,
+ showConfTxPage: showConfTxPage,
+ // config screen
+ SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE',
+ SET_RPC_TARGET: 'SET_RPC_TARGET',
+ SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET',
+ SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
+ SET_PREVIOUS_PROVIDER: 'SET_PREVIOUS_PROVIDER',
+ 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,
+ addKnownMethodData,
+ UPDATE_TOKENS: 'UPDATE_TOKENS',
+ updateAndSetCustomRpc: updateAndSetCustomRpc,
+ setRpcTarget: setRpcTarget,
+ delRpcTarget: delRpcTarget,
+ setProviderType: setProviderType,
+ SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
+ setHardwareWalletDefaultHdPath,
+ updateProviderType,
+ // loading overlay
+ SHOW_LOADING: 'SHOW_LOADING_INDICATION',
+ HIDE_LOADING: 'HIDE_LOADING_INDICATION',
+ showLoadingIndication: showLoadingIndication,
+ hideLoadingIndication: hideLoadingIndication,
+ // buy Eth with coinbase
+ onboardingBuyEthView,
+ ONBOARDING_BUY_ETH_VIEW: 'ONBOARDING_BUY_ETH_VIEW',
+ BUY_ETH: 'BUY_ETH',
+ buyEth: buyEth,
+ buyEthView: buyEthView,
+ buyWithShapeShift,
+ BUY_ETH_VIEW: 'BUY_ETH_VIEW',
+ COINBASE_SUBVIEW: 'COINBASE_SUBVIEW',
+ coinBaseSubview: coinBaseSubview,
+ SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
+ shapeShiftSubview: shapeShiftSubview,
+ PAIR_UPDATE: 'PAIR_UPDATE',
+ pairUpdate: pairUpdate,
+ coinShiftRquest: coinShiftRquest,
+ SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION',
+ showSubLoadingIndication: showSubLoadingIndication,
+ HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION',
+ hideSubLoadingIndication: hideSubLoadingIndication,
+// QR STUFF:
+ SHOW_QR: 'SHOW_QR',
+ showQrView: showQrView,
+ reshowQrCode: reshowQrCode,
+ SHOW_QR_VIEW: 'SHOW_QR_VIEW',
+// FORGOT PASSWORD:
+ BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU',
+ goBackToInitView: goBackToInitView,
+ RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
+ BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
+ backToUnlockView: backToUnlockView,
+ // SHOWING KEYCHAIN
+ SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN',
+ showNewKeychain: showNewKeychain,
+
+ callBackgroundThenUpdate,
+ forceUpdateMetamaskState,
+
+ TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU',
+ toggleAccountMenu,
+
+ useEtherscanProvider,
+
+ SET_USE_BLOCKIE: 'SET_USE_BLOCKIE',
+ setUseBlockie,
+
+ SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS',
+ SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT',
+ setParticipateInMetaMetrics,
+ setMetaMetricsSendCount,
+
+ // locale
+ SET_CURRENT_LOCALE: 'SET_CURRENT_LOCALE',
+ SET_LOCALE_MESSAGES: 'SET_LOCALE_MESSAGES',
+ setCurrentLocale,
+ updateCurrentLocale,
+ setLocaleMessages,
+ //
+ // Feature Flags
+ setFeatureFlag,
+ updateFeatureFlags,
+ UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS',
+
+ // Preferences
+ setPreference,
+ updatePreferences,
+ UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
+ setUseNativeCurrencyAsPrimaryCurrencyPreference,
+ setShowFiatConversionOnTestnetsPreference,
+
+ // Migration of users to new UI
+ setCompletedUiMigration,
+ completeUiMigration,
+ COMPLETE_UI_MIGRATION: 'COMPLETE_UI_MIGRATION',
+
+ // Onboarding
+ setCompletedOnboarding,
+ completeOnboarding,
+ COMPLETE_ONBOARDING: 'COMPLETE_ONBOARDING',
+
+ setMouseUserState,
+ SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
+
+ // Network
+ updateNetworkEndpointType,
+ UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
+
+ retryTransaction,
+ SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
+ CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
+ setPendingTokens,
+ clearPendingTokens,
+
+ createCancelTransaction,
+ createSpeedUpTransaction,
+
+ approveProviderRequest,
+ rejectProviderRequest,
+ clearApprovedOrigins,
+
+ setFirstTimeFlowType,
+ SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE',
+}
+
+module.exports = actions
+
+var background = null
+function _setBackgroundConnection (backgroundConnection) {
+ background = backgroundConnection
+}
+
+function goHome () {
+ return {
+ type: actions.GO_HOME,
+ }
+}
+
+// async actions
+
+function tryUnlockMetamask (password) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ dispatch(actions.unlockInProgress())
+ log.debug(`background.submitPassword`)
+
+ return new Promise((resolve, reject) => {
+ background.submitPassword(password, error => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve()
+ })
+ })
+ .then(() => {
+ dispatch(actions.unlockSucceeded())
+ return forceUpdateMetamaskState(dispatch)
+ })
+ .then(() => {
+ return new Promise((resolve, reject) => {
+ background.verifySeedPhrase(err => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ resolve()
+ })
+ })
+ })
+ .then(() => {
+ dispatch(actions.transitionForward())
+ dispatch(actions.hideLoadingIndication())
+ })
+ .catch(err => {
+ dispatch(actions.unlockFailed(err.message))
+ dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
+ })
+ }
+}
+
+function transitionForward () {
+ return {
+ type: this.TRANSITION_FORWARD,
+ }
+}
+
+function transitionBackward () {
+ return {
+ type: this.TRANSITION_BACKWARD,
+ }
+}
+
+function confirmSeedWords () {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.clearSeedWordCache`)
+ return new Promise((resolve, reject) => {
+ background.clearSeedWordCache((err, account) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.info('Seed word cache cleared. ' + account)
+ dispatch(actions.showAccountsPage())
+ resolve(account)
+ })
+ })
+ }
+}
+
+function createNewVaultAndRestore (password, seed) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.createNewVaultAndRestore`)
+
+ return new Promise((resolve, reject) => {
+ background.clearSeedWordCache((err) => {
+ if (err) {
+ return reject(err)
+ }
+
+ background.createNewVaultAndRestore(password, seed, (err) => {
+ if (err) {
+ return reject(err)
+ }
+
+ resolve()
+ })
+ })
+ })
+ .then(() => dispatch(actions.unMarkPasswordForgotten()))
+ .then(() => {
+ dispatch(actions.showAccountsPage())
+ dispatch(actions.hideLoadingIndication())
+ })
+ .catch(err => {
+ dispatch(actions.displayWarning(err.message))
+ dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
+ })
+ }
+}
+
+function createNewVaultAndKeychain (password) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.createNewVaultAndKeychain`)
+
+ return new Promise((resolve, reject) => {
+ background.createNewVaultAndKeychain(password, err => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.debug(`background.placeSeedWords`)
+
+ background.placeSeedWords((err) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ resolve()
+ })
+ })
+ })
+ .then(() => forceUpdateMetamaskState(dispatch))
+ .then(() => dispatch(actions.hideLoadingIndication()))
+ .catch(() => dispatch(actions.hideLoadingIndication()))
+ }
+}
+
+function createNewVaultAndGetSeedPhrase (password) {
+ return async dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ try {
+ await createNewVault(password)
+ const seedWords = await verifySeedPhrase()
+ dispatch(actions.hideLoadingIndication())
+ return seedWords
+ } catch (error) {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(error.message))
+ throw new Error(error.message)
+ }
+ }
+}
+
+function unlockAndGetSeedPhrase (password) {
+ return async dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ try {
+ await submitPassword(password)
+ const seedWords = await verifySeedPhrase()
+ await forceUpdateMetamaskState(dispatch)
+ dispatch(actions.hideLoadingIndication())
+ return seedWords
+ } catch (error) {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(error.message))
+ throw new Error(error.message)
+ }
+ }
+}
+
+function revealSeedConfirmation () {
+ return {
+ type: this.REVEAL_SEED_CONFIRMATION,
+ }
+}
+
+function submitPassword (password) {
+ return new Promise((resolve, reject) => {
+ background.submitPassword(password, error => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve()
+ })
+ })
+}
+
+function createNewVault (password) {
+ return new Promise((resolve, reject) => {
+ background.createNewVaultAndKeychain(password, error => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve(true)
+ })
+ })
+}
+
+function verifyPassword (password) {
+ return new Promise((resolve, reject) => {
+ background.submitPassword(password, error => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve(true)
+ })
+ })
+}
+
+function verifySeedPhrase () {
+ return new Promise((resolve, reject) => {
+ background.verifySeedPhrase((error, seedWords) => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve(seedWords)
+ })
+ })
+}
+
+function requestRevealSeed (password) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.submitPassword`)
+ return new Promise((resolve, reject) => {
+ background.submitPassword(password, err => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.debug(`background.placeSeedWords`)
+ background.placeSeedWords((err, result) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.showNewVaultSeed(result))
+ dispatch(actions.hideLoadingIndication())
+ resolve()
+ })
+ })
+ })
+ }
+}
+
+function requestRevealSeedWords (password) {
+ return async dispatch => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.submitPassword`)
+
+ try {
+ await verifyPassword(password)
+ const seedWords = await verifySeedPhrase()
+ dispatch(actions.hideLoadingIndication())
+ return seedWords
+ } catch (error) {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(error.message))
+ throw new Error(error.message)
+ }
+ }
+}
+
+function fetchInfoToSync () {
+ return dispatch => {
+ log.debug(`background.fetchInfoToSync`)
+ return new Promise((resolve, reject) => {
+ background.fetchInfoToSync((err, result) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+ resolve(result)
+ })
+ })
+ }
+}
+
+function resetAccount () {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ return new Promise((resolve, reject) => {
+ background.resetAccount((err, account) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.info('Transaction history reset for ' + account)
+ dispatch(actions.showAccountsPage())
+ resolve(account)
+ })
+ })
+ }
+}
+
+function removeAccount (address) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ return new Promise((resolve, reject) => {
+ background.removeAccount(address, (err, account) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.info('Account removed: ' + account)
+ dispatch(actions.showAccountsPage())
+ resolve()
+ })
+ })
+ }
+}
+
+function addNewKeyring (type, opts) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.addNewKeyring`)
+ background.addNewKeyring(type, opts, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) return dispatch(actions.displayWarning(err.message))
+ dispatch(actions.showAccountsPage())
+ })
+ }
+}
+
+function importNewAccount (strategy, args) {
+ return async (dispatch) => {
+ let newState
+ dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
+ try {
+ log.debug(`background.importAccountWithStrategy`)
+ await pify(background.importAccountWithStrategy).call(background, strategy, args)
+ log.debug(`background.getState`)
+ newState = await pify(background.getState).call(background)
+ } catch (err) {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(err.message))
+ throw err
+ }
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.updateMetamaskState(newState))
+ if (newState.selectedAddress) {
+ dispatch({
+ type: actions.SHOW_ACCOUNT_DETAIL,
+ value: newState.selectedAddress,
+ })
+ }
+ return newState
+ }
+}
+
+function navigateToNewAccountScreen () {
+ return {
+ type: this.NEW_ACCOUNT_SCREEN,
+ }
+}
+
+function addNewAccount () {
+ log.debug(`background.addNewAccount`)
+ return (dispatch, getState) => {
+ const oldIdentities = getState().metamask.identities
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.addNewAccount((err, { identities: newIdentities}) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+ const newAccountAddress = Object.keys(newIdentities).find(address => !oldIdentities[address])
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(newAccountAddress)
+ })
+ })
+ }
+}
+
+function checkHardwareStatus (deviceName, hdPath) {
+ log.debug(`background.checkHardwareStatus`, deviceName, hdPath)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(unlocked)
+ })
+ })
+ }
+}
+
+function forgetDevice (deviceName) {
+ log.debug(`background.forgetDevice`, deviceName)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.forgetDevice(deviceName, (err, response) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve()
+ })
+ })
+ }
+}
+
+function connectHardware (deviceName, page, hdPath) {
+ log.debug(`background.connectHardware`, deviceName, page, hdPath)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.connectHardware(deviceName, page, hdPath, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(accounts)
+ })
+ })
+ }
+}
+
+function unlockHardwareWalletAccount (index, deviceName, hdPath) {
+ log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+ return resolve()
+ })
+ })
+ }
+}
+
+function showInfoPage () {
+ return {
+ type: actions.SHOW_INFO_PAGE,
+ }
+}
+
+function showQrScanner (ROUTE) {
+ return (dispatch, getState) => {
+ return WebcamUtils.checkStatus()
+ .then(status => {
+ if (!status.environmentReady) {
+ // We need to switch to fullscreen mode to ask for permission
+ global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`)
+ } else {
+ dispatch(actions.showModal({
+ name: 'QR_SCANNER',
+ }))
+ }
+ }).catch(e => {
+ dispatch(actions.showModal({
+ name: 'QR_SCANNER',
+ error: true,
+ errorType: e.type,
+ }))
+ })
+ }
+}
+
+function setCurrentCurrency (currencyCode) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setCurrentCurrency`)
+ background.setCurrentCurrency(currencyCode, (err, data) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ log.error(err.stack)
+ return dispatch(actions.displayWarning(err.message))
+ }
+ dispatch({
+ type: actions.SET_CURRENT_FIAT,
+ value: {
+ currentCurrency: data.currentCurrency,
+ conversionRate: data.conversionRate,
+ conversionDate: data.conversionDate,
+ },
+ })
+ })
+ }
+}
+
+function signMsg (msgData) {
+ log.debug('action - signMsg')
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+
+ return new Promise((resolve, reject) => {
+ log.debug(`actions calling background.signMessage`)
+ background.signMessage(msgData, (err, newState) => {
+ log.debug('signMessage called back')
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function signPersonalMsg (msgData) {
+ log.debug('action - signPersonalMsg')
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ log.debug(`actions calling background.signPersonalMessage`)
+ background.signPersonalMessage(msgData, (err, newState) => {
+ log.debug('signPersonalMessage called back')
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function signTypedMsg (msgData) {
+ log.debug('action - signTypedMsg')
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ log.debug(`actions calling background.signTypedMessage`)
+ background.signTypedMessage(msgData, (err, newState) => {
+ log.debug('signTypedMessage called back')
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function signTx (txData) {
+ return (dispatch) => {
+ global.ethQuery.sendTransaction(txData, (err, data) => {
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ })
+ dispatch(actions.showConfTxPage({}))
+ }
+}
+
+function setGasLimit (gasLimit) {
+ return {
+ type: actions.UPDATE_GAS_LIMIT,
+ value: gasLimit,
+ }
+}
+
+function setGasPrice (gasPrice) {
+ return {
+ type: actions.UPDATE_GAS_PRICE,
+ value: gasPrice,
+ }
+}
+
+function setGasTotal (gasTotal) {
+ return {
+ type: actions.UPDATE_GAS_TOTAL,
+ value: gasTotal,
+ }
+}
+
+function updateGasData ({
+ gasPrice,
+ blockGasLimit,
+ recentBlocks,
+ selectedAddress,
+ selectedToken,
+ to,
+ value,
+ data,
+}) {
+ return (dispatch) => {
+ dispatch(actions.gasLoadingStarted())
+ return estimateGas({
+ estimateGasMethod: background.estimateGas,
+ blockGasLimit,
+ selectedAddress,
+ selectedToken,
+ to,
+ value,
+ estimateGasPrice: gasPrice,
+ data,
+ })
+ .then(gas => {
+ dispatch(actions.setGasLimit(gas))
+ dispatch(gasDuck.setCustomGasLimit(gas))
+ dispatch(updateSendErrors({ gasLoadingError: null }))
+ dispatch(actions.gasLoadingFinished())
+ })
+ .catch(err => {
+ log.error(err)
+ dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
+ dispatch(actions.gasLoadingFinished())
+ })
+ }
+}
+
+function gasLoadingStarted () {
+ return {
+ type: actions.GAS_LOADING_STARTED,
+ }
+}
+
+function gasLoadingFinished () {
+ return {
+ type: actions.GAS_LOADING_FINISHED,
+ }
+}
+
+function updateSendTokenBalance ({
+ selectedToken,
+ tokenContract,
+ address,
+}) {
+ return (dispatch) => {
+ const tokenBalancePromise = tokenContract
+ ? tokenContract.balanceOf(address)
+ : Promise.resolve()
+ return tokenBalancePromise
+ .then(usersToken => {
+ if (usersToken) {
+ const newTokenBalance = calcTokenBalance({ selectedToken, usersToken })
+ dispatch(setSendTokenBalance(newTokenBalance))
+ }
+ })
+ .catch(err => {
+ log.error(err)
+ updateSendErrors({ tokenBalance: 'tokenBalanceError' })
+ })
+ }
+}
+
+function updateSendErrors (errorObject) {
+ return {
+ type: actions.UPDATE_SEND_ERRORS,
+ value: errorObject,
+ }
+}
+
+function updateSendWarnings (warningObject) {
+ return {
+ type: actions.UPDATE_SEND_WARNINGS,
+ value: warningObject,
+ }
+}
+
+function setSendTokenBalance (tokenBalance) {
+ return {
+ type: actions.UPDATE_SEND_TOKEN_BALANCE,
+ value: tokenBalance,
+ }
+}
+
+function updateSendHexData (value) {
+ return {
+ type: actions.UPDATE_SEND_HEX_DATA,
+ value,
+ }
+}
+
+function updateSendTo (to, nickname = '') {
+ return {
+ type: actions.UPDATE_SEND_TO,
+ value: { to, nickname },
+ }
+}
+
+function updateSendAmount (amount) {
+ return {
+ type: actions.UPDATE_SEND_AMOUNT,
+ value: amount,
+ }
+}
+
+function updateSendMemo (memo) {
+ return {
+ type: actions.UPDATE_SEND_MEMO,
+ value: memo,
+ }
+}
+
+function setMaxModeTo (bool) {
+ return {
+ type: actions.UPDATE_MAX_MODE,
+ value: bool,
+ }
+}
+
+function updateSend (newSend) {
+ return {
+ type: actions.UPDATE_SEND,
+ value: newSend,
+ }
+}
+
+function clearSend () {
+ return {
+ type: actions.CLEAR_SEND,
+ }
+}
+
+
+function sendTx (txData) {
+ log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
+ return (dispatch, getState) => {
+ log.debug(`actions calling background.approveTransaction`)
+ window.onbeforeunload = null
+ background.approveTransaction(txData.id, (err) => {
+ if (err) {
+ dispatch(actions.txError(err))
+ return log.error(err.message)
+ }
+ dispatch(actions.completedTx(txData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+ })
+ }
+}
+
+function signTokenTx (tokenAddress, toAddress, amount, txData) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ const token = global.eth.contract(abi).at(tokenAddress)
+ token.transfer(toAddress, ethUtil.addHexPrefix(amount), txData)
+ .catch(err => {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(err.message))
+ })
+ dispatch(actions.showConfTxPage({}))
+ }
+}
+
+function updateTransaction (txData) {
+ log.info('actions: updateTx: ' + JSON.stringify(txData))
+ return dispatch => {
+ log.debug(`actions calling background.updateTx`)
+ dispatch(actions.showLoadingIndication())
+
+ return new Promise((resolve, reject) => {
+ background.updateTransaction(txData, (err) => {
+ dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
+ if (err) {
+ dispatch(actions.txError(err))
+ dispatch(actions.goHome())
+ log.error(err.message)
+ return reject(err)
+ }
+
+ resolve(txData)
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
+ dispatch(actions.showConfTxPage({ id: txData.id }))
+ dispatch(actions.hideLoadingIndication())
+ return txData
+ })
+ }
+}
+
+function updateAndApproveTx (txData) {
+ log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
+ return (dispatch, getState) => {
+ log.debug(`actions calling background.updateAndApproveTx`)
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ background.updateAndApproveTransaction(txData, err => {
+ dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
+ dispatch(actions.clearSend())
+
+ if (err) {
+ dispatch(actions.txError(err))
+ dispatch(actions.goHome())
+ log.error(err.message)
+ reject(err)
+ }
+
+ resolve(txData)
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
+ dispatch(actions.clearSend())
+ dispatch(actions.completedTx(txData.id))
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return txData
+ })
+ .catch((err) => {
+ dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
+ })
+ }
+}
+
+function completedTx (id) {
+ return {
+ type: actions.COMPLETED_TX,
+ value: id,
+ }
+}
+
+function updateTransactionParams (id, txParams) {
+ return {
+ type: actions.UPDATE_TRANSACTION_PARAMS,
+ id,
+ value: txParams,
+ }
+}
+
+function txError (err) {
+ return {
+ type: actions.TRANSACTION_ERROR,
+ message: err.message,
+ }
+}
+
+function cancelMsg (msgData) {
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ log.debug(`background.cancelMessage`)
+ background.cancelMessage(msgData.id, (err, newState) => {
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(msgData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function cancelPersonalMsg (msgData) {
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ const id = msgData.id
+ background.cancelPersonalMessage(id, (err, newState) => {
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function cancelTypedMsg (msgData) {
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ const id = msgData.id
+ background.cancelTypedMessage(id, (err, newState) => {
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ return reject(err)
+ }
+
+ dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return resolve(msgData)
+ })
+ })
+ }
+}
+
+function cancelTx (txData) {
+ return (dispatch, getState) => {
+ log.debug(`background.cancelTransaction`)
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ return new Promise((resolve, reject) => {
+ background.cancelTransaction(txData.id, err => {
+ if (err) {
+ return reject(err)
+ }
+
+ resolve()
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
+ dispatch(actions.clearSend())
+ dispatch(actions.completedTx(txData.id))
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return 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) => {
+ window.onbeforeunload = null
+ 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) => {
+ background.cancelTransaction(txData.id, () => {
+ dispatch(actions.completedTx(txData.id))
+ i === txsData.length - 1 ? dispatch(actions.goHome()) : null
+ })
+ })
+ }
+}
+//
+// initialize screen
+//
+
+function showCreateVault () {
+ return {
+ type: actions.SHOW_CREATE_VAULT,
+ }
+}
+
+function showRestoreVault () {
+ return {
+ type: actions.SHOW_RESTORE_VAULT,
+ }
+}
+
+function markPasswordForgotten () {
+ return (dispatch) => {
+ return background.markPasswordForgotten(() => {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.forgotPassword())
+ forceUpdateMetamaskState(dispatch)
+ })
+ }
+}
+
+function unMarkPasswordForgotten () {
+ return dispatch => {
+ return new Promise(resolve => {
+ background.unMarkPasswordForgotten(() => {
+ dispatch(actions.forgotPassword(false))
+ resolve()
+ })
+ })
+ .then(() => forceUpdateMetamaskState(dispatch))
+ }
+}
+
+function forgotPassword (forgotPasswordState = true) {
+ return {
+ type: actions.FORGOT_PASSWORD,
+ value: forgotPasswordState,
+ }
+}
+
+function showInitializeMenu () {
+ return {
+ type: actions.SHOW_INIT_MENU,
+ }
+}
+
+function showImportPage () {
+ return {
+ type: actions.SHOW_IMPORT_PAGE,
+ }
+}
+
+function showNewAccountPage (formToSelect) {
+ return {
+ type: actions.SHOW_NEW_ACCOUNT_PAGE,
+ formToSelect,
+ }
+}
+
+function setNewAccountForm (formToSelect) {
+ return {
+ type: actions.SET_NEW_ACCOUNT_FORM,
+ formToSelect,
+ }
+}
+
+function createNewVaultInProgress () {
+ return {
+ type: actions.CREATE_NEW_VAULT_IN_PROGRESS,
+ }
+}
+
+function showNewVaultSeed (seed) {
+ return {
+ type: actions.SHOW_NEW_VAULT_SEED,
+ value: seed,
+ }
+}
+
+function closeWelcomeScreen () {
+ return {
+ type: actions.CLOSE_WELCOME_SCREEN,
+ }
+}
+
+function backToUnlockView () {
+ return {
+ type: actions.BACK_TO_UNLOCK_VIEW,
+ }
+}
+
+function showNewKeychain () {
+ return {
+ type: actions.SHOW_NEW_KEYCHAIN,
+ }
+}
+
+//
+// unlock screen
+//
+
+function unlockInProgress () {
+ return {
+ type: actions.UNLOCK_IN_PROGRESS,
+ }
+}
+
+function unlockFailed (message) {
+ return {
+ type: actions.UNLOCK_FAILED,
+ value: message,
+ }
+}
+
+function unlockSucceeded (message) {
+ return {
+ type: actions.UNLOCK_SUCCEEDED,
+ value: message,
+ }
+}
+
+function unlockMetamask (account) {
+ return {
+ type: actions.UNLOCK_METAMASK,
+ value: account,
+ }
+}
+
+function updateMetamaskState (newState) {
+ return {
+ type: actions.UPDATE_METAMASK_STATE,
+ value: newState,
+ }
+}
+
+const backgroundSetLocked = () => {
+ return new Promise((resolve, reject) => {
+ background.setLocked(error => {
+ if (error) {
+ return reject(error)
+ }
+ resolve()
+ })
+ })
+}
+
+const updateMetamaskStateFromBackground = () => {
+ log.debug(`background.getState`)
+
+ return new Promise((resolve, reject) => {
+ background.getState((error, newState) => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve(newState)
+ })
+ })
+}
+
+function lockMetamask () {
+ log.debug(`background.setLocked`)
+
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ return backgroundSetLocked()
+ .then(() => updateMetamaskStateFromBackground())
+ .catch(error => {
+ dispatch(actions.displayWarning(error.message))
+ return Promise.reject(error)
+ })
+ .then(newState => {
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+ dispatch({ type: actions.LOCK_METAMASK })
+ })
+ .catch(() => {
+ dispatch(actions.hideLoadingIndication())
+ dispatch({ type: actions.LOCK_METAMASK })
+ })
+ }
+}
+
+function setCurrentAccountTab (newTabName) {
+ log.debug(`background.setCurrentAccountTab: ${newTabName}`)
+ return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName)
+}
+
+function setSelectedToken (tokenAddress) {
+ return {
+ type: actions.SET_SELECTED_TOKEN,
+ value: tokenAddress || null,
+ }
+}
+
+function setSelectedAddress (address) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setSelectedAddress`)
+ background.setSelectedAddress(address, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ })
+ }
+}
+
+function showAccountDetail (address) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setSelectedAddress`)
+ background.setSelectedAddress(address, (err, tokens) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ dispatch(updateTokens(tokens))
+ dispatch({
+ type: actions.SHOW_ACCOUNT_DETAIL,
+ value: address,
+ })
+ dispatch(actions.setSelectedToken())
+ })
+ }
+}
+
+function backToAccountDetail (address) {
+ return {
+ type: actions.BACK_TO_ACCOUNT_DETAIL,
+ value: address,
+ }
+}
+
+function showAccountsPage () {
+ return {
+ type: actions.SHOW_ACCOUNTS_PAGE,
+ }
+}
+
+function showConfTxPage ({transForward = true, id}) {
+ return {
+ type: actions.SHOW_CONF_TX_PAGE,
+ transForward,
+ id,
+ }
+}
+
+function nextTx () {
+ return {
+ type: actions.NEXT_TX,
+ }
+}
+
+function viewPendingTx (txId) {
+ return {
+ type: actions.VIEW_PENDING_TX,
+ value: txId,
+ }
+}
+
+function previousTx () {
+ return {
+ type: actions.PREVIOUS_TX,
+ }
+}
+
+function editTx (txId) {
+ return {
+ type: actions.EDIT_TX,
+ value: txId,
+ }
+}
+
+function showConfigPage (transitionForward = true) {
+ return {
+ type: actions.SHOW_CONFIG_PAGE,
+ value: transitionForward,
+ }
+}
+
+function showAddTokenPage (transitionForward = true) {
+ return {
+ type: actions.SHOW_ADD_TOKEN_PAGE,
+ value: transitionForward,
+ }
+}
+
+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, image, (err, tokens) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+ dispatch(actions.updateTokens(tokens))
+ resolve(tokens)
+ })
+ })
+ }
+}
+
+function removeToken (address) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.removeToken(address, (err, tokens) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+ dispatch(actions.updateTokens(tokens))
+ resolve(tokens)
+ })
+ })
+ }
+}
+
+function addTokens (tokens) {
+ return dispatch => {
+ if (Array.isArray(tokens)) {
+ dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens[0])))
+ return Promise.all(tokens.map(({ address, symbol, decimals }) => (
+ dispatch(addToken(address, symbol, decimals))
+ )))
+ } else {
+ dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens)))
+ return Promise.all(
+ Object
+ .entries(tokens)
+ .map(([_, { address, symbol, decimals }]) => (
+ dispatch(addToken(address, symbol, decimals))
+ ))
+ )
+ }
+ }
+}
+
+function removeSuggestedTokens () {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ window.onbeforeunload = null
+ 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 addKnownMethodData (fourBytePrefix, methodData) {
+ return (dispatch) => {
+ background.addKnownMethodData(fourBytePrefix, methodData)
+ }
+}
+
+function updateTokens (newTokens) {
+ return {
+ type: actions.UPDATE_TOKENS,
+ newTokens,
+ }
+}
+
+function clearPendingTokens () {
+ return {
+ type: actions.CLEAR_PENDING_TOKENS,
+ }
+}
+
+function goBackToInitView () {
+ return {
+ type: actions.BACK_TO_INIT_MENU,
+ }
+}
+
+//
+// notice
+//
+
+function markNoticeRead (notice) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.markNoticeRead`)
+ return new Promise((resolve, reject) => {
+ background.markNoticeRead(notice, (err, notice) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ if (notice) {
+ dispatch(actions.showNotice(notice))
+ resolve(true)
+ } else {
+ dispatch(actions.clearNotices())
+ resolve(false)
+ }
+ })
+ })
+ }
+}
+
+function showNotice (notice) {
+ return {
+ type: actions.SHOW_NOTICE,
+ value: notice,
+ }
+}
+
+function clearNotices () {
+ return {
+ type: actions.CLEAR_NOTICES,
+ }
+}
+
+function markAccountsFound () {
+ log.debug(`background.markAccountsFound`)
+ return callBackgroundThenUpdate(background.markAccountsFound)
+}
+
+function retryTransaction (txId, gasPrice) {
+ log.debug(`background.retryTransaction`)
+ let newTxId
+
+ return dispatch => {
+ return new Promise((resolve, reject) => {
+ background.retryTransaction(txId, gasPrice, (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)
+ }
+}
+
+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)
+ }
+}
+
+function createSpeedUpTransaction (txId, customGasPrice) {
+ log.debug('background.createSpeedUpTransaction')
+ let newTx
+
+ return dispatch => {
+ return new Promise((resolve, reject) => {
+ background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+
+ const { selectedAddressTxList } = newState
+ newTx = selectedAddressTxList[selectedAddressTxList.length - 1]
+ resolve(newState)
+ })
+ })
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => newTx)
+ }
+}
+
+//
+// config
+//
+
+function setProviderType (type) {
+ return (dispatch, getState) => {
+ const { type: currentProviderType } = getState().metamask.provider
+ log.debug(`background.setProviderType`, type)
+ background.setProviderType(type, (err, result) => {
+ if (err) {
+ log.error(err)
+ return dispatch(actions.displayWarning('Had a problem changing networks!'))
+ }
+ dispatch(setPreviousProvider(currentProviderType))
+ dispatch(actions.updateProviderType(type))
+ dispatch(actions.setSelectedToken())
+ })
+
+ }
+}
+
+function updateProviderType (type) {
+ return {
+ type: actions.SET_PROVIDER_TYPE,
+ value: type,
+ }
+}
+
+function setPreviousProvider (type) {
+ return {
+ type: actions.SET_PREVIOUS_PROVIDER,
+ value: type,
+ }
+}
+
+function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) {
+ return (dispatch) => {
+ log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`)
+ background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => {
+ if (err) {
+ log.error(err)
+ return dispatch(actions.displayWarning('Had a problem changing networks!'))
+ }
+ dispatch({
+ type: actions.SET_RPC_TARGET,
+ value: newRpc,
+ })
+ })
+ }
+}
+
+function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) {
+ return (dispatch) => {
+ log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
+ background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => {
+ if (err) {
+ log.error(err)
+ 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())
+ })
+ }
+}
+
+// Calls the addressBookController to add a new address.
+function addToAddressBook (recipient, nickname = '') {
+ log.debug(`background.addToAddressBook`)
+ return (dispatch) => {
+ background.setAddressBook(recipient, nickname, (err, result) => {
+ if (err) {
+ log.error(err)
+ return dispatch(self.displayWarning('Address book failed to update'))
+ }
+ })
+ }
+}
+
+function useEtherscanProvider () {
+ log.debug(`background.useEtherscanProvider`)
+ background.useEtherscanProvider()
+ return {
+ type: actions.USE_ETHERSCAN_PROVIDER,
+ }
+}
+
+function showNetworkDropdown () {
+ return {
+ type: actions.NETWORK_DROPDOWN_OPEN,
+ }
+}
+
+function hideNetworkDropdown () {
+ return {
+ type: actions.NETWORK_DROPDOWN_CLOSE,
+ }
+}
+
+
+function showModal (payload) {
+ return {
+ type: actions.MODAL_OPEN,
+ payload,
+ }
+}
+
+function hideModal (payload) {
+ return {
+ type: actions.MODAL_CLOSE,
+ payload,
+ }
+}
+
+function showSidebar ({ transitionName, type, props }) {
+ return {
+ type: actions.SIDEBAR_OPEN,
+ value: {
+ transitionName,
+ type,
+ props,
+ },
+ }
+}
+
+function hideSidebar () {
+ return {
+ type: actions.SIDEBAR_CLOSE,
+ }
+}
+
+function showAlert (msg) {
+ return {
+ type: actions.ALERT_OPEN,
+ value: msg,
+ }
+}
+
+function hideAlert () {
+ return {
+ type: actions.ALERT_CLOSE,
+ }
+}
+
+/**
+ * This action will receive two types of values via qrCodeData
+ * an object with the following structure {type, values}
+ * or null (used to clear the previous value)
+ */
+function qrCodeDetected (qrCodeData) {
+ return {
+ type: actions.QR_CODE_DETECTED,
+ value: qrCodeData,
+ }
+}
+
+function showLoadingIndication (message) {
+ return {
+ type: actions.SHOW_LOADING,
+ value: message,
+ }
+}
+
+function setHardwareWalletDefaultHdPath ({ device, path }) {
+ return {
+ type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
+ value: {device, path},
+ }
+}
+
+function hideLoadingIndication () {
+ return {
+ type: actions.HIDE_LOADING,
+ }
+}
+
+function showSubLoadingIndication () {
+ return {
+ type: actions.SHOW_SUB_LOADING_INDICATION,
+ }
+}
+
+function hideSubLoadingIndication () {
+ return {
+ type: actions.HIDE_SUB_LOADING_INDICATION,
+ }
+}
+
+function displayWarning (text) {
+ return {
+ type: actions.DISPLAY_WARNING,
+ value: text,
+ }
+}
+
+function hideWarning () {
+ return {
+ type: actions.HIDE_WARNING,
+ }
+}
+
+function requestExportAccount () {
+ return {
+ type: actions.REQUEST_ACCOUNT_EXPORT,
+ }
+}
+
+function exportAccount (password, address) {
+ var self = this
+
+ return function (dispatch) {
+ dispatch(self.showLoadingIndication())
+
+ log.debug(`background.submitPassword`)
+ return new Promise((resolve, reject) => {
+ background.submitPassword(password, function (err) {
+ if (err) {
+ log.error('Error in submiting password.')
+ dispatch(self.hideLoadingIndication())
+ dispatch(self.displayWarning('Incorrect Password.'))
+ return reject(err)
+ }
+ log.debug(`background.exportAccount`)
+ return background.exportAccount(address, function (err, result) {
+ dispatch(self.hideLoadingIndication())
+
+ if (err) {
+ log.error(err)
+ dispatch(self.displayWarning('Had a problem exporting the account.'))
+ return reject(err)
+ }
+
+ // dispatch(self.exportAccountComplete())
+ dispatch(self.showPrivateKey(result))
+
+ return resolve(result)
+ })
+ })
+ })
+ }
+}
+
+function exportAccountComplete () {
+ return {
+ type: actions.EXPORT_ACCOUNT,
+ }
+}
+
+function showPrivateKey (key) {
+ return {
+ type: actions.SHOW_PRIVATE_KEY,
+ value: key,
+ }
+}
+
+function setAccountLabel (account, label) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setAccountLabel`)
+
+ return new Promise((resolve, reject) => {
+ background.setAccountLabel(account, label, (err) => {
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+
+ dispatch({
+ type: actions.SET_ACCOUNT_LABEL,
+ value: { account, label },
+ })
+
+ resolve(account)
+ })
+ })
+ }
+}
+
+function showSendPage () {
+ return {
+ type: actions.SHOW_SEND_PAGE,
+ }
+}
+
+function showSendTokenPage () {
+ return {
+ type: actions.SHOW_SEND_TOKEN_PAGE,
+ }
+}
+
+function buyEth (opts) {
+ return (dispatch) => {
+ const url = getBuyEthUrl(opts)
+ global.platform.openWindow({ url })
+ dispatch({
+ type: actions.BUY_ETH,
+ })
+ }
+}
+
+function onboardingBuyEthView (address) {
+ return {
+ type: actions.ONBOARDING_BUY_ETH_VIEW,
+ value: address,
+ }
+}
+
+function buyEthView (address) {
+ return {
+ type: actions.BUY_ETH_VIEW,
+ value: address,
+ }
+}
+
+function coinBaseSubview () {
+ return {
+ type: actions.COINBASE_SUBVIEW,
+ }
+}
+
+function pairUpdate (coin) {
+ return (dispatch) => {
+ dispatch(actions.showSubLoadingIndication())
+ dispatch(actions.hideWarning())
+ shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
+ dispatch(actions.hideSubLoadingIndication())
+ if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
+ dispatch({
+ type: actions.PAIR_UPDATE,
+ value: {
+ marketinfo: mktResponse,
+ },
+ })
+ })
+ }
+}
+
+function shapeShiftSubview (network) {
+ var pair = 'btc_eth'
+ return (dispatch) => {
+ dispatch(actions.showSubLoadingIndication())
+ shapeShiftRequest('marketinfo', {pair}, (mktResponse) => {
+ shapeShiftRequest('getcoins', {}, (response) => {
+ dispatch(actions.hideSubLoadingIndication())
+ if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
+ dispatch({
+ type: actions.SHAPESHIFT_SUBVIEW,
+ value: {
+ marketinfo: mktResponse,
+ coinOptions: response,
+ },
+ })
+ })
+ })
+ }
+}
+
+function coinShiftRquest (data, marketData) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ shapeShiftRequest('shift', { method: 'POST', data}, (response) => {
+ dispatch(actions.hideLoadingIndication())
+ if (response.error) return dispatch(actions.displayWarning(response.error))
+ var message = `
+ Deposit your ${response.depositType} to the address below:`
+ log.debug(`background.createShapeShiftTx`)
+ background.createShapeShiftTx(response.deposit, response.depositType)
+ dispatch(actions.showQrView(response.deposit, [message].concat(marketData)))
+ })
+ }
+}
+
+function buyWithShapeShift (data) {
+ return dispatch => new Promise((resolve, reject) => {
+ shapeShiftRequest('shift', { method: 'POST', data}, (response) => {
+ if (response.error) {
+ return reject(response.error)
+ }
+ background.createShapeShiftTx(response.deposit, response.depositType)
+ return resolve(response)
+ })
+ })
+}
+
+function showQrView (data, message) {
+ return {
+ type: actions.SHOW_QR_VIEW,
+ value: {
+ message: message,
+ data: data,
+ },
+ }
+}
+function reshowQrCode (data, coin) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
+ if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
+
+ var message = [
+ `Deposit your ${coin} to the address below:`,
+ `Deposit Limit: ${mktResponse.limit}`,
+ `Deposit Minimum:${mktResponse.minimum}`,
+ ]
+
+ dispatch(actions.hideLoadingIndication())
+ return dispatch(actions.showQrView(data, message))
+ // return dispatch(actions.showModal({
+ // name: 'SHAPESHIFT_DEPOSIT_TX',
+ // Qr: { data, message },
+ // }))
+ })
+ }
+}
+
+function shapeShiftRequest (query, options, cb) {
+ var queryResponse, method
+ !options ? options = {} : null
+ options.method ? method = options.method : method = 'GET'
+
+ var requestListner = function (request) {
+ try {
+ queryResponse = JSON.parse(this.responseText)
+ cb ? cb(queryResponse) : null
+ return queryResponse
+ } catch (e) {
+ cb ? cb({error: e}) : null
+ return e
+ }
+ }
+
+ var shapShiftReq = new XMLHttpRequest()
+ shapShiftReq.addEventListener('load', requestListner)
+ shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true)
+
+ if (options.method === 'POST') {
+ var jsonObj = JSON.stringify(options.data)
+ shapShiftReq.setRequestHeader('Content-Type', 'application/json')
+ return shapShiftReq.send(jsonObj)
+ } else {
+ return shapShiftReq.send()
+ }
+}
+
+function setFeatureFlag (feature, activated, notificationType) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.setFeatureFlag(feature, activated, (err, updatedFeatureFlags) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+ dispatch(actions.updateFeatureFlags(updatedFeatureFlags))
+ notificationType && dispatch(actions.showModal({ name: notificationType }))
+ resolve(updatedFeatureFlags)
+ })
+ })
+ }
+}
+
+function updateFeatureFlags (updatedFeatureFlags) {
+ return {
+ type: actions.UPDATE_FEATURE_FLAGS,
+ value: updatedFeatureFlags,
+ }
+}
+
+function setPreference (preference, value) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.setPreference(preference, value, (err, updatedPreferences) => {
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.updatePreferences(updatedPreferences))
+ resolve(updatedPreferences)
+ })
+ })
+ }
+}
+
+function updatePreferences (value) {
+ return {
+ type: actions.UPDATE_PREFERENCES,
+ value,
+ }
+}
+
+function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) {
+ return setPreference('useNativeCurrencyAsPrimaryCurrency', value)
+}
+
+function setShowFiatConversionOnTestnetsPreference (value) {
+ return setPreference('showFiatInTestnets', value)
+}
+
+function setCompletedOnboarding () {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.markAllNoticesRead(err => {
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.clearNotices())
+ resolve(false)
+ })
+ })
+ .then(() => {
+ return new Promise((resolve, reject) => {
+ background.completeOnboarding(err => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completeOnboarding())
+ dispatch(actions.hideLoadingIndication())
+ resolve()
+ })
+ })
+ })
+ }
+}
+
+function completeOnboarding () {
+ return {
+ type: actions.COMPLETE_ONBOARDING,
+ }
+}
+
+function setCompletedUiMigration () {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.completeUiMigration(err => {
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completeUiMigration())
+ resolve()
+ })
+ })
+ }
+}
+
+function completeUiMigration () {
+ return {
+ type: actions.COMPLETE_UI_MIGRATION,
+ }
+}
+
+function setNetworkNonce (networkNonce) {
+ return {
+ type: actions.SET_NETWORK_NONCE,
+ value: networkNonce,
+ }
+}
+
+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)
+ })
+ })
+ }
+}
+
+function setMouseUserState (isMouseUser) {
+ return {
+ type: actions.SET_MOUSE_USER_STATE,
+ value: isMouseUser,
+ }
+}
+
+// Call Background Then Update
+//
+// A function generator for a common pattern wherein:
+// We show loading indication.
+// We call a background method.
+// We hide loading indication.
+// If it errored, we show a warning.
+// If it didn't, we update the state.
+function callBackgroundThenUpdateNoSpinner (method, ...args) {
+ return (dispatch) => {
+ method.call(background, ...args, (err) => {
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ forceUpdateMetamaskState(dispatch)
+ })
+ }
+}
+
+function callBackgroundThenUpdate (method, ...args) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ method.call(background, ...args, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ forceUpdateMetamaskState(dispatch)
+ })
+ }
+}
+
+function forceUpdateMetamaskState (dispatch) {
+ log.debug(`background.getState`)
+ return new Promise((resolve, reject) => {
+ background.getState((err, newState) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.updateMetamaskState(newState))
+ resolve(newState)
+ })
+ })
+}
+
+function toggleAccountMenu () {
+ return {
+ type: actions.TOGGLE_ACCOUNT_MENU,
+ }
+}
+
+function setParticipateInMetaMetrics (val) {
+ return (dispatch) => {
+ log.debug(`background.setParticipateInMetaMetrics`)
+ return new Promise((resolve, reject) => {
+ background.setParticipateInMetaMetrics(val, (err, metaMetricsId) => {
+ log.debug(err)
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch({
+ type: actions.SET_PARTICIPATE_IN_METAMETRICS,
+ value: val,
+ })
+
+ resolve([val, metaMetricsId])
+ })
+ })
+ }
+}
+
+function setMetaMetricsSendCount (val) {
+ return (dispatch) => {
+ log.debug(`background.setMetaMetricsSendCount`)
+ return new Promise((resolve, reject) => {
+ background.setMetaMetricsSendCount(val, (err) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch({
+ type: actions.SET_METAMETRICS_SEND_COUNT,
+ value: val,
+ })
+
+ resolve(val)
+ })
+ })
+ }
+}
+
+function setUseBlockie (val) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setUseBlockie`)
+ background.setUseBlockie(val, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ })
+ dispatch({
+ type: actions.SET_USE_BLOCKIE,
+ value: val,
+ })
+ }
+}
+
+function updateCurrentLocale (key) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ return fetchLocale(key)
+ .then((localeMessages) => {
+ log.debug(`background.setCurrentLocale`)
+ background.setCurrentLocale(key, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ dispatch(actions.setCurrentLocale(key))
+ dispatch(actions.setLocaleMessages(localeMessages))
+ })
+ })
+ }
+}
+
+function setCurrentLocale (key) {
+ return {
+ type: actions.SET_CURRENT_LOCALE,
+ value: key,
+ }
+}
+
+function setLocaleMessages (localeMessages) {
+ return {
+ type: actions.SET_LOCALE_MESSAGES,
+ value: localeMessages,
+ }
+}
+
+function updateNetworkEndpointType (networkEndpointType) {
+ return {
+ type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,
+ value: networkEndpointType,
+ }
+}
+
+function setPendingTokens (pendingTokens) {
+ const { customToken = {}, selectedTokens = {} } = pendingTokens
+ const { address, symbol, decimals } = customToken
+ const tokens = address && symbol && decimals
+ ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
+ : selectedTokens
+
+ return {
+ type: actions.SET_PENDING_TOKENS,
+ payload: tokens,
+ }
+}
+
+function approveProviderRequest (tabID) {
+ return (dispatch) => {
+ background.approveProviderRequest(tabID)
+ }
+}
+
+function rejectProviderRequest (tabID) {
+ return (dispatch) => {
+ background.rejectProviderRequest(tabID)
+ }
+}
+
+function clearApprovedOrigins () {
+ return (dispatch) => {
+ background.clearApprovedOrigins()
+ }
+}
+
+function setFirstTimeFlowType (type) {
+ return (dispatch) => {
+ log.debug(`background.setFirstTimeFlowType`)
+ background.setFirstTimeFlowType(type, (err) => {
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ })
+ dispatch({
+ type: actions.SET_FIRST_TIME_FLOW_TYPE,
+ value: type,
+ })
+ }
+}
diff --git a/ui/app/store/store.js b/ui/app/store/store.js
new file mode 100644
index 000000000..9f12f469e
--- /dev/null
+++ b/ui/app/store/store.js
@@ -0,0 +1,21 @@
+const createStore = require('redux').createStore
+const applyMiddleware = require('redux').applyMiddleware
+const thunkMiddleware = require('redux-thunk').default
+const rootReducer = require('../ducks')
+const createLogger = require('redux-logger').createLogger
+
+global.METAMASK_DEBUG = process.env.METAMASK_DEBUG
+
+module.exports = configureStore
+
+const loggerMiddleware = createLogger({
+ predicate: () => global.METAMASK_DEBUG,
+})
+
+const middlewares = [thunkMiddleware, loggerMiddleware]
+
+const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)
+
+function configureStore (initialState) {
+ return createStoreWithMiddleware(rootReducer, initialState)
+}
diff --git a/ui/app/util.js b/ui/app/util.js
deleted file mode 100644
index 3237e5feb..000000000
--- a/ui/app/util.js
+++ /dev/null
@@ -1,326 +0,0 @@
-const abi = require('human-standard-token-abi')
-const ethUtil = require('ethereumjs-util')
-const hexToBn = require('../../app/scripts/lib/hex-to-bn')
-import { DateTime } from 'luxon'
-
-const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1)
-const GWEI_FACTOR = new ethUtil.BN(1e9)
-const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
-
-// formatData :: ( date: <Unix Timestamp> ) -> String
-function formatDate (date, format = 'M/d/y \'at\' T') {
- return DateTime.fromMillis(date).toFormat(format)
-}
-
-var valueTable = {
- wei: '1000000000000000000',
- kwei: '1000000000000000',
- mwei: '1000000000000',
- gwei: '1000000000',
- szabo: '1000000',
- finney: '1000',
- ether: '1',
- kether: '0.001',
- mether: '0.000001',
- gether: '0.000000001',
- tether: '0.000000000001',
-}
-var bnTable = {}
-for (var currency in valueTable) {
- bnTable[currency] = new ethUtil.BN(valueTable[currency], 10)
-}
-
-module.exports = {
- valuesFor: valuesFor,
- addressSummary: addressSummary,
- miniAddressSummary: miniAddressSummary,
- isAllOneCase: isAllOneCase,
- isValidAddress: isValidAddress,
- isValidENSAddress,
- numericBalance: numericBalance,
- parseBalance: parseBalance,
- formatBalance: formatBalance,
- generateBalanceObject: generateBalanceObject,
- dataSize: dataSize,
- readableDate: readableDate,
- normalizeToWei: normalizeToWei,
- normalizeEthStringToWei: normalizeEthStringToWei,
- normalizeNumberToWei: normalizeNumberToWei,
- valueTable: valueTable,
- bnTable: bnTable,
- isHex: isHex,
- formatDate,
- bnMultiplyByFraction,
- getTxFeeBn,
- shortenBalance,
- getContractAtAddress,
- exportAsFile: exportAsFile,
- isInvalidChecksumAddress,
- allNull,
- getTokenAddressFromTokenObject,
- checksumAddress,
- addressSlicer,
- isEthNetwork,
-}
-
-function isEthNetwork (netId) {
- if (!netId || netId === '1' || netId === '3' || netId === '4' || netId === '42' || netId === '5777') {
- return true
- }
-
- return false
-}
-
-function valuesFor (obj) {
- if (!obj) return []
- return Object.keys(obj)
- .map(function (key) { return obj[key] })
-}
-
-function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) {
- if (!address) return ''
- let checked = checksumAddress(address)
- if (!includeHex) {
- checked = ethUtil.stripHexPrefix(checked)
- }
- return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...'
-}
-
-function miniAddressSummary (address) {
- if (!address) return ''
- var checked = checksumAddress(address)
- return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...'
-}
-
-function isValidAddress (address, network) {
- var prefixed = ethUtil.addHexPrefix(address)
- if (address === '0x0000000000000000000000000000000000000000') return false
- return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
-}
-
-function isValidENSAddress (address) {
- return address.match(/^.{7,}\.(eth|test)$/)
-}
-
-function isInvalidChecksumAddress (address) {
- var prefixed = ethUtil.addHexPrefix(address)
- if (address === '0x0000000000000000000000000000000000000000') return false
- return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed)
-}
-
-function isAllOneCase (address) {
- if (!address) return true
- var lower = address.toLowerCase()
- var upper = address.toUpperCase()
- return address === lower || address === upper
-}
-
-// Takes wei Hex, returns wei BN, even if input is null
-function numericBalance (balance) {
- if (!balance) return new ethUtil.BN(0, 16)
- var stripped = ethUtil.stripHexPrefix(balance)
- return new ethUtil.BN(stripped, 16)
-}
-
-// Takes hex, returns [beforeDecimal, afterDecimal]
-function parseBalance (balance) {
- var beforeDecimal, afterDecimal
- const wei = numericBalance(balance)
- var weiString = wei.toString()
- const trailingZeros = /0+$/
-
- beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0'
- afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '')
- if (afterDecimal === '') { afterDecimal = '0' }
- return [beforeDecimal, afterDecimal]
-}
-
-// Takes wei hex, returns an object with three properties.
-// Its "formatted" property is what we generally use to render values.
-function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') {
- var parsed = needsParse ? parseBalance(balance) : balance.split('.')
- var beforeDecimal = parsed[0]
- var afterDecimal = parsed[1]
- var formatted = 'None'
- if (decimalsToKeep === undefined) {
- if (beforeDecimal === '0') {
- if (afterDecimal !== '0') {
- var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits
- if (sigFigs) { afterDecimal = sigFigs[0] }
- formatted = '0.' + afterDecimal + ` ${ticker}`
- }
- } else {
- formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}`
- }
- } else {
- afterDecimal += Array(decimalsToKeep).join('0')
- formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}`
- }
- return formatted
-}
-
-
-function generateBalanceObject (formattedBalance, decimalsToKeep = 1) {
- var balance = formattedBalance.split(' ')[0]
- var label = formattedBalance.split(' ')[1]
- var beforeDecimal = balance.split('.')[0]
- var afterDecimal = balance.split('.')[1]
- var shortBalance = shortenBalance(balance, decimalsToKeep)
-
- if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') {
- // eslint-disable-next-line eqeqeq
- if (afterDecimal == 0) {
- balance = '0'
- } else {
- balance = '<1.0e-5'
- }
- } else if (beforeDecimal !== '0') {
- balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}`
- }
-
- return { balance, label, shortBalance }
-}
-
-function shortenBalance (balance, decimalsToKeep = 1) {
- var truncatedValue
- var convertedBalance = parseFloat(balance)
- if (convertedBalance > 1000000) {
- truncatedValue = (balance / 1000000).toFixed(decimalsToKeep)
- return `${truncatedValue}m`
- } else if (convertedBalance > 1000) {
- truncatedValue = (balance / 1000).toFixed(decimalsToKeep)
- return `${truncatedValue}k`
- } else if (convertedBalance === 0) {
- return '0'
- } else if (convertedBalance < 0.001) {
- return '<0.001'
- } else if (convertedBalance < 1) {
- var stringBalance = convertedBalance.toString()
- if (stringBalance.split('.')[1].length > 3) {
- return convertedBalance.toFixed(3)
- } else {
- return stringBalance
- }
- } else {
- return convertedBalance.toFixed(decimalsToKeep)
- }
-}
-
-function dataSize (data) {
- var size = data ? ethUtil.stripHexPrefix(data).length : 0
- return size + ' bytes'
-}
-
-// Takes a BN and an ethereum currency name,
-// returns a BN in wei
-function normalizeToWei (amount, currency) {
- try {
- return amount.mul(bnTable.wei).div(bnTable[currency])
- } catch (e) {}
- return amount
-}
-
-function normalizeEthStringToWei (str) {
- const parts = str.split('.')
- let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei)
- if (parts[1]) {
- var decimal = parts[1]
- while (decimal.length < 18) {
- decimal += '0'
- }
- if (decimal.length > 18) {
- decimal = decimal.slice(0, 18)
- }
- const decimalBN = new ethUtil.BN(decimal, 10)
- eth = eth.add(decimalBN)
- }
- return eth
-}
-
-var multiple = new ethUtil.BN('10000', 10)
-function normalizeNumberToWei (n, currency) {
- var enlarged = n * 10000
- var amount = new ethUtil.BN(String(enlarged), 10)
- return normalizeToWei(amount, currency).div(multiple)
-}
-
-function readableDate (ms) {
- var date = new Date(ms)
- var month = date.getMonth()
- var day = date.getDate()
- var year = date.getFullYear()
- var hours = date.getHours()
- var minutes = '0' + date.getMinutes()
- var seconds = '0' + date.getSeconds()
-
- var dateStr = `${month}/${day}/${year}`
- var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}`
- return `${dateStr} ${time}`
-}
-
-function isHex (str) {
- return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/))
-}
-
-function bnMultiplyByFraction (targetBN, numerator, denominator) {
- const numBN = new ethUtil.BN(numerator)
- const denomBN = new ethUtil.BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
-
-function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) {
- const gasBn = hexToBn(gas)
- const gasPriceBn = hexToBn(gasPrice)
- const txFeeBn = gasBn.mul(gasPriceBn)
-
- return txFeeBn.toString(16)
-}
-
-function getContractAtAddress (tokenAddress) {
- return global.eth.contract(abi).at(tokenAddress)
-}
-
-function exportAsFile (filename, data, type = 'text/csv') {
- // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
- const blob = new Blob([data], {type})
- if (window.navigator.msSaveOrOpenBlob) {
- window.navigator.msSaveBlob(blob, filename)
- } else {
- const elem = window.document.createElement('a')
- elem.target = '_blank'
- elem.href = window.URL.createObjectURL(blob)
- elem.download = filename
- document.body.appendChild(elem)
- elem.click()
- document.body.removeChild(elem)
- }
-}
-
-function allNull (obj) {
- return Object.entries(obj).every(([key, value]) => value === null)
-}
-
-function getTokenAddressFromTokenObject (token) {
- return Object.values(token)[0].address.toLowerCase()
-}
-
-/**
- * Safely checksumms a potentially-null address
- *
- * @param {String} [address] - address to checksum
- * @param {String} [network] - network id
- * @returns {String} - checksummed address
- *
- */
-function checksumAddress (address, network) {
- const checksummed = address ? ethUtil.toChecksumAddress(address) : ''
- return checksummed
-}
-
-function addressSlicer (address = '') {
- if (address.length < 11) {
- return address
- }
-
- return `${address.slice(0, 6)}...${address.slice(-4)}`
-}
diff --git a/ui/index.js b/ui/index.js
index bd9ecc28b..ac860e0db 100644
--- a/ui/index.js
+++ b/ui/index.js
@@ -1,10 +1,10 @@
const render = require('react-dom').render
const h = require('react-hyperscript')
-const Root = require('./app/root')
-const actions = require('./app/actions')
-const configureStore = require('./app/store')
+const Root = require('./app/pages')
+const actions = require('./app/store/actions')
+const configureStore = require('./app/store/store')
const txHelper = require('./lib/tx-helper')
-const { fetchLocale } = require('./i18n-helper')
+const { fetchLocale } = require('./app/helpers/utils/i18n-helper')
const log = require('loglevel')
module.exports = launchMetamaskUi
diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js
index 7fadbceff..2ea943297 100644
--- a/ui/lib/icon-factory.js
+++ b/ui/lib/icon-factory.js
@@ -1,6 +1,6 @@
var iconFactory
const isValidAddress = require('ethereumjs-util').isValidAddress
-const { checksumAddress } = require('../app/util')
+const { checksumAddress } = require('../app/helpers/utils/util')
const contractMap = require('eth-contract-metadata')
module.exports = function (jazzicon) {
diff --git a/ui/lib/lost-accounts-notice.js b/ui/lib/lost-accounts-notice.js
index 948b13db6..840bd8dca 100644
--- a/ui/lib/lost-accounts-notice.js
+++ b/ui/lib/lost-accounts-notice.js
@@ -1,4 +1,4 @@
-const summary = require('../app/util').addressSummary
+const summary = require('../app/helpers/utils/util').addressSummary
module.exports = function (lostAccounts) {
return {
diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js
index 260dbaa39..cdacc5ed7 100644
--- a/ui/lib/tx-helper.js
+++ b/ui/lib/tx-helper.js
@@ -1,4 +1,4 @@
-const valuesFor = require('../app/util').valuesFor
+const valuesFor = require('../app/helpers/utils/util').valuesFor
const log = require('loglevel')
module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) {