aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/.gitignore66
-rw-r--r--ui/app/accounts/new-account/index.js87
-rw-r--r--ui/app/actions.js2507
-rw-r--r--ui/app/app.js359
-rw-r--r--ui/app/components/account-dropdowns.js325
-rw-r--r--ui/app/components/account-export.js138
-rw-r--r--ui/app/components/account-menu/index.js248
-rw-r--r--ui/app/components/account-panel.js86
-rw-r--r--ui/app/components/app-header/app-header.component.js136
-rw-r--r--ui/app/components/app-header/app-header.container.js38
-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.js1
-rw-r--r--ui/app/components/app/account-menu/index.scss177
-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.scss50
-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.scss27
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js69
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js1
-rwxr-xr-xui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss54
-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.js10
-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.js47
-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.js156
-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.js1
-rw-r--r--ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss133
-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.js1
-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.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss17
-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.js33
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js11
-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.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss28
-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.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/index.scss146
-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.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-price-button-group/index.scss238
-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.js108
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js354
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/index.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-price-chart/index.scss132
-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.js48
-rw-r--r--ui/app/components/app/gas-customization/gas-slider/index.js1
-rw-r--r--ui/app/components/app/gas-customization/gas-slider/index.scss54
-rw-r--r--ui/app/components/app/gas-customization/gas.selectors.js14
-rw-r--r--ui/app/components/app/gas-customization/index.scss7
-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.js1
-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.js57
-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.js241
-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.js1
-rw-r--r--ui/app/components/app/modals/loading-network-error/loading-network-error.component.js29
-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.js1
-rw-r--r--ui/app/components/app/modals/metametrics-opt-in-modal/index.scss30
-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.js149
-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.js65
-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.js27
-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.js187
-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.js11
-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.js31
-rw-r--r--ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js26
-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.js61
-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.js19
-rw-r--r--ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js104
-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.js62
-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.js1
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js27
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js12
-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/page-container/tests/page-container.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-component.test.js28
-rw-r--r--ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js28
-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.js24
-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.js166
-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.js59
-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.js232
-rw-r--r--ui/app/components/app/send/tests/send-selectors.test.js705
-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.scss112
-rw-r--r--ui/app/components/app/sidebars/sidebar.component.js69
-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.js97
-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.scss84
-rw-r--r--ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js101
-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.js335
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js1
-rw-r--r--ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js55
-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.js13
-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.js33
-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.scss63
-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.scss129
-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.scss46
-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.scss45
-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.scss92
-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.js1
-rw-r--r--ui/app/components/app/ui-migration-annoucement/index.scss22
-rw-r--r--ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js33
-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.js202
-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-component.js111
-rw-r--r--ui/app/components/button-group/button-group.component.js61
-rw-r--r--ui/app/components/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/button-group/tests/button-group-component.test.js97
-rw-r--r--ui/app/components/button/button.component.js45
-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-detail-row/index.scss46
-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.scss64
-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-header/index.scss27
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js137
-rw-r--r--ui/app/components/confirm-page-container/index.js8
-rw-r--r--ui/app/components/confirm-page-container/index.scss5
-rw-r--r--ui/app/components/copyable.js53
-rw-r--r--ui/app/components/currency-display/currency-display.component.js45
-rw-r--r--ui/app/components/currency-display/currency-display.container.js42
-rw-r--r--ui/app/components/currency-display/tests/currency-display.container.test.js145
-rw-r--r--ui/app/components/currency-input/currency-input.component.js121
-rw-r--r--ui/app/components/currency-input/currency-input.container.js28
-rw-r--r--ui/app/components/currency-input/index.scss7
-rw-r--r--ui/app/components/currency-input/tests/currency-input.component.test.js251
-rw-r--r--ui/app/components/currency-input/tests/currency-input.container.test.js60
-rw-r--r--ui/app/components/customize-gas-modal/index.js374
-rw-r--r--ui/app/components/dropdowns/account-details-dropdown.js109
-rw-r--r--ui/app/components/dropdowns/account-dropdown-mini.js75
-rw-r--r--ui/app/components/dropdowns/components/account-dropdowns.js473
-rw-r--r--ui/app/components/dropdowns/components/network-dropdown-icon.js31
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js361
-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/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.scss63
-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/menu-bar/menu-bar.component.js63
-rw-r--r--ui/app/components/menu-bar/menu-bar.container.js26
-rw-r--r--ui/app/components/modal/index.scss62
-rw-r--r--ui/app/components/modal/modal.component.js80
-rw-r--r--ui/app/components/modal/tests/modal.component.test.js103
-rw-r--r--ui/app/components/modals/account-details-modal.js100
-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.js68
-rw-r--r--ui/app/components/modals/cancel-transaction/cancel-transaction.container.js62
-rw-r--r--ui/app/components/modals/cancel-transaction/index.scss18
-rw-r--r--ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js56
-rw-r--r--ui/app/components/modals/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.js143
-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.js241
-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.scss9
-rw-r--r--ui/app/components/modals/modal.js483
-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/modals/welcome-beta/index.js1
-rw-r--r--ui/app/components/modals/welcome-beta/welcome-beta.component.js30
-rw-r--r--ui/app/components/modals/welcome-beta/welcome-beta.container.js4
-rw-r--r--ui/app/components/network-display/network-display.component.js76
-rw-r--r--ui/app/components/network.js142
-rw-r--r--ui/app/components/page-container/index.scss212
-rw-r--r--ui/app/components/page-container/page-container-footer/page-container-footer.component.js66
-rw-r--r--ui/app/components/page-container/page-container-header/page-container-header.component.js80
-rw-r--r--ui/app/components/page-container/page-container.component.js122
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js319
-rw-r--r--ui/app/components/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/components/pages/add-token/index.scss25
-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/authenticated.js34
-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.js412
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js203
-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.js157
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.container.js33
-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.js274
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js159
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js112
-rw-r--r--ui/app/components/pages/create-account/index.js113
-rw-r--r--ui/app/components/pages/create-account/new-account.js108
-rw-r--r--ui/app/components/pages/home/home.component.js87
-rw-r--r--ui/app/components/pages/home/home.container.js32
-rw-r--r--ui/app/components/pages/index.scss7
-rw-r--r--ui/app/components/pages/initialized.js25
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js189
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js177
-rw-r--r--ui/app/components/pages/metamask-route.js28
-rw-r--r--ui/app/components/pages/notice.js203
-rw-r--r--ui/app/components/pages/provider-approval/provider-approval.component.js28
-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.js544
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.container.js74
-rw-r--r--ui/app/components/pages/settings/settings.component.js54
-rw-r--r--ui/app/components/pages/unlock-page/index.scss52
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js179
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.container.js31
-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.js50
-rw-r--r--ui/app/components/qr-code.js63
-rw-r--r--ui/app/components/selected-account/selected-account.component.js54
-rw-r--r--ui/app/components/selected-account/selected-account.container.js13
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.component.js76
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.container.js17
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-component.test.js133
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-container.test.js34
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js60
-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.js22
-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/amount-max-button/tests/amount-max-button-utils.test.js27
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js120
-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-component.test.js188
-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-README.md0
-rw-r--r--ui/app/components/send/send-content/send-content.component.js41
-rw-r--r--ui/app/components/send/send-content/send-content.scss0
-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/from-dropdown/from-dropdown-README.md0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js46
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/index.js1
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js88
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row-README.md0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.component.js63
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.container.js46
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js121
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js110
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js58
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js53
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js48
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js27
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js14
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js70
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js70
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js49
-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.js43
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js72
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.container.js44
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js14
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.utils.js21
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js149
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js115
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js47
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js57
-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.js104
-rw-r--r--ui/app/components/send/send-footer/send-footer.container.js105
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-component.test.js232
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js196
-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.js212
-rw-r--r--ui/app/components/send/send.constants.js57
-rw-r--r--ui/app/components/send/send.container.js108
-rw-r--r--ui/app/components/send/send.selectors.js299
-rw-r--r--ui/app/components/send/send.utils.js330
-rw-r--r--ui/app/components/send/tests/send-component.test.js338
-rw-r--r--ui/app/components/send/tests/send-container.test.js174
-rw-r--r--ui/app/components/send/tests/send-selectors-test-data.js231
-rw-r--r--ui/app/components/send/tests/send-selectors.test.js705
-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/index.scss120
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js177
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.constants.js3
-rw-r--r--ui/app/components/shapeshift-form.js256
-rw-r--r--ui/app/components/shift-list-item.js209
-rw-r--r--ui/app/components/sidebars/index.scss74
-rw-r--r--ui/app/components/sidebars/sidebar.component.js49
-rw-r--r--ui/app/components/sidebars/tests/sidebars-component.test.js88
-rw-r--r--ui/app/components/signature-request.js307
-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.component.js23
-rw-r--r--ui/app/components/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/token-cell.js165
-rw-r--r--ui/app/components/token-currency-display/token-currency-display.component.js54
-rw-r--r--ui/app/components/token-input/tests/token-input.component.test.js308
-rw-r--r--ui/app/components/token-input/tests/token-input.container.test.js129
-rw-r--r--ui/app/components/token-input/token-input.component.js136
-rw-r--r--ui/app/components/token-input/token-input.container.js27
-rw-r--r--ui/app/components/token-list.js188
-rw-r--r--ui/app/components/tooltip-v2.js66
-rw-r--r--ui/app/components/transaction-action/transaction-action.component.js58
-rw-r--r--ui/app/components/transaction-activity-log/index.scss70
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js35
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js208
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.component.js96
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.container.js12
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.util.js93
-rw-r--r--ui/app/components/transaction-breakdown/index.scss23
-rw-r--r--ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js37
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js39
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown.component.js100
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown.container.js11
-rw-r--r--ui/app/components/transaction-list-item-details/index.scss49
-rw-r--r--ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js66
-rw-r--r--ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js111
-rw-r--r--ui/app/components/transaction-list-item/index.scss133
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js184
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js38
-rw-r--r--ui/app/components/transaction-list/index.scss46
-rw-r--r--ui/app/components/transaction-list/transaction-list.component.js119
-rw-r--r--ui/app/components/transaction-list/transaction-list.container.js51
-rw-r--r--ui/app/components/transaction-status/index.scss33
-rw-r--r--ui/app/components/transaction-status/transaction-status.component.js59
-rw-r--r--ui/app/components/transaction-view-balance/index.scss79
-rw-r--r--ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js71
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.component.js98
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.container.js32
-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.js1
-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.js1
-rw-r--r--ui/app/components/ui/breadcrumbs/breadcrumbs.component.js29
-rw-r--r--ui/app/components/ui/breadcrumbs/index.js1
-rw-r--r--ui/app/components/ui/breadcrumbs/index.scss15
-rw-r--r--ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js22
-rw-r--r--ui/app/components/ui/button-group/button-group.component.js73
-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.js111
-rw-r--r--ui/app/components/ui/button/button.component.js51
-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.js145
-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.scss26
-rw-r--r--ui/app/components/ui/currency-input/tests/currency-input.component.test.js345
-rw-r--r--ui/app/components/ui/currency-input/tests/currency-input.container.test.js170
-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.js1
-rw-r--r--ui/app/components/ui/lock-icon/lock-icon.component.js32
-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.scss219
-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.js68
-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.js83
-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.js131
-rw-r--r--ui/app/components/ui/page-container/tests/page-container.component.test.js (renamed from ui/app/components/send/account-list-item/account-list-item.scss)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.scss149
-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.js4
-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.scss14
-rw-r--r--ui/app/components/ui/token-balance/token-balance.component.js25
-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.js350
-rw-r--r--ui/app/components/ui/token-input/tests/token-input.container.test.js255
-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.js70
-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.scss55
-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/index.scss44
-rw-r--r--ui/app/components/unit-input/unit-input.component.js104
-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/tests/user-preferenced-currency-display.container.test.js115
-rw-r--r--ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js46
-rw-r--r--ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js54
-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.js230
-rw-r--r--ui/app/conf-tx.js224
-rw-r--r--ui/app/constants/common.js6
-rw-r--r--ui/app/constants/transactions.js23
-rw-r--r--ui/app/conversion-util.js251
-rw-r--r--ui/app/css/index.scss2
-rw-r--r--ui/app/css/itcss/base/index.scss7
-rw-r--r--ui/app/css/itcss/components/account-dropdown.scss29
-rw-r--r--ui/app/css/itcss/components/account-menu.scss153
-rw-r--r--ui/app/css/itcss/components/buttons.scss12
-rw-r--r--ui/app/css/itcss/components/gas-slider.scss51
-rw-r--r--ui/app/css/itcss/components/index.scss6
-rw-r--r--ui/app/css/itcss/components/loading-overlay.scss29
-rw-r--r--ui/app/css/itcss/components/network.scss2
-rw-r--r--ui/app/css/itcss/components/new-account.scss43
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss17
-rw-r--r--ui/app/css/itcss/components/send.scss40
-rw-r--r--ui/app/css/itcss/components/tab-bar.scss68
-rw-r--r--ui/app/css/itcss/components/welcome-screen.scss60
-rw-r--r--ui/app/css/itcss/generic/index.scss4
-rw-r--r--ui/app/css/itcss/settings/variables.scss4
-rw-r--r--ui/app/ducks/app/app.js788
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js411
-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/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/send.duck.js79
-rw-r--r--ui/app/ducks/send/send-duck.test.js186
-rw-r--r--ui/app/ducks/send/send.duck.js106
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js685
-rw-r--r--ui/app/ducks/tests/send-duck.test.js155
-rw-r--r--ui/app/first-time/init-menu.js224
-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.js24
-rw-r--r--ui/app/helpers/conversions.util.js84
-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.js12
-rw-r--r--ui/app/helpers/higher-order-components/authenticated/index.js1
-rw-r--r--ui/app/helpers/higher-order-components/i18n-provider.js55
-rw-r--r--ui/app/helpers/higher-order-components/initialized/index.js1
-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.js12
-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.js159
-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.js251
-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.js3
-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.js184
-rw-r--r--ui/app/helpers/utils/token-util.js118
-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/with-method-data/with-method-data.component.js52
-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/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.js14
-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.js12
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/index.js1
-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.js1
-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.js12
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.scss53
-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.js20
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/index.js1
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/index.scss159
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.js1
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.scss136
-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.js41
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss48
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.js1
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss57
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.scss88
-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.js1
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.scss42
-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.scss197
-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.js1
-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.scss51
-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.js92
-rw-r--r--ui/app/reducers/app.js778
-rw-r--r--ui/app/reducers/locale.js17
-rw-r--r--ui/app/reducers/metamask.js384
-rw-r--r--ui/app/root.js23
-rw-r--r--ui/app/routes.js68
-rw-r--r--ui/app/select-app.js72
-rw-r--r--ui/app/selectors.js207
-rw-r--r--ui/app/selectors/confirm-transaction.js15
-rw-r--r--ui/app/selectors/custom-gas.js325
-rw-r--r--ui/app/selectors/custom-gas.test.js595
-rw-r--r--ui/app/selectors/selectors.js313
-rw-r--r--ui/app/selectors/transactions.js254
-rw-r--r--ui/app/store.js21
-rw-r--r--ui/app/store/actions.js2765
-rw-r--r--ui/app/store/store.js21
-rw-r--r--ui/app/token-util.js118
-rw-r--r--ui/app/util.js314
-rw-r--r--ui/app/welcome-screen.js83
-rw-r--r--ui/css.js1
-rw-r--r--ui/index.js8
-rw-r--r--ui/lib/icon-factory.js2
-rw-r--r--ui/lib/local-storage-helpers.js20
-rw-r--r--ui/lib/lost-accounts-notice.js2
-rw-r--r--ui/lib/shallow-with-context.js7
-rw-r--r--ui/lib/test-timeout.js5
-rw-r--r--ui/lib/tx-helper.js4
1011 files changed, 48497 insertions, 34534 deletions
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 501fef76d..000000000
--- a/ui/app/actions.js
+++ /dev/null
@@ -1,2507 +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 {
- calcGasTotal,
- 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 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',
- 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,
- 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_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,
- updateSendFrom,
- updateSendHexData,
- updateSendTo,
- updateSendAmount,
- updateSendMemo,
- setMaxModeTo,
- updateSend,
- updateSendErrors,
- 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',
- showConfigPage,
- SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
- SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
- showAddTokenPage,
- showAddSuggestedTokenPage,
- addToken,
- addTokens,
- removeToken,
- updateTokens,
- removeSuggestedTokens,
- UPDATE_TOKENS: 'UPDATE_TOKENS',
- setRpcTarget: setRpcTarget,
- delRpcTarget: delRpcTarget,
- setProviderType: setProviderType,
- SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
- setHardwareWalletDefaultHdPath,
- 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,
-
- // 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,
-
- 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,
- approveProviderRequest,
- rejectProviderRequest,
- clearApprovedOrigins,
-}
-
-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())
- })
- }
-}
-
-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 revealSeedConfirmation () {
- return {
- type: this.REVEAL_SEED_CONFIRMATION,
- }
-}
-
-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 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 ({
- blockGasLimit,
- recentBlocks,
- selectedAddress,
- selectedToken,
- to,
- value,
- data,
-}) {
- return (dispatch) => {
- dispatch(actions.gasLoadingStarted())
- return new Promise((resolve, reject) => {
- background.getGasPrice((err, data) => {
- if (err) return reject(err)
- return resolve(data)
- })
- })
- .then(estimateGasPrice => {
- return Promise.all([
- Promise.resolve(estimateGasPrice),
- estimateGas({
- estimateGasMethod: background.estimateGas,
- blockGasLimit,
- selectedAddress,
- selectedToken,
- to,
- value,
- estimateGasPrice,
- data,
- }),
- ])
- })
- .then(([gasPrice, gas]) => {
- dispatch(actions.setGasPrice(gasPrice))
- dispatch(actions.setGasLimit(gas))
- return calcGasTotal(gas, gasPrice)
- })
- .then((gasEstimate) => {
- dispatch(actions.setGasTotal(gasEstimate))
- 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.toString(10)))
- }
- })
- .catch(err => {
- log.error(err)
- updateSendErrors({ tokenBalance: 'tokenBalanceError' })
- })
- }
-}
-
-function updateSendErrors (errorObject) {
- return {
- type: actions.UPDATE_SEND_ERRORS,
- value: errorObject,
- }
-}
-
-function setSendTokenBalance (tokenBalance) {
- return {
- type: actions.UPDATE_SEND_TOKEN_BALANCE,
- value: tokenBalance,
- }
-}
-
-function updateSendFrom (from) {
- return {
- type: actions.UPDATE_SEND_FROM,
- value: from,
- }
-}
-
-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 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) {
- log.debug(`background.retryTransaction`)
- let newTxId
-
- return (dispatch) => {
- return new Promise((resolve, reject) => {
- background.retryTransaction(txId, (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)
- }
-}
-
-//
-// config
-//
-
-function setProviderType (type) {
- return (dispatch) => {
- 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(actions.updateProviderType(type))
- dispatch(actions.setSelectedToken())
- })
-
- }
-}
-
-function updateProviderType (type) {
- return {
- type: actions.SET_PROVIDER_TYPE,
- value: type,
- }
-}
-
-function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname = '') {
- return (dispatch) => {
- log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
- background.setCustomRpc(newRpc, chainId, ticker, nickname, (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 }) {
- return {
- type: actions.SIDEBAR_OPEN,
- value: {
- transitionName,
- type,
- },
- }
-}
-
-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 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 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 (origin) {
- return (dispatch) => {
- background.approveProviderRequest(origin)
- }
-}
-
-function rejectProviderRequest (origin) {
- return (dispatch) => {
- background.rejectProviderRequest(origin)
- }
-}
-
-function clearApprovedOrigins () {
- return (dispatch) => {
- background.clearApprovedOrigins()
- }
-}
diff --git a/ui/app/app.js b/ui/app/app.js
deleted file mode 100644
index b3aff1f39..000000000
--- a/ui/app/app.js
+++ /dev/null
@@ -1,359 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const { Route, Switch, withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const h = require('react-hyperscript')
-const actions = require('./actions')
-const classnames = require('classnames')
-const log = require('loglevel')
-
-// init
-const InitializeScreen = require('../../mascara/src/app/first-time').default
-// accounts
-const SendTransactionScreen = require('./components/send/send.container')
-const ConfirmTransaction = require('./components/pages/confirm-transaction')
-
-// slideout menu
-const Sidebar = require('./components/sidebars').default
-
-// other views
-import Home from './components/pages/home'
-import Settings from './components/pages/settings'
-const Authenticated = require('./components/pages/authenticated')
-const Initialized = require('./components/pages/initialized')
-const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
-const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
-const AddTokenPage = require('./components/pages/add-token')
-const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
-const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
-const CreateAccountPage = require('./components/pages/create-account')
-const NoticeScreen = require('./components/pages/notice')
-
-const Loading = require('./components/loading-screen')
-const NetworkDropdown = require('./components/dropdowns/network-dropdown')
-const AccountMenu = require('./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'
-
-// Routes
-const {
- DEFAULT_ROUTE,
- UNLOCK_ROUTE,
- SETTINGS_ROUTE,
- REVEAL_SEED_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,
- NOTICE_ROUTE,
-} = require('./routes')
-
-class App extends Component {
- componentWillMount () {
- const { currentCurrency, setCurrentCurrencyToUSD } = this.props
-
- if (!currentCurrency) {
- setCurrentCurrencyToUSD()
- }
- }
-
- renderRoutes () {
- const exact = true
-
- return (
- h(Switch, [
- h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
- h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
- h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
- h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
- h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
- h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
- h(Authenticated, {
- path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
- component: ConfirmTransaction,
- }),
- h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
- h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
- h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
- h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
- h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
- h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
- ])
- )
- }
-
- render () {
- const {
- isLoading,
- alertMessage,
- loadingMessage,
- network,
- isMouseUser,
- provider,
- frequentRpcListDetail,
- currentView,
- setMouseUserState,
- sidebar,
- } = this.props
- const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
- const loadMessage = loadingMessage || isLoadingNetwork ?
- this.getConnectingLabel(loadingMessage) : null
- log.debug('Main ui render function')
-
- return (
- h('.flex-column.full-height', {
- className: classnames({ 'mouse-user-styles': isMouseUser }),
- style: {
- overflowX: 'hidden',
- position: 'relative',
- alignItems: 'center',
- },
- tabIndex: '0',
- onClick: () => setMouseUserState(true),
- onKeyDown: (e) => {
- if (e.keyCode === 9) {
- setMouseUserState(false)
- }
- },
- }, [
-
- // global modal
- h(Modal, {}, []),
-
- // global alert
- h(Alert, {visible: this.props.alertOpen, msg: alertMessage}),
-
- h(AppHeader),
-
- // sidebar
- h(Sidebar, {
- sidebarOpen: sidebar.isOpen,
- hideSidebar: this.props.hideSidebar,
- transitionName: sidebar.transitionName,
- type: sidebar.type,
- }),
-
- // network dropdown
- h(NetworkDropdown, {
- provider,
- frequentRpcListDetail,
- }, []),
-
- h(AccountMenu),
-
- h('div.main-container-wrapper', [
- (isLoading || isLoadingNetwork) && h(Loading, {
- loadingMessage: loadMessage,
- }),
-
- // content
- this.renderRoutes(),
- ]),
- ])
- )
- }
-
- 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 } = 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('connectingToUnknown')
- }
-
- 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,
- isMascara: PropTypes.bool,
- 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,
- unapprovedMsgCount: PropTypes.number,
- unapprovedPersonalMsgCount: PropTypes.number,
- unapprovedTypedMessagesCount: PropTypes.number,
- welcomeScreenSeen: PropTypes.bool,
- isPopup: PropTypes.bool,
- betaUI: PropTypes.bool,
- isMouseUser: PropTypes.bool,
- setMouseUserState: PropTypes.func,
- t: PropTypes.func,
-}
-
-function mapStateToProps (state) {
- const { appState, metamask } = state
- const {
- networkDropdownOpen,
- sidebar,
- alertOpen,
- alertMessage,
- isLoading,
- loadingMessage,
- } = appState
-
- const {
- identities,
- accounts,
- address,
- keyrings,
- isInitialized,
- noActiveNotices,
- seedWords,
- unapprovedTxs,
- nextUnreadNotice,
- lostAccounts,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = 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,
- isMascara: state.metamask.isMascara,
- isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
- isPopup: state.metamask.isPopup,
- seedWords: state.metamask.seedWords,
- unapprovedTxs,
- unapprovedMsgs: state.metamask.unapprovedMsgs,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- menuOpen: state.appState.menuOpen,
- network: state.metamask.network,
- provider: state.metamask.provider,
- forgottenPassword: state.appState.forgottenPassword,
- nextUnreadNotice,
- lostAccounts,
- frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
- currentCurrency: state.metamask.currentCurrency,
- isMouseUser: state.appState.isMouseUser,
- betaUI: state.metamask.featureFlags.betaUI,
- isRevealingSeedWords: state.metamask.isRevealingSeedWords,
- Qr: state.appState.Qr,
- welcomeScreenSeen: state.metamask.welcomeScreenSeen,
-
- // state needed to get account dropdown temporarily rendering from app bar
- identities,
- selected,
- keyrings,
- }
-}
-
-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,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(App)
diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js
deleted file mode 100644
index 06376e48b..000000000
--- a/ui/app/components/account-dropdowns.js
+++ /dev/null
@@ -1,325 +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 { 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) => {
- 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,
-}
-
-module.exports = {
- AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
-}
diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js
deleted file mode 100644
index 865207487..000000000
--- a/ui/app/components/account-export.js
+++ /dev/null
@@ -1,138 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const inherits = require('util').inherits
-const exportAsFile = require('../util').exportAsFile
-const copyToClipboard = require('copy-to-clipboard')
-const actions = require('../actions')
-const ethUtil = require('ethereumjs-util')
-const connect = require('react-redux').connect
-
-ExportAccountView.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(ExportAccountView)
-
-
-inherits(ExportAccountView, Component)
-function ExportAccountView () {
- Component.call(this)
-}
-
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- }
-}
-
-ExportAccountView.prototype.render = function () {
- const state = this.props
- const accountDetail = state.accountDetail
- const nickname = state.identities[state.address].name
-
- if (!accountDetail) return h('div')
- const accountExport = accountDetail.accountExport
-
- const notExporting = accountExport === 'none'
- const exportRequested = accountExport === 'requested'
- const accountExported = accountExport === 'completed'
-
- if (notExporting) return h('div')
-
- if (exportRequested) {
- const warning = this.context.t('exportPrivateKeyWarning')
- return (
- h('div', {
- style: {
- display: 'inline-block',
- textAlign: 'center',
- },
- },
- [
- h('div', {
- key: 'exporting',
- style: {
- margin: '0 20px',
- },
- }, [
- h('p.error', warning),
- h('input#exportAccount.sizing-input', {
- type: 'password',
- placeholder: this.context.t('confirmPassword').toLowerCase(),
- onKeyPress: this.onExportKeyPress.bind(this),
- style: {
- position: 'relative',
- top: '1.5px',
- marginBottom: '7px',
- },
- }),
- ]),
- h('div', {
- key: 'buttons',
- style: {
- margin: '0 20px',
- },
- },
- [
- h('button', {
- onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
- style: {
- marginRight: '10px',
- },
- }, this.context.t('submit')),
- h('button', {
- onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
- }, this.context.t('cancel')),
- ]),
- (this.props.warning) && (
- h('span.error', {
- style: {
- margin: '20px',
- },
- }, this.props.warning.split('-'))
- ),
- ])
- )
- }
-
- if (accountExported) {
- const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
-
- return h('div.privateKey', {
- style: {
- margin: '0 20px',
- },
- }, [
- h('label', this.context.t('copyPrivateKey') + ':'),
- h('p.error.cursor-pointer', {
- style: {
- textOverflow: 'ellipsis',
- overflow: 'hidden',
- webkitUserSelect: 'text',
- maxWidth: '275px',
- },
- onClick: function (event) {
- copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
- },
- }, plainKey),
- h('button', {
- onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
- }, this.context.t('done')),
- h('button', {
- style: {
- marginLeft: '10px',
- },
- onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey),
- }, this.context.t('saveAsFile')),
- ])
- }
-}
-
-ExportAccountView.prototype.onExportKeyPress = function (event) {
- if (event.key !== 'Enter') return
- event.preventDefault()
-
- const input = document.getElementById('exportAccount').value
- this.props.dispatch(actions.exportAccount(input, this.props.address))
-}
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
deleted file mode 100644
index 94eae8d07..000000000
--- a/ui/app/components/account-menu/index.js
+++ /dev/null
@@ -1,248 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const { compose } = require('recompose')
-const { withRouter } = require('react-router-dom')
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const actions = require('../../actions')
-const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
-const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
-const Tooltip = require('../tooltip')
-import Identicon from '../identicon'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import { PRIMARY } from '../../constants/common'
-
-const {
- SETTINGS_ROUTE,
- INFO_ROUTE,
- NEW_ACCOUNT_ROUTE,
- IMPORT_ACCOUNT_ROUTE,
- CONNECT_HARDWARE_ROUTE,
- DEFAULT_ROUTE,
-} = require('../../routes')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(AccountMenu)
-
-AccountMenu.contextTypes = {
- t: PropTypes.func,
-}
-
-inherits(AccountMenu, Component)
-function AccountMenu () { Component.call(this) }
-
-function mapStateToProps (state) {
- return {
- selectedAddress: state.metamask.selectedAddress,
- isAccountMenuOpen: state.metamask.isAccountMenuOpen,
- keyrings: state.metamask.keyrings,
- identities: state.metamask.identities,
- accounts: state.metamask.accounts,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
- showAccountDetail: address => {
- dispatch(actions.showAccountDetail(address))
- dispatch(actions.hideSidebar())
- dispatch(actions.toggleAccountMenu())
- },
- lockMetamask: () => {
- dispatch(actions.lockMetamask())
- dispatch(actions.hideWarning())
- dispatch(actions.hideSidebar())
- dispatch(actions.toggleAccountMenu())
- },
- showConfigPage: () => {
- dispatch(actions.showConfigPage())
- dispatch(actions.hideSidebar())
- dispatch(actions.toggleAccountMenu())
- },
- showInfoPage: () => {
- dispatch(actions.showInfoPage())
- dispatch(actions.hideSidebar())
- dispatch(actions.toggleAccountMenu())
- },
- showRemoveAccountConfirmationModal: (identity) => {
- return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
- },
- }
-}
-
-AccountMenu.prototype.render = function () {
- const {
- isAccountMenuOpen,
- toggleAccountMenu,
- lockMetamask,
- history,
- } = this.props
-
- return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [
- h(CloseArea, { onClick: toggleAccountMenu }),
- h(Item, {
- className: 'account-menu__header',
- }, [
- this.context.t('myAccounts'),
- h('button.account-menu__logout-button', {
- onClick: () => {
- lockMetamask()
- history.push(DEFAULT_ROUTE)
- },
- }, this.context.t('logout')),
- ]),
- h(Divider),
- h('div.account-menu__accounts', this.renderAccounts()),
- h(Divider),
- h(Item, {
- onClick: () => {
- toggleAccountMenu()
- history.push(NEW_ACCOUNT_ROUTE)
- },
- icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }),
- text: this.context.t('createAccount'),
- }),
- h(Item, {
- onClick: () => {
- toggleAccountMenu()
- history.push(IMPORT_ACCOUNT_ROUTE)
- },
- icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
- text: this.context.t('importAccount'),
- }),
- h(Item, {
- onClick: () => {
- toggleAccountMenu()
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
- } else {
- history.push(CONNECT_HARDWARE_ROUTE)
- }
- },
- icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
- text: this.context.t('connectHardwareWallet'),
- }),
- h(Divider),
- h(Item, {
- onClick: () => {
- toggleAccountMenu()
- history.push(INFO_ROUTE)
- },
- icon: h('img', { src: 'images/mm-info-icon.svg' }),
- text: this.context.t('infoHelp'),
- }),
- h(Item, {
- onClick: () => {
- toggleAccountMenu()
- history.push(SETTINGS_ROUTE)
- },
- icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }),
- text: this.context.t('settings'),
- }),
- ])
-}
-
-AccountMenu.prototype.renderAccounts = function () {
- 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 h(
- 'div.account-menu__account.menu__item--clickable',
- { onClick: () => showAccountDetail(identity.address) },
- [
- h('div.account-menu__check-mark', [
- isSelected ? h('div.account-menu__check-mark-icon') : null,
- ]),
-
- h(
- Identicon,
- {
- address: identity.address,
- diameter: 24,
- },
- ),
-
- h('div.account-menu__account-info', [
- h('div.account-menu__name', identity.name || ''),
- h(UserPreferencedCurrencyDisplay, {
- className: 'account-menu__balance',
- value: balanceValue,
- type: PRIMARY,
- }),
- ]),
-
- this.renderKeyringType(keyring),
- this.renderRemoveAccount(keyring, identity),
- ],
- )
- })
-}
-
-AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) {
- // Any account that's not from the HD wallet Keyring can be removed
- const type = keyring.type
- const isRemovable = type !== 'HD Key Tree'
- if (isRemovable) {
- return h(Tooltip, {
- title: this.context.t('removeAccount'),
- position: 'bottom',
- }, [
- h('a.remove-account-icon', {
- onClick: (e) => this.removeAccount(e, identity),
- }, ''),
- ])
- }
- return null
-}
-
-AccountMenu.prototype.removeAccount = function (e, identity) {
- e.preventDefault()
- e.stopPropagation()
- const { showRemoveAccountConfirmationModal } = this.props
- showRemoveAccountConfirmationModal(identity)
-}
-
-AccountMenu.prototype.renderKeyringType = function (keyring) {
- try { // Sometimes keyrings aren't loaded yet:
- const type = keyring.type
- let label
- switch (type) {
- case 'Trezor Hardware':
- case 'Ledger Hardware':
- label = this.context.t('hardware')
- break
- case 'Simple Key Pair':
- label = this.context.t('imported')
- break
- default:
- label = ''
- }
-
- return label !== '' ? h('.keyring-label.allcaps', label) : null
-
- } catch (e) { return }
-}
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 c82dc1de9..000000000
--- a/ui/app/components/app-header/app-header.component.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { matchPath } from 'react-router-dom'
-import Identicon from '../identicon'
-
-const {
- ENVIRONMENT_TYPE_NOTIFICATION,
- ENVIRONMENT_TYPE_POPUP,
-} = require('../../../../app/scripts/lib/enums')
-const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
-const NetworkIndicator = require('../network')
-
-export default class AppHeader extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- location: 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,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- handleNetworkIndicatorClick (event) {
- event.preventDefault()
- event.stopPropagation()
-
- const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
-
- return networkDropdownOpen === false
- ? showNetworkDropdown()
- : hideNetworkDropdown()
- }
-
- isConfirming () {
- const { location } = this.props
-
- return Boolean(matchPath(location.pathname, {
- path: CONFIRM_TRANSACTION_ROUTE, exact: false,
- }))
- }
-
- renderAccountMenu () {
- const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
-
- return isUnlocked && (
- <div
- className={classnames('account-menu__icon', {
- 'account-menu__icon--disabled': this.isConfirming(),
- })}
- onClick={() => this.isConfirming() || toggleAccountMenu()}
- >
- <Identicon
- address={selectedAddress}
- diameter={32}
- />
- </div>
- )
- }
-
- hideAppHeader () {
- const { location } = this.props
-
- const isInitializing = Boolean(matchPath(location.pathname, {
- path: INITIALIZE_ROUTE, exact: false,
- }))
-
- if (isInitializing) {
- return true
- }
-
- if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
- return true
- }
-
- if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) {
- return true
- }
- }
-
- render () {
- const {
- network,
- provider,
- history,
- isUnlocked,
- } = this.props
-
- if (this.hideAppHeader()) {
- return null
- }
-
- 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-beta.svg"
- height={30}
- />
- <img
- className="app-header__metafox-logo app-header__metafox-logo--icon"
- src="/images/logo/metamask-fox.svg"
- height={42}
- width={42}
- />
- </div>
- <div className="app-header__account-menu-container">
- <div className="app-header__network-component-wrapper">
- <NetworkIndicator
- network={network}
- provider={provider}
- onClick={event => this.handleNetworkIndicatorClick(event)}
- disabled={this.isConfirming()}
- />
- </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 30d3f8cc4..000000000
--- a/ui/app/components/app-header/app-header.container.js
+++ /dev/null
@@ -1,38 +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,
- } = metamask
-
- return {
- networkDropdownOpen,
- network,
- provider,
- selectedAddress,
- isUnlocked,
- }
-}
-
-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/app/account-menu/index.js b/ui/app/components/app/account-menu/index.js
new file mode 100644
index 000000000..b2b4e4c6f
--- /dev/null
+++ b/ui/app/components/app/account-menu/index.js
@@ -0,0 +1 @@
+export { default } from './account-menu.container'
diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss
new file mode 100644
index 000000000..9a61bf887
--- /dev/null
+++ b/ui/app/components/app/account-menu/index.scss
@@ -0,0 +1,177 @@
+.account-menu {
+ position: fixed;
+ z-index: 100;
+ top: 58px;
+ width: 310px;
+
+ @media screen and (max-width: 575px) {
+ right: calc(((100vw - 100%) / 2) + 8px);
+ }
+
+ @media screen and (min-width: 576px) {
+ right: calc((100vw - 85vw) / 2);
+ }
+
+ @media screen and (min-width: 769px) {
+ right: calc((100vw - 80vw) / 2);
+ }
+
+ @media screen and (min-width: 1281px) {
+ right: calc((100vw - 65vw) / 2);
+ }
+
+ &__icon {
+ margin-left: 20px;
+ cursor: pointer;
+
+ &--disabled {
+ cursor: initial;
+ }
+ }
+
+ &__header {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__logout-button {
+ border: 1px solid $dusty-gray;
+ background-color: transparent;
+ color: $white;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 23px;
+ padding: 0 24px;
+ }
+
+ &__item-icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ &__accounts {
+ display: flex;
+ flex-flow: column nowrap;
+ overflow-y: auto;
+ max-height: 256px;
+ position: relative;
+ z-index: 200;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ @media screen and (max-width: 575px) {
+ max-height: 228px;
+ }
+
+ .keyring-label {
+ margin-top: 5px;
+ background-color: $dusty-gray;
+ color: $black;
+ font-weight: normal;
+ letter-spacing: .5px;
+ }
+ }
+
+ &__account {
+ display: flex;
+ flex-flow: row nowrap;
+ padding: 16px 14px;
+ flex: 0 0 auto;
+
+ @media screen and (max-width: 575px) {
+ padding: 12px 14px;
+ }
+
+ .remove-account-icon {
+ width: 15px;
+ margin-left: 10px;
+ height: 15px;
+ }
+
+ &:hover {
+ .remove-account-icon::after {
+ content: '\00D7';
+ font-size: 25px;
+ color: $white;
+ cursor: pointer;
+ position: absolute;
+ margin-top: -5px;
+ }
+ }
+ }
+
+ &__account-info {
+ flex: 1 0 auto;
+ display: flex;
+ flex-flow: column nowrap;
+ }
+
+ &__check-mark {
+ width: 14px;
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__check-mark-icon {
+ background-image: url("images/check-white.svg");
+ height: 18px;
+ width: 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ margin: 3px 0;
+ }
+
+ .identicon {
+ margin: 0 12px 0 0;
+ flex: 0 0 auto;
+ }
+
+ &__name {
+ color: $white;
+ font-size: 18px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 200px;
+ }
+
+ &__balance {
+ color: $dusty-gray;
+ font-size: 14px;
+ }
+
+ &__action {
+ font-size: 16px;
+ line-height: 18px;
+ cursor: pointer;
+ }
+
+ &__accounts-container {
+ position: relative;
+ }
+
+ &__scroll-button {
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+ height: 28px;
+ width: 28px;
+ border-radius: 14px;
+ background: #3f3f3f;
+ z-index: 201;
+ cursor: pointer;
+ opacity: .8;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
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/app/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss
new file mode 100644
index 000000000..1672ef8c6
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss
@@ -0,0 +1,50 @@
+.confirm-detail-row {
+ padding: 14px 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ &__label {
+ font-size: .75rem;
+ font-weight: 500;
+ color: $scorpion;
+ text-transform: uppercase;
+ }
+
+ &__details {
+ flex: 1;
+ text-align: end;
+ min-width: 0;
+ }
+
+ &__primary {
+ font-size: 1.5rem;
+ justify-content: flex-end;
+ }
+
+ &__secondary {
+ color: $oslo-gray;
+ justify-content: flex-end;
+ }
+
+ &__header-text {
+ font-size: .75rem;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ color: $scorpion;
+
+ &--edit {
+ color: $curious-blue;
+ cursor: pointer;
+ }
+
+ &--total {
+ font-size: .625rem;
+ }
+ }
+
+ .advanced-gas-inputs__gas-edit-rows {
+ margin-bottom: 16px;
+ }
+}
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/app/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss
new file mode 100644
index 000000000..be77edbdf
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss
@@ -0,0 +1,27 @@
+.confirm-page-container-header {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+
+ &__row {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid $geyser;
+ padding: 4px 13px 4px 13px;
+ flex: 0 0 auto;
+ }
+
+ &__back-button-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/app/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
new file mode 100755
index 000000000..8327f997b
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js
@@ -0,0 +1,69 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const ConfirmPageContainerNavigation = props => {
+ const { onNextTx, totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = props
+
+ return (
+ <div className="confirm-page-container-navigation"
+ style={{
+ display: showNavigation ? 'flex' : 'none',
+ }}
+ >
+ <div className="confirm-page-container-navigation__container"
+ style={{
+ visibility: prevTxId ? 'initial' : 'hidden',
+ }}>
+ <div
+ className="confirm-page-container-navigation__arrow"
+ onClick={() => onNextTx(firstTx)}>
+ <img src="/images/double-arrow.svg" />
+ </div>
+ <div
+ className="confirm-page-container-navigation__arrow"
+ onClick={() => onNextTx(prevTxId)}>
+ <img src="/images/single-arrow.svg" />
+ </div>
+ </div>
+ <div className="confirm-page-container-navigation__textcontainer">
+ <div className="confirm-page-container-navigation__navtext">
+ {positionOfCurrentTx} {ofText} {totalTx}
+ </div>
+ <div className="confirm-page-container-navigation__longtext">
+ {requestsWaitingText}
+ </div>
+ </div>
+ <div
+ className="confirm-page-container-navigation__container"
+ style={{
+ visibility: nextTxId ? 'initial' : 'hidden',
+ }}>
+ <div
+ className="confirm-page-container-navigation__arrow"
+ onClick={() => onNextTx(nextTxId)}>
+ <img className="confirm-page-container-navigation__imageflip" src="/images/single-arrow.svg" />
+ </div>
+ <div
+ className="confirm-page-container-navigation__arrow"
+ onClick={() => onNextTx(lastTx)}>
+ <img className="confirm-page-container-navigation__imageflip" src="/images/double-arrow.svg" />
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmPageContainerNavigation.propTypes = {
+ totalTx: PropTypes.number,
+ positionOfCurrentTx: PropTypes.number,
+ onNextTx: PropTypes.func,
+ nextTxId: PropTypes.string,
+ prevTxId: PropTypes.string,
+ showNavigation: PropTypes.bool,
+ firstTx: PropTypes.string,
+ lastTx: PropTypes.string,
+ ofText: PropTypes.string,
+ requestsWaitingText: PropTypes.string,
+}
+
+export default ConfirmPageContainerNavigation
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js
new file mode 100755
index 000000000..d97c1b447
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-navigation.component'
diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss
new file mode 100755
index 000000000..0cf184c60
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss
@@ -0,0 +1,54 @@
+.confirm-page-container-navigation {
+ display: flex;
+ justify-content: space-between;
+ font: inherit;
+ padding: 4px 10px 4px 10px;
+ border-bottom: 1px solid $geyser;
+ flex: 0 0 auto;
+
+ &__container {
+ display: flex;
+ }
+
+ &__arrow {
+ cursor: pointer;
+ display: flex;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ &__arrow:hover {
+ -webkit-transform: scale(1.1);
+ -moz-transform: scale(1.1);
+ -o-transform: scale(1.1);
+ transform: scale(1.1);
+ }
+
+ &__arrow:active {
+ -webkit-transform: scale(0.95);
+ -moz-transform: scale(0.95);
+ -o-transform: scale(0.95);
+ transform: scale(0.95);
+ }
+
+ &__textcontainer {
+ text-align: center;
+ }
+
+ &__navtext {
+ font-size: 9px;
+ font-weight: bold;
+ }
+
+ &__longtext {
+ color: $oslo-gray;
+ font-size: 8px;
+ }
+
+ &__imageflip {
+ -webkit-transform: scaleX(-1);
+ -moz-transform: scaleX(-1);
+ -o-transform: scaleX(-1);
+ transform: scaleX(-1);
+ }
+} \ No newline at end of file
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/app/confirm-page-container/index.js b/ui/app/components/app/confirm-page-container/index.js
new file mode 100644
index 000000000..28b17614e
--- /dev/null
+++ b/ui/app/components/app/confirm-page-container/index.js
@@ -0,0 +1,10 @@
+export { default } from './confirm-page-container.component'
+export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
+export { default as ConfirmDetailRow } from './confirm-detail-row'
+export { default as ConfirmPageContainerNavigation } from './confirm-page-container-navigation'
+
+export {
+ default as ConfirmPageContainerContent,
+ ConfirmPageContainerSummary,
+ ConfirmPageContainerError,
+} from './confirm-page-container-content'
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/app/dropdowns/components/network-dropdown-icon.js b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js
new file mode 100644
index 000000000..d4a2c2ff7
--- /dev/null
+++ b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js
@@ -0,0 +1,47 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+
+
+inherits(NetworkDropdownIcon, Component)
+module.exports = NetworkDropdownIcon
+
+function NetworkDropdownIcon () {
+ Component.call(this)
+}
+
+NetworkDropdownIcon.prototype.render = function () {
+ const {
+ backgroundColor,
+ isSelected,
+ innerBorder = 'none',
+ diameter = '12',
+ loading,
+ } = this.props
+
+ return loading
+ ? h('span.pointer.network-indicator', {
+ style: {
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ }, [
+ h('img', {
+ style: {
+ width: '27px',
+ },
+ src: 'images/loading.svg',
+ }),
+ ])
+ : h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
+ h('div', {
+ style: {
+ background: backgroundColor,
+ border: innerBorder,
+ height: `${diameter}px`,
+ width: `${diameter}px`,
+ },
+ })
+ )
+}
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/app/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
new file mode 100644
index 000000000..95894140c
--- /dev/null
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
@@ -0,0 +1,156 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+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,
+ insufficientBalance: PropTypes.bool,
+ customPriceIsSafe: PropTypes.bool,
+ isSpeedUp: PropTypes.bool,
+ showGasPriceInfoModal: PropTypes.func,
+ showGasLimitInfoModal: PropTypes.func,
+ }
+
+ debouncedGasLimitReset = debounce((dVal) => {
+ if (dVal < 21000) {
+ this.props.updateCustomGasLimit(21000)
+ }
+ }, 1000, { trailing: true })
+
+ onChangeGasLimit = (val) => {
+ this.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-gas-inputs__gas-edit-row__input-wrapper">
+ <input
+ className={classnames('advanced-gas-inputs__gas-edit-row__input', {
+ 'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
+ 'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
+ })}
+ type="number"
+ value={value}
+ onChange={event => onChange(Number(event.target.value))}
+ />
+ <div className={classnames('advanced-gas-inputs__gas-edit-row__input-arrows', {
+ 'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
+ 'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
+ })}>
+ <div
+ className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
+ onClick={() => onChange(value + 1)}
+ >
+ <i className="fa fa-sm fa-angle-up" />
+ </div>
+ <div
+ className="advanced-gas-inputs__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-gas-inputs__gas-edit-row__${errorType}-text`}>
+ { errorText }
+ </div>
+ : null }
+ </div>
+ )
+ }
+
+ infoButton (onClick) {
+ return <i className="fa fa-info-circle" onClick={onClick} />
+ }
+
+ renderGasEditRow (gasInputArgs) {
+ return (
+ <div className="advanced-gas-inputs__gas-edit-row">
+ <div className="advanced-gas-inputs__gas-edit-row__label">
+ { this.context.t(gasInputArgs.labelKey) }
+ { this.infoButton(() => gasInputArgs.infoOnClick()) }
+ </div>
+ { this.gasInput(gasInputArgs) }
+ </div>
+ )
+ }
+
+ render () {
+ const {
+ customGasPrice,
+ updateCustomGasPrice,
+ customGasLimit,
+ insufficientBalance,
+ customPriceIsSafe,
+ isSpeedUp,
+ showGasPriceInfoModal,
+ showGasLimitInfoModal,
+ } = this.props
+
+ return (
+ <div className="advanced-gas-inputs__gas-edit-rows">
+ { this.renderGasEditRow({
+ labelKey: 'gasPrice',
+ value: customGasPrice,
+ onChange: updateCustomGasPrice,
+ insufficientBalance,
+ customPriceIsSafe,
+ showGWEI: true,
+ isSpeedUp,
+ infoOnClick: showGasPriceInfoModal,
+ }) }
+ { this.renderGasEditRow({
+ labelKey: 'gasLimit',
+ value: customGasLimit,
+ onChange: this.onChangeGasLimit,
+ insufficientBalance,
+ customPriceIsSafe,
+ infoOnClick: showGasLimitInfoModal,
+ }) }
+ </div>
+ )
+ }
+}
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/app/gas-customization/advanced-gas-inputs/index.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js
new file mode 100644
index 000000000..bd8abaa3e
--- /dev/null
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js
@@ -0,0 +1 @@
+export { default } from './advanced-gas-inputs.container'
diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss
new file mode 100644
index 000000000..50953cbe5
--- /dev/null
+++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss
@@ -0,0 +1,133 @@
+.advanced-gas-inputs {
+ &__gas-edit-rows {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-between;
+ }
+
+ &__gas-edit-row {
+ display: flex;
+ flex-flow: column;
+ width: 47.5%;
+
+ &__label {
+ color: #313B5E;
+ font-size: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ @media screen and (max-width: 576px) {
+ font-size: 10px;
+ }
+
+ .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: 100%;
+ 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/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/app/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
new file mode 100644
index 000000000..492037f25
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js
@@ -0,0 +1 @@
+export { default } from './advanced-tab-content.component'
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/app/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
new file mode 100644
index 000000000..61b681e1a
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js
@@ -0,0 +1 @@
+export { default } from './time-remaining.component'
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..e2115af7f
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss
@@ -0,0 +1,17 @@
+.time-remaining {
+ color: #313A5E;
+ font-size: 16px;
+
+ .minutes-num, .seconds-num {
+ font-size: 16px;
+ }
+
+ .seconds-num {
+ margin-left: 7px;
+ font-size: 16px;
+ }
+
+ .minutes-label, .seconds-label {
+ font-size: 16px;
+ }
+} \ No newline at end of file
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/app/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
new file mode 100644
index 000000000..826d41f9c
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js
@@ -0,0 +1,33 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { getTimeBreakdown } from './time-remaining.utils'
+
+export default class TimeRemaining extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ milliseconds: PropTypes.number,
+ }
+
+ render () {
+ const {
+ milliseconds,
+ } = this.props
+
+ const {
+ minutes,
+ seconds,
+ } = getTimeBreakdown(milliseconds)
+
+ return (
+ <div className="time-remaining">
+ <span className="minutes-num">{minutes}</span>
+ <span className="minutes-label">{this.context.t('minutesShorthand')}</span>
+ <span className="seconds-num">{seconds}</span>
+ <span className="seconds-label">{this.context.t('secondsShorthand')}</span>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..cf43e0acb
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js
@@ -0,0 +1,11 @@
+function getTimeBreakdown (milliseconds) {
+ return {
+ hours: Math.floor(milliseconds / 3600000),
+ minutes: Math.floor((milliseconds % 3600000) / 60000),
+ seconds: Math.floor((milliseconds % 60000) / 1000),
+ }
+}
+
+module.exports = {
+ getTimeBreakdown,
+}
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/app/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
new file mode 100644
index 000000000..078d50fce
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js
@@ -0,0 +1 @@
+export { default } from './basic-tab-content.component'
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..e34e4e328
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss
@@ -0,0 +1,28 @@
+.basic-tab-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding-left: 21px;
+ height: 324px;
+ background: #F5F7F8;
+ border-bottom: 1px solid #d2d8dd;
+
+ &__title {
+ margin-top: 19px;
+ font-size: 16px;
+ color: $black;
+ }
+
+ &__blurb {
+ font-size: 12px;
+ color: $black;
+ margin-top: 5px;
+ margin-bottom: 15px;
+ }
+
+ &__footer-blurb {
+ font-size: 12px;
+ color: #979797;
+ margin-top: 15px;
+ }
+}
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/app/gas-customization/gas-modal-page-container/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js
new file mode 100644
index 000000000..ec0ebad22
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js
@@ -0,0 +1 @@
+export { default } from './gas-modal-page-container.container'
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss
new file mode 100644
index 000000000..b9e0f59c4
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss
@@ -0,0 +1,146 @@
+@import './advanced-tab-content/index';
+@import './basic-tab-content/index';
+
+.gas-modal-page-container {
+ .page-container {
+ max-width: 391px;
+ min-height: 585px;
+ overflow-y: initial;
+
+ @media screen and (max-width: $break-small) {
+ &__content {
+ display: flex;
+ overflow-y: initial;
+ }
+ }
+
+ &__header {
+ padding: 0px;
+ padding-top: 16px;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
+ }
+
+ &__footer {
+ header {
+ padding-top: 12px;
+ padding-bottom: 12px;
+ }
+ }
+
+ &__header-close-text {
+ font-size: 14px;
+ color: #4EADE7;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ cursor: pointer;
+ overflow: hidden;
+ }
+
+ &__title {
+ color: $black;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 16px;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ margin-right: 0;
+ }
+
+ &__subtitle {
+ display: none;
+ }
+
+ &__tabs {
+ margin-top: 0px;
+ }
+
+ &__tab {
+ width: 100%;
+ font-size: 14px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ &--selected {
+ color: $curious-blue;
+ border-bottom: 2px solid $curious-blue;
+ }
+ }
+ }
+}
+
+.gas-modal-content {
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ }
+
+ &__basic-tab {
+ height: 219px;
+ }
+
+
+ &__info-row, &__info-row--fade {
+ width: 100%;
+ background: $polar;
+ padding: 15px 21px;
+ display: flex;
+ flex-flow: column;
+ color: $scorpion;
+ font-size: 12px;
+
+ @media screen and (max-width: $break-small) {
+ padding: 4px 21px;
+ }
+
+ &__send-info, &__transaction-info, &__total-info, &__fiat-total-info {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-between;
+ }
+
+ &__fiat-total-info {
+ justify-content: flex-end;
+ }
+
+ &__total-info {
+ &__label {
+ font-size: 16px;
+
+ @media screen and (max-width: $break-small) {
+ font-size: 14px;
+ }
+ }
+
+ &__value {
+ font-size: 16px;
+ font-weight: bold;
+
+ @media screen and (max-width: $break-small) {
+ font-size: 14px;
+ }
+ }
+ }
+
+ &__transaction-info, &__send-info {
+ &__label {
+ font-size: 12px;
+ }
+
+ &__value {
+ font-size: 14px;
+ }
+ }
+ }
+
+ &__info-row--fade {
+ background: white;
+ color: $dusty-gray;
+ border-top: 1px solid $mischka;
+ }
+}
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/app/gas-customization/gas-price-button-group/index.js b/ui/app/components/app/gas-customization/gas-price-button-group/index.js
new file mode 100644
index 000000000..775648330
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.js
@@ -0,0 +1 @@
+export { default } from './gas-price-button-group.component'
diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/index.scss b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss
new file mode 100644
index 000000000..cb2f3ecf1
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss
@@ -0,0 +1,238 @@
+.gas-price-button-group {
+ margin-top: 22px;
+ display: flex;
+ justify-content: space-evenly;
+ width: 100%;
+ padding-left: 20px;
+ padding-right: 20px;
+
+ &__primary-currency {
+ font-size: 18px;
+ height: 20.5px;
+ margin-bottom: 7.5px;
+ }
+
+ &__time-estimate {
+ margin-top: 5.5px;
+ color: $silver-chalice;
+ height: 15.4px;
+ }
+
+ &__loading-container {
+ height: 130px;
+ }
+
+ .button-group__button, .button-group__button--active {
+ height: 130px;
+ max-width: 108px;
+ font-size: 12px;
+ flex-direction: column;
+ align-items: center;
+ display: flex;
+ padding-top: 17px;
+ border-radius: 4px;
+ border: 2px solid $spindle;
+ background: $white;
+ color: $scorpion;
+
+ div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ i {
+ &:last-child {
+ display: none;
+ }
+ }
+ }
+
+ .button-group__button--active {
+ border: 2px solid $curious-blue;
+ color: $scorpion;
+
+ i {
+ &:last-child {
+ display: flex;
+ color: $curious-blue;
+ margin-top: 8px
+ }
+ }
+ }
+}
+
+.gas-price-button-group--small {
+ display: flex;
+ justify-content: stretch;
+
+ @media screen and (max-width: $break-small) {
+ max-width: 260px;
+ }
+
+ &__button-fiat-price {
+ font-size: 13px;
+ }
+
+ &__button-label {
+ font-size: 16px;
+ }
+
+ &__label {
+ font-weight: 500;
+ }
+
+ &__primary-currency {
+ font-size: 12px;
+
+ @media screen and (max-width: 575px) {
+ font-size: 10px;
+ }
+ }
+
+ &__secondary-currency {
+ font-size: 12px;
+
+ @media screen and (max-width: 575px) {
+ font-size: 10px;
+ }
+ }
+
+ &__loading-container {
+ height: 78px;
+ }
+
+ .button-group__button, .button-group__button--active {
+ height: 78px;
+ background: white;
+ color: $scorpion;
+ padding-top: 9px;
+ padding-left: 8.5px;
+
+ @media screen and (max-width: $break-small) {
+ padding-left: 4px;
+ }
+
+ div {
+ display: flex;
+ flex-flow: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+
+ i {
+ &:last-child {
+ display: none;
+ }
+ }
+ }
+
+ .button-group__button--active {
+ color: $white;
+ background: $dodger-blue;
+
+ i {
+ &:last-child {
+ display: flex;
+ color: $curious-blue;
+ margin-top: 10px
+ }
+ }
+ }
+}
+
+.gas-price-button-group--alt {
+ display: flex;
+ justify-content: stretch;
+ width: 95%;
+
+ &__button-fiat-price {
+ font-size: 13px;
+ }
+
+ &__button-label {
+ font-size: 16px;
+ }
+
+ &__label {
+ font-weight: 500;
+ font-size: 10px;
+ text-transform: capitalize;
+ }
+
+ &__primary-currency {
+ font-size: 11px;
+ margin-top: 3px;
+ }
+
+ &__secondary-currency {
+ font-size: 11px;
+ }
+
+ &__loading-container {
+ height: 78px;
+ }
+
+ &__time-estimate {
+ font-size: 14px;
+ font-weight: 500;
+ margin-top: 4px;
+ color: $black;
+ }
+
+ .button-group__button, .button-group__button--active {
+ height: 78px;
+ background: white;
+ color: #2A4055;
+ width: 108px;
+ height: 97px;
+ box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.151579);
+ border-radius: 6px;
+ border: none;
+
+ div {
+ display: flex;
+ flex-flow: column;;
+ align-items: flex-start;
+ justify-content: flex-start;
+ position: relative;
+ }
+
+ .button-check-wrapper {
+ display: none;
+ }
+
+ &:first-child {
+ margin-right: 6px;
+ }
+
+ &:last-child {
+ margin-left: 6px;
+ }
+ }
+
+ .button-group__button--active {
+ background: #F7FCFF;
+ border: 2px solid #2C8BDC;
+
+ .button-check-wrapper {
+ height: 16px;
+ width: 16px;
+ border-radius: 8px;
+ position: absolute;
+ top: -11px;
+ right: -10px;
+ background: #D5ECFA;
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ align-items: center;
+ }
+
+ i {
+ display: flex;
+ color: $curious-blue;
+ font-size: 12px;
+ }
+ }
+}
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/app/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
new file mode 100644
index 000000000..c0eaf4852
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js
@@ -0,0 +1,108 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import * as d3 from 'd3'
+import {
+ generateChart,
+ getCoordinateData,
+ handleChartUpdate,
+ hideDataUI,
+ setTickPosition,
+ handleMouseMove,
+} from './gas-price-chart.utils.js'
+
+export default class GasPriceChart extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ gasPrices: PropTypes.array,
+ estimatedTimes: PropTypes.array,
+ gasPricesMax: PropTypes.number,
+ estimatedTimesMax: PropTypes.number,
+ currentPrice: PropTypes.number,
+ updateCustomGasPrice: PropTypes.func,
+ }
+
+ renderChart ({
+ currentPrice,
+ gasPrices,
+ estimatedTimes,
+ gasPricesMax,
+ estimatedTimesMax,
+ updateCustomGasPrice,
+ }) {
+ const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax, this.context.t)
+ setTimeout(function () {
+ setTickPosition('y', 0, -5, 8)
+ setTickPosition('y', 1, -3, -5)
+ setTickPosition('x', 0, 3)
+ setTickPosition('x', 1, 3, -8)
+
+ const { x: domainX } = getCoordinateData('.domain')
+ const { x: yAxisX } = getCoordinateData('.c3-axis-y-label')
+ const { x: tickX } = getCoordinateData('.c3-axis-x .tick')
+
+ d3.select('.c3-axis-x .tick').attr('transform', 'translate(' + (domainX - tickX) / 2 + ', 0)')
+ d3.select('.c3-axis-x-label').attr('transform', 'translate(0,-15)')
+ d3.select('.c3-axis-y-label').attr('transform', 'translate(' + (domainX - yAxisX - 12) + ', 2) rotate(-90)')
+ d3.select('.c3-xgrid-focus line').attr('y2', 98)
+
+ d3.select('.c3-chart').on('mouseout', () => {
+ hideDataUI(chart, '#overlayed-circle')
+ })
+
+ d3.select('.c3-chart').on('click', () => {
+ const { x: newGasPrice } = d3.select('#overlayed-circle').datum()
+ updateCustomGasPrice(newGasPrice)
+ })
+
+ const { x: chartXStart, width: chartWidth } = getCoordinateData('.c3-areas-data1')
+
+ handleChartUpdate({
+ chart,
+ gasPrices,
+ newPrice: currentPrice,
+ cssId: '#set-circle',
+ })
+
+ d3.select('.c3-chart').on('mousemove', function () {
+ handleMouseMove({
+ xMousePos: d3.event.clientX,
+ chartXStart,
+ chartWidth,
+ gasPrices,
+ estimatedTimes,
+ chart,
+ })
+ })
+ }, 0)
+
+ this.chart = chart
+ }
+
+ componentDidUpdate (prevProps) {
+ const { gasPrices, currentPrice: newPrice } = this.props
+
+ if (prevProps.currentPrice !== newPrice) {
+ handleChartUpdate({
+ chart: this.chart,
+ gasPrices,
+ newPrice,
+ cssId: '#set-circle',
+ })
+ }
+ }
+
+ componentDidMount () {
+ this.renderChart(this.props)
+ }
+
+ render () {
+ return (
+ <div className="gas-price-chart" id="container">
+ <div className="gas-price-chart__root" id="chart"></div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..f19dafcc1
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js
@@ -0,0 +1,354 @@
+import * as d3 from 'd3'
+import c3 from 'c3'
+import BigNumber from 'bignumber.js'
+
+const newBigSigDig = n => (new BigNumber(n.toPrecision(15)))
+const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b))
+const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus')
+const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div')
+
+export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) {
+ const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({
+ xMousePos,
+ chartXStart,
+ chartWidth,
+ gasPrices,
+ estimatedTimes,
+ })
+
+ if (currentPosValue === null && newTimeEstimate === null) {
+ hideDataUI(chart, '#overlayed-circle')
+ return
+ }
+
+ const indexOfNewCircle = estimatedTimes.length + 1
+ const dataUIObj = generateDataUIObj(currentPosValue, indexOfNewCircle, newTimeEstimate)
+
+ chart.internal.overlayPoint(dataUIObj, indexOfNewCircle)
+ chart.internal.showTooltip([dataUIObj], d3.select('.c3-areas-data1')._groups[0])
+ chart.internal.showXGridFocus([dataUIObj])
+}
+
+export function getCoordinateData (selector) {
+ const node = d3.select(selector).node()
+ return node ? node.getBoundingClientRect() : {}
+}
+
+export function generateDataUIObj (x, index, value) {
+ return {
+ x,
+ value,
+ index,
+ id: 'data1',
+ name: 'data1',
+ }
+}
+
+export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) {
+ const {
+ closestLowerValueIndex,
+ closestLowerValue,
+ closestHigherValueIndex,
+ closestHigherValue,
+ } = getAdjacentGasPrices({ gasPrices, priceToPosition: newPrice })
+
+ if (closestLowerValue && closestHigherValue) {
+ setSelectedCircle({
+ chart,
+ newPrice,
+ closestLowerValueIndex,
+ closestLowerValue,
+ closestHigherValueIndex,
+ closestHigherValue,
+ })
+ } else {
+ hideDataUI(chart, cssId)
+ }
+}
+
+export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) {
+ const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition)
+ const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition)
+ return {
+ closestLowerValueIndex,
+ closestHigherValueIndex,
+ closestHigherValue: gasPrices[closestHigherValueIndex],
+ closestLowerValue: gasPrices[closestLowerValueIndex],
+ }
+}
+
+export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) {
+ const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX))
+ const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated()
+
+ return newTimeEstimate.toNumber()
+}
+
+
+export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) {
+ const chartMouseXPos = bigNumMinus(xMousePos, chartXStart)
+ const posPercentile = bigNumDiv(chartMouseXPos, chartWidth)
+
+ const currentPosValue = (bigNumMinus(gasPrices[gasPrices.length - 1], gasPrices[0]))
+ .times(newBigSigDig(posPercentile))
+ .plus(newBigSigDig(gasPrices[0]))
+ .toNumber()
+
+ const {
+ closestLowerValueIndex,
+ closestLowerValue,
+ closestHigherValueIndex,
+ closestHigherValue,
+ } = getAdjacentGasPrices({ gasPrices, priceToPosition: currentPosValue })
+
+ return !closestHigherValue || !closestLowerValue
+ ? {
+ currentPosValue: null,
+ newTimeEstimate: null,
+ }
+ : {
+ currentPosValue,
+ newTimeEstimate: extrapolateY({
+ higherY: estimatedTimes[closestHigherValueIndex],
+ lowerY: estimatedTimes[closestLowerValueIndex],
+ higherX: closestHigherValue,
+ lowerX: closestLowerValue,
+ xForExtrapolation: currentPosValue,
+ }),
+ }
+}
+
+export function hideDataUI (chart, dataNodeId) {
+ const overLayedCircle = d3.select(dataNodeId)
+ if (!overLayedCircle.empty()) {
+ overLayedCircle.remove()
+ }
+ d3.select('.c3-tooltip-container').style('display', 'none !important')
+ chart.internal.hideXGridFocus()
+}
+
+export function setTickPosition (axis, n, newPosition, secondNewPosition) {
+ const positionToShift = axis === 'y' ? 'x' : 'y'
+ const secondPositionToShift = axis === 'y' ? 'y' : 'x'
+ d3.select('#chart')
+ .select(`.c3-axis-${axis}`)
+ .selectAll('.tick')
+ .filter((d, i) => i === n)
+ .select('text')
+ .attr(positionToShift, 0)
+ .select('tspan')
+ .attr(positionToShift, newPosition)
+ .attr(secondPositionToShift, secondNewPosition || 0)
+ .style('visibility', 'visible')
+}
+
+export function appendOrUpdateCircle ({ data, itemIndex, cx, cy, cssId, appendOnly }) {
+ const circle = this.main
+ .select('.c3-selected-circles' + this.getTargetSelectorSuffix(data.id))
+ .selectAll(`.c3-selected-circle-${itemIndex}`)
+
+ if (appendOnly || circle.empty()) {
+ circle.data([data])
+ .enter().append('circle')
+ .attr('class', () => this.generateClass('c3-selected-circle', itemIndex))
+ .attr('id', cssId)
+ .attr('cx', cx)
+ .attr('cy', cy)
+ .attr('stroke', () => this.color(data))
+ .attr('r', 6)
+ } else {
+ circle.data([data])
+ .attr('cx', cx)
+ .attr('cy', cy)
+ }
+}
+
+export function setSelectedCircle ({
+ chart,
+ newPrice,
+ closestLowerValueIndex,
+ closestLowerValue,
+ closestHigherValueIndex,
+ closestHigherValue,
+}) {
+ const numberOfValues = chart.internal.data.xs.data1.length
+
+ const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`)
+ let { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`)
+ let count = closestHigherValueIndex + 1
+
+ if (lowerX && higherX) {
+ while (lowerX === higherX) {
+ higherX = getCoordinateData(`.c3-circle-${count}`).x
+ higherY = getCoordinateData(`.c3-circle-${count}`).y
+ count++
+ }
+ }
+
+ const currentX = bigNumMinus(higherX, lowerX)
+ .times(bigNumMinus(newPrice, closestLowerValue))
+ .div(bigNumMinus(closestHigherValue, closestLowerValue))
+ .plus(newBigSigDig(lowerX))
+
+ const newTimeEstimate = extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX })
+
+ chart.internal.selectPoint(
+ generateDataUIObj(currentX.toNumber(), numberOfValues, newTimeEstimate),
+ numberOfValues
+ )
+}
+
+
+export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) {
+ const gasPricesMaxPadded = gasPricesMax + 1
+ const chart = c3.generate({
+ size: {
+ height: 165,
+ },
+ transition: {
+ duration: 0,
+ },
+ padding: {left: 20, right: 15, top: 6, bottom: 10},
+ data: {
+ x: 'x',
+ columns: [
+ ['x', ...gasPrices],
+ ['data1', ...estimatedTimes],
+ ],
+ types: {
+ data1: 'area',
+ },
+ selection: {
+ enabled: false,
+ },
+ },
+ color: {
+ data1: '#259de5',
+ },
+ axis: {
+ x: {
+ min: gasPrices[0],
+ max: gasPricesMax,
+ tick: {
+ values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)],
+ outer: false,
+ format: function (val) { return val + ' GWEI' },
+ },
+ padding: {left: gasPricesMax / 50, right: gasPricesMax / 50},
+ label: {
+ text: 'Gas Price ($)',
+ position: 'outer-center',
+ },
+ },
+ y: {
+ padding: {top: 7, bottom: 7},
+ tick: {
+ values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)],
+ outer: false,
+ },
+ label: {
+ text: 'Confirmation time (sec)',
+ position: 'outer-middle',
+ },
+ min: 0,
+ },
+ },
+ legend: {
+ show: false,
+ },
+ grid: {
+ x: {},
+ lines: {
+ front: false,
+ },
+ },
+ point: {
+ focus: {
+ expand: {
+ enabled: false,
+ r: 3.5,
+ },
+ },
+ },
+ tooltip: {
+ format: {
+ title: (v) => v.toPrecision(4),
+ },
+ contents: function (d) {
+ const titleFormat = this.config.tooltip_format_title
+ let text
+ d.forEach(el => {
+ if (el && (el.value || el.value === 0) && !text) {
+ text = "<table class='" + 'custom-tooltip' + "'>" + "<tr><th colspan='2'>" + titleFormat(el.x) + '</th></tr>'
+ }
+ })
+ return text + '</table>' + "<div class='tooltip-arrow'></div>"
+ },
+ position: function (data) {
+ if (d3.select('#overlayed-circle').empty()) {
+ return { top: -100, left: -100 }
+ }
+
+ const { x: circleX, y: circleY, width: circleWidth } = getCoordinateData('#overlayed-circle')
+ const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-chart')
+
+ // TODO: Confirm the below constants work with all data sets and screen sizes
+ const flipTooltip = circleY - circleWidth < chartYStart + 5
+
+ d3
+ .select('.tooltip-arrow')
+ .style('margin-top', flipTooltip ? '-16px' : '4px')
+
+ return {
+ top: bigNumMinus(circleY, chartYStart).minus(19).plus(flipTooltip ? circleWidth + 38 : 0).toNumber(),
+ left: bigNumMinus(circleX, chartXStart).plus(newBigSigDig(circleWidth)).minus(bigNumDiv(gasPricesMaxPadded, 50)).toNumber(),
+ }
+ },
+ show: true,
+ },
+ })
+
+ chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) {
+ const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-areas-data1')
+
+ d3.select('#set-circle').remove()
+
+ appendOrUpdateCircle.bind(this)({
+ data,
+ itemIndex,
+ cx: () => bigNumMinus(data.x, chartXStart).plus(11).toNumber(),
+ cy: () => bigNumMinus(data.value, chartYStart).plus(10).toNumber(),
+ cssId: 'set-circle',
+ appendOnly: true,
+ })
+ }
+
+ chart.internal.overlayPoint = function (data, itemIndex) {
+ appendOrUpdateCircle.bind(this)({
+ data,
+ itemIndex,
+ cx: this.circleX.bind(this),
+ cy: this.circleY.bind(this),
+ cssId: 'overlayed-circle',
+ })
+ }
+
+ chart.internal.showTooltip = function (selectedData, element) {
+ const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0))
+
+ if (dataToShow.length) {
+ this.tooltip.html(
+ this.config.tooltip_contents.call(this, selectedData, this.axis.getXAxisTickFormat(), this.getYFormat(), this.color)
+ ).style('display', 'flex')
+
+ // Get tooltip dimensions
+ const tWidth = this.tooltip.property('offsetWidth')
+ const tHeight = this.tooltip.property('offsetHeight')
+ const position = this.config.tooltip_position.call(this, dataToShow, tWidth, tHeight, element)
+ // Set tooltip
+ this.tooltip.style('top', position.top + 'px').style('left', position.left + 'px')
+ }
+ }
+
+ return chart
+}
diff --git a/ui/app/components/app/gas-customization/gas-price-chart/index.js b/ui/app/components/app/gas-customization/gas-price-chart/index.js
new file mode 100644
index 000000000..9895acb62
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-chart/index.js
@@ -0,0 +1 @@
+export { default } from './gas-price-chart.component'
diff --git a/ui/app/components/app/gas-customization/gas-price-chart/index.scss b/ui/app/components/app/gas-customization/gas-price-chart/index.scss
new file mode 100644
index 000000000..097543104
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-price-chart/index.scss
@@ -0,0 +1,132 @@
+.gas-price-chart {
+ display: flex;
+ position: relative;
+ justify-content: center;
+
+ &__root {
+ max-height: 154px;
+ max-width: 391px;
+ position: relative;
+ overflow: hidden;
+
+ @media screen and (max-width: $break-small) {
+ max-width: 326px;
+ }
+ }
+
+ .tick text, .c3-axis-x-label, .c3-axis-y-label {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: bold;
+ line-height: normal;
+ font-size: 8px;
+ text-align: center;
+ fill: #9A9CA6 !important;
+ }
+
+ .c3-tooltip-container {
+ display: flex;
+ justify-content: center !important;
+ align-items: flex-end !important;
+ }
+
+ .custom-tooltip {
+ background: rgba(0, 0, 0, 1);
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ border-radius: 3px;
+ opacity: 1 !important;
+ height: 21px;
+ z-index: 1;
+ }
+
+ .tooltip-arrow {
+ background: black;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5);
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ opacity: 1 !important;
+ width: 9px;
+ height: 9px;
+ margin-top: 4px;
+ }
+
+ .custom-tooltip th {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ font-size: 10px;
+ text-align: center;
+ padding: 3px;
+ color: #FFFFFF;
+ }
+
+ .c3-circle {
+ visibility: hidden;
+ }
+
+ .c3-selected-circle, .c3-circle._expanded_ {
+ fill: #FFFFFF !important;
+ stroke-width: 2.4px !important;
+ stroke: #2d9fd9 !important;
+ /* visibility: visible; */
+ }
+
+ #set-circle {
+ fill: #313A5E !important;
+ stroke: #313A5E !important;
+ }
+
+ .c3-axis-x-label, .c3-axis-y-label {
+ font-weight: normal;
+ }
+
+ .tick text tspan {
+ visibility: hidden;
+ }
+
+ .c3-circle {
+ fill: #2d9fd9 !important;
+ }
+
+ .c3-line-data1 {
+ stroke: #2d9fd9 !important;
+ background: rgba(0,0,0,0) !important;
+ color: rgba(0,0,0,0) !important;
+ }
+
+ .c3 path {
+ fill: none;
+ }
+
+ .c3 path.c3-area-data1 {
+ opacity: 1;
+ fill: #e9edf1 !important;
+ }
+
+ .c3-xgrid-line line {
+ stroke: #B8B8B8 !important;
+ }
+
+ .c3-xgrid-focus {
+ stroke: #aaa;
+ }
+
+ .c3-axis-x .domain {
+ fill: none;
+ stroke: none;
+ }
+
+ .c3-axis-y .domain {
+ fill: none;
+ stroke: #C8CCD6;
+ }
+
+ .c3-event-rect {
+ cursor: pointer;
+ }
+}
+
+#chart {
+ background: #F8F9FB
+}
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/app/gas-customization/gas-slider/gas-slider.component.js b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js
new file mode 100644
index 000000000..5836e7dfc
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class AdvancedTabContent extends Component {
+ static propTypes = {
+ onChange: PropTypes.func,
+ lowLabel: PropTypes.string,
+ highLabel: PropTypes.string,
+ value: PropTypes.number,
+ step: PropTypes.number,
+ max: PropTypes.number,
+ min: PropTypes.number,
+ }
+
+ render () {
+ const {
+ onChange,
+ lowLabel,
+ highLabel,
+ value,
+ step,
+ max,
+ min,
+ } = this.props
+
+ return (
+ <div className="gas-slider">
+ <input
+ className="gas-slider__input"
+ type="range"
+ step={step}
+ max={max}
+ min={min}
+ value={value}
+ id="gasSlider"
+ onChange={event => onChange(event.target.value)}
+ />
+ <div className="gas-slider__bar">
+ <div className="gas-slider__colored"/>
+ </div>
+ <div className="gas-slider__labels">
+ <span>{lowLabel}</span>
+ <span>{highLabel}</span>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/app/gas-customization/gas-slider/index.js b/ui/app/components/app/gas-customization/gas-slider/index.js
new file mode 100644
index 000000000..f1752c93f
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-slider/index.js
@@ -0,0 +1 @@
+export { default } from './gas-slider.component'
diff --git a/ui/app/components/app/gas-customization/gas-slider/index.scss b/ui/app/components/app/gas-customization/gas-slider/index.scss
new file mode 100644
index 000000000..e6c734367
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas-slider/index.scss
@@ -0,0 +1,54 @@
+.gas-slider {
+ position: relative;
+ width: 322px;
+
+ &__input {
+ width: 322px;
+ margin-left: -2px;
+ z-index: 2;
+ }
+
+ input[type=range] {
+ -webkit-appearance: none !important;
+ }
+
+ input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none !important;
+ height: 34px;
+ width: 34px;
+ background-color: $curious-blue;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
+ border-radius: 50%;
+ position: relative;
+ z-index: 10;
+ }
+
+ &__bar {
+ height: 6px;
+ width: 322px;
+ background: $alto;
+ display: flex;
+ justify-content: space-between;
+ position: absolute;
+ top: 16px;
+ z-index: 0;
+ border-radius: 4px;
+ }
+
+ &__colored {
+ height: 6px;
+ border-radius: 4px;
+ margin-left: 102px;
+ width: 322px;
+ z-index: 1;
+ background-color: $blizzard-blue;
+ }
+
+ &__labels {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ margin-top: -6px;
+ color: $mid-gray;
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/app/gas-customization/gas.selectors.js b/ui/app/components/app/gas-customization/gas.selectors.js
new file mode 100644
index 000000000..89374b5f1
--- /dev/null
+++ b/ui/app/components/app/gas-customization/gas.selectors.js
@@ -0,0 +1,14 @@
+const selectors = {
+ getCurrentBlockTime,
+ getBasicGasEstimateLoadingStatus,
+}
+
+module.exports = selectors
+
+function getCurrentBlockTime (state) {
+ return state.gas.currentBlockTime
+}
+
+function getBasicGasEstimateLoadingStatus (state) {
+ return state.gas.basicEstimateIsLoading
+}
diff --git a/ui/app/components/app/gas-customization/index.scss b/ui/app/components/app/gas-customization/index.scss
new file mode 100644
index 000000000..b06c1d044
--- /dev/null
+++ b/ui/app/components/app/gas-customization/index.scss
@@ -0,0 +1,7 @@
+@import './gas-slider/index';
+
+@import './gas-modal-page-container/index';
+
+@import './gas-price-chart/index';
+
+@import './advanced-gas-inputs/index';
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/app/loading-network-screen/index.js b/ui/app/components/app/loading-network-screen/index.js
new file mode 100644
index 000000000..726b4b530
--- /dev/null
+++ b/ui/app/components/app/loading-network-screen/index.js
@@ -0,0 +1 @@
+export { default } from './loading-network-screen.container'
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/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js
new file mode 100644
index 000000000..345951b0f
--- /dev/null
+++ b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js
@@ -0,0 +1,57 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import CancelTransaction from '../cancel-transaction.component'
+import CancelTransactionGasFee from '../cancel-transaction-gas-fee'
+import Modal from '../../../modal'
+
+describe('CancelTransaction Component', () => {
+ const t = key => key
+
+ it('should render a CancelTransaction modal', () => {
+ const wrapper = shallow(
+ <CancelTransaction
+ newGasFee="0x1319718a5000"
+ />,
+ { context: { t }}
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(Modal).length, 1)
+ assert.equal(wrapper.find(CancelTransactionGasFee).length, 1)
+ assert.equal(wrapper.find(CancelTransactionGasFee).props().value, '0x1319718a5000')
+ assert.equal(wrapper.find('.cancel-transaction__title').text(), 'cancellationGasFee')
+ assert.equal(wrapper.find('.cancel-transaction__description').text(), 'attemptToCancelDescription')
+ })
+
+ it('should pass the correct props to the Modal component', async () => {
+ const createCancelTransactionSpy = sinon.stub().callsFake(() => Promise.resolve())
+ const hideModalSpy = sinon.spy()
+
+ const wrapper = shallow(
+ <CancelTransaction
+ defaultNewGasPrice="0x3b9aca00"
+ createCancelTransaction={createCancelTransactionSpy}
+ hideModal={hideModalSpy}
+ showTransactionConfirmedModal={() => {}}
+ />,
+ { context: { t }}
+ )
+
+ assert.equal(wrapper.find(Modal).length, 1)
+ const modalProps = wrapper.find(Modal).props()
+
+ assert.equal(modalProps.headerText, 'attemptToCancel')
+ assert.equal(modalProps.submitText, 'yesLetsTry')
+ assert.equal(modalProps.cancelText, 'nevermind')
+
+ assert.equal(createCancelTransactionSpy.callCount, 0)
+ assert.equal(hideModalSpy.callCount, 0)
+ await modalProps.onSubmit()
+ assert.equal(createCancelTransactionSpy.callCount, 1)
+ assert.equal(hideModalSpy.callCount, 1)
+ modalProps.onCancel()
+ assert.equal(hideModalSpy.callCount, 2)
+ })
+})
diff --git a/ui/app/components/modals/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..def88f085
--- /dev/null
+++ b/ui/app/components/app/modals/deposit-ether-modal.js
@@ -0,0 +1,241 @@
+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
+let COINSWITCH_ROW_TITLE
+let COINSWITCH_ROW_TEXT
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ address: state.metamask.selectedAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ toCoinbase: (address) => {
+ dispatch(actions.buyEth({ service: 'coinbase', address, amount: 0 }))
+ },
+ toCoinSwitch: (address) => {
+ dispatch(actions.buyEth({ service: 'coinswitch', address }))
+ },
+ 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')
+ COINSWITCH_ROW_TITLE = context.t('buyCoinSwitch')
+ COINSWITCH_ROW_TEXT = context.t('buyCoinSwitchExplainer')
+
+ 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, toCoinSwitch, 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/coinswitch_logo.png\')',
+ height: '40px',
+ },
+ }),
+ title: COINSWITCH_ROW_TITLE,
+ text: COINSWITCH_ROW_TEXT,
+ buttonLabel: this.context.t('continueToCoinSwitch'),
+ onButtonClick: () => toCoinSwitch(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/app/modals/loading-network-error/index.js b/ui/app/components/app/modals/loading-network-error/index.js
new file mode 100644
index 000000000..b3737458a
--- /dev/null
+++ b/ui/app/components/app/modals/loading-network-error/index.js
@@ -0,0 +1 @@
+export { default } from './loading-network-error.container'
diff --git a/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js
new file mode 100644
index 000000000..44f71e4b2
--- /dev/null
+++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import Modal, { ModalContent } from '../../modal'
+
+const LoadingNetworkError = (props, context) => {
+ const { t } = context
+ const { hideModal } = props
+
+ return (
+ <Modal
+ onSubmit={() => hideModal()}
+ submitText={t('tryAgain')}
+ >
+ <ModalContent
+ description={'Oops! Something went wrong.'}
+ />
+ </Modal>
+ )
+}
+
+LoadingNetworkError.contextTypes = {
+ t: PropTypes.func,
+}
+
+LoadingNetworkError.propTypes = {
+ hideModal: PropTypes.func,
+}
+
+export default LoadingNetworkError
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/app/modals/metametrics-opt-in-modal/index.js b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js
new file mode 100644
index 000000000..47f946757
--- /dev/null
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in-modal.container'
diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss
new file mode 100644
index 000000000..88b6d7a4d
--- /dev/null
+++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss
@@ -0,0 +1,30 @@
+.metametrics-opt-in-modal {
+ .metametrics-opt-in__main {
+ justify-content: center;
+ margin-left: 3%;
+ margin-right: 0%;
+ max-height: 75vh;
+
+ @media screen and (max-width: 575px) {
+ max-height: 100vh;
+ }
+ }
+
+
+ .metametrics-opt-in__title {
+ font-size: 38px;
+ }
+
+ .metametrics-opt-in__content {
+ padding-right: 6px;
+ }
+
+ .metametrics-opt-in__footer {
+ @media screen and (max-width: 575px) {
+ margin-top: 10px;
+ justify-content: center;
+ margin-left: 2%;
+ max-height: 520px;
+ }
+ }
+} \ No newline at end of file
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/app/network.js b/ui/app/components/app/network.js
new file mode 100644
index 000000000..e18404f42
--- /dev/null
+++ b/ui/app/components/app/network.js
@@ -0,0 +1,149 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const classnames = require('classnames')
+const inherits = require('util').inherits
+const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
+
+Network.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect()(Network)
+
+
+inherits(Network, Component)
+
+function Network () {
+ Component.call(this)
+}
+
+Network.prototype.render = function () {
+ const props = this.props
+ const context = this.context
+ const networkNumber = props.network
+ let providerName, providerNick, providerUrl
+ try {
+ providerName = props.provider.type
+ providerNick = props.provider.nickname || ''
+ providerUrl = props.provider.rpcTarget
+ } catch (e) {
+ providerName = null
+ }
+ const providerId = providerNick || providerName || providerUrl || null
+ let iconName
+ let hoverText
+
+ if (providerName === 'mainnet') {
+ hoverText = context.t('mainnet')
+ iconName = 'ethereum-network'
+ } else if (providerName === 'ropsten') {
+ hoverText = context.t('ropsten')
+ iconName = 'ropsten-test-network'
+ } else if (parseInt(networkNumber) === 3) {
+ hoverText = context.t('ropsten')
+ iconName = 'ropsten-test-network'
+ } else if (providerName === 'kovan') {
+ hoverText = context.t('kovan')
+ iconName = 'kovan-test-network'
+ } else if (providerName === 'rinkeby') {
+ hoverText = context.t('rinkeby')
+ iconName = 'rinkeby-test-network'
+ } else {
+ hoverText = providerId
+ iconName = 'private-network'
+ }
+
+ return (
+ h('div.network-component.pointer', {
+ className: classnames({
+ 'network-component--disabled': this.props.disabled,
+ 'ethereum-network': providerName === 'mainnet',
+ 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3,
+ 'kovan-test-network': providerName === 'kovan',
+ 'rinkeby-test-network': providerName === 'rinkeby',
+ }),
+ title: hoverText,
+ onClick: (event) => {
+ if (!this.props.disabled) {
+ this.props.onClick(event)
+ }
+ },
+ }, [
+ (function () {
+ switch (iconName) {
+ case 'ethereum-network':
+ return h('.network-indicator', [
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#038789', // $blue-lagoon
+ nonSelectBackgroundColor: '#15afb2',
+ loading: networkNumber === 'loading',
+ }),
+ h('.network-name', context.t('mainnet')),
+ h('i.fa.fa-chevron-down.fa-lg.network-caret'),
+ ])
+ case 'ropsten-test-network':
+ return h('.network-indicator', [
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#e91550', // $crimson
+ nonSelectBackgroundColor: '#ec2c50',
+ loading: networkNumber === 'loading',
+ }),
+ h('.network-name', context.t('ropsten')),
+ h('i.fa.fa-chevron-down.fa-lg.network-caret'),
+ ])
+ case 'kovan-test-network':
+ return h('.network-indicator', [
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#690496', // $purple
+ nonSelectBackgroundColor: '#b039f3',
+ loading: networkNumber === 'loading',
+ }),
+ h('.network-name', context.t('kovan')),
+ h('i.fa.fa-chevron-down.fa-lg.network-caret'),
+ ])
+ case 'rinkeby-test-network':
+ return h('.network-indicator', [
+ h(NetworkDropdownIcon, {
+ backgroundColor: '#ebb33f', // $tulip-tree
+ nonSelectBackgroundColor: '#ecb23e',
+ loading: networkNumber === 'loading',
+ }),
+ h('.network-name', context.t('rinkeby')),
+ h('i.fa.fa-chevron-down.fa-lg.network-caret'),
+ ])
+ default:
+ return h('.network-indicator', [
+ networkNumber === 'loading'
+ ? h('span.pointer.network-indicator', {
+ style: {
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ onClick: (event) => this.props.onClick(event),
+ }, [
+ h('img', {
+ title: context.t('attemptingConnect'),
+ style: {
+ width: '27px',
+ },
+ src: 'images/loading.svg',
+ }),
+ ])
+ : h('i.fa.fa-question-circle.fa-lg', {
+ style: {
+ margin: '10px',
+ color: 'rgb(125, 128, 130)',
+ },
+ }),
+
+ h('.network-name', providerNick || context.t('privateNetwork')),
+ h('i.fa.fa-chevron-down.fa-lg.network-caret'),
+ ])
+ }
+ })(),
+ ])
+ )
+}
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/app/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
new file mode 100644
index 000000000..f17137c1e
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -0,0 +1,65 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class AmountMaxButton extends Component {
+
+ static propTypes = {
+ balance: PropTypes.string,
+ gasTotal: PropTypes.string,
+ maxModeOn: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ setAmountToMax: PropTypes.func,
+ setMaxModeTo: PropTypes.func,
+ tokenBalance: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ setMaxAmount () {
+ const {
+ balance,
+ gasTotal,
+ selectedToken,
+ setAmountToMax,
+ tokenBalance,
+ } = this.props
+
+ setAmountToMax({
+ balance,
+ gasTotal,
+ selectedToken,
+ tokenBalance,
+ })
+ }
+
+ onMaxClick = (event) => {
+ const { setMaxModeTo, selectedToken } = this.props
+
+ fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), {
+ 'headers': {},
+ 'method': 'GET',
+ })
+
+ event.preventDefault()
+ setMaxModeTo(true)
+ this.setMaxAmount()
+ }
+
+ render () {
+ return this.props.maxModeOn
+ ? null
+ : (
+ <div>
+ <span
+ className="send-v2__amount-max"
+ onClick={this.onMaxClick}
+ >
+ {this.context.t('max')}
+ </span>
+ </div>
+ )
+ }
+
+}
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/app/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
new file mode 100644
index 000000000..1ee858f67
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
@@ -0,0 +1,27 @@
+import assert from 'assert'
+import {
+ calcMaxAmount,
+} from '../amount-max-button.utils.js'
+
+describe('amount-max-button utils', () => {
+
+ describe('calcMaxAmount()', () => {
+ it('should calculate the correct amount when no selectedToken defined', () => {
+ assert.deepEqual(calcMaxAmount({
+ balance: 'ffffff',
+ gasTotal: 'ff',
+ selectedToken: false,
+ }), 'ffff00')
+ })
+
+ it('should calculate the correct amount when a selectedToken is defined', () => {
+ assert.deepEqual(calcMaxAmount({
+ selectedToken: {
+ decimals: 10,
+ },
+ tokenBalance: '64',
+ }), 'e8d4a51000')
+ })
+ })
+
+})
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/app/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
new file mode 100644
index 000000000..14a71129f
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
@@ -0,0 +1,187 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import SendAmountRow from '../send-amount-row.component.js'
+
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+import AmountMaxButton from '../amount-max-button/amount-max-button.container'
+import UserPreferencedTokenInput from '../../../../user-preferenced-token-input'
+
+const propsMethodSpies = {
+ setMaxModeTo: sinon.spy(),
+ updateSendAmount: sinon.spy(),
+ updateSendAmountError: sinon.spy(),
+ updateGas: sinon.spy(),
+ updateGasFeeError: sinon.spy(),
+}
+
+sinon.spy(SendAmountRow.prototype, 'updateAmount')
+sinon.spy(SendAmountRow.prototype, 'validateAmount')
+sinon.spy(SendAmountRow.prototype, 'updateGas')
+
+describe('SendAmountRow Component', function () {
+ let wrapper
+ let instance
+
+ beforeEach(() => {
+ wrapper = shallow(<SendAmountRow
+ amount={'mockAmount'}
+ amountConversionRate={'mockAmountConversionRate'}
+ balance={'mockBalance'}
+ conversionRate={7}
+ convertedCurrency={'mockConvertedCurrency'}
+ gasTotal={'mockGasTotal'}
+ inError={false}
+ primaryCurrency={'mockPrimaryCurrency'}
+ selectedToken={ { address: 'mockTokenAddress' } }
+ setMaxModeTo={propsMethodSpies.setMaxModeTo}
+ tokenBalance={'mockTokenBalance'}
+ updateGasFeeError={propsMethodSpies.updateGasFeeError}
+ updateSendAmount={propsMethodSpies.updateSendAmount}
+ updateSendAmountError={propsMethodSpies.updateSendAmountError}
+ updateGas={propsMethodSpies.updateGas}
+ />, { context: { t: str => str + '_t' } })
+ instance = wrapper.instance()
+ })
+
+ afterEach(() => {
+ propsMethodSpies.setMaxModeTo.resetHistory()
+ propsMethodSpies.updateSendAmount.resetHistory()
+ propsMethodSpies.updateSendAmountError.resetHistory()
+ propsMethodSpies.updateGasFeeError.resetHistory()
+ SendAmountRow.prototype.validateAmount.resetHistory()
+ SendAmountRow.prototype.updateAmount.resetHistory()
+ })
+
+ describe('validateAmount', () => {
+
+ it('should call updateSendAmountError with the correct params', () => {
+ assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendAmountError.getCall(0).args,
+ [{
+ amount: 'someAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 7,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: { address: 'mockTokenAddress' },
+ tokenBalance: 'mockTokenBalance',
+ }]
+ )
+ })
+
+ it('should call updateGasFeeError if selectedToken is truthy', () => {
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateGasFeeError.getCall(0).args,
+ [{
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 7,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: { address: 'mockTokenAddress' },
+ tokenBalance: 'mockTokenBalance',
+ }]
+ )
+ })
+
+ it('should call not updateGasFeeError if selectedToken is falsey', () => {
+ wrapper.setProps({ selectedToken: null })
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ })
+
+ })
+
+ describe('updateAmount', () => {
+
+ it('should call setMaxModeTo', () => {
+ assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
+ instance.updateAmount('someAmount')
+ assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.setMaxModeTo.getCall(0).args,
+ [false]
+ )
+ })
+
+ it('should call updateSendAmount', () => {
+ assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
+ instance.updateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendAmount.getCall(0).args,
+ ['someAmount']
+ )
+ })
+
+ })
+
+ describe('render', () => {
+ it('should render a SendRowWrapper component', () => {
+ assert.equal(wrapper.find(SendRowWrapper).length, 1)
+ })
+
+ it('should pass the correct props to SendRowWrapper', () => {
+ const {
+ errorType,
+ label,
+ showError,
+ } = wrapper.find(SendRowWrapper).props()
+
+ assert.equal(errorType, 'amount')
+
+ assert.equal(label, 'amount_t:')
+
+ assert.equal(showError, false)
+ })
+
+ it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => {
+ assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton))
+ })
+
+ it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => {
+ assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput))
+ })
+
+ it('should render the UserPreferencedTokenInput with the correct props', () => {
+ const {
+ onBlur,
+ onChange,
+ error,
+ value,
+ } = wrapper.find(SendRowWrapper).childAt(1).props()
+ assert.equal(error, false)
+ assert.equal(value, 'mockAmount')
+ assert.equal(SendAmountRow.prototype.updateGas.callCount, 0)
+ assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
+ onBlur('mockNewAmount')
+ assert.equal(SendAmountRow.prototype.updateGas.callCount, 1)
+ assert.deepEqual(
+ SendAmountRow.prototype.updateGas.getCall(0).args,
+ ['mockNewAmount']
+ )
+ assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
+ assert.deepEqual(
+ SendAmountRow.prototype.updateAmount.getCall(0).args,
+ ['mockNewAmount']
+ )
+ assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
+ onChange('mockNewAmount')
+ assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
+ assert.deepEqual(
+ SendAmountRow.prototype.validateAmount.getCall(0).args,
+ ['mockNewAmount']
+ )
+ })
+ })
+})
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/app/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
new file mode 100644
index 000000000..fe3ac9aa1
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import { getSendFromObject } from '../../send.selectors.js'
+import SendFromRow from './send-from-row.component'
+
+function mapStateToProps (state) {
+ return {
+ from: getSendFromObject(state),
+ }
+}
+
+export default connect(mapStateToProps)(SendFromRow)
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/app/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
new file mode 100644
index 000000000..18811c57e
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendFromRow from '../send-from-row.component.js'
+import AccountListItem from '../../../account-list-item'
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+
+describe('SendFromRow Component', function () {
+ describe('render', () => {
+ const wrapper = shallow(
+ <SendFromRow
+ from={ { address: 'mockAddress' } }
+ />,
+ { context: { t: str => str + '_t' } }
+ )
+
+ it('should render a SendRowWrapper component', () => {
+ assert.equal(wrapper.find(SendRowWrapper).length, 1)
+ })
+
+ it('should pass the correct props to SendRowWrapper', () => {
+ const { label } = wrapper.find(SendRowWrapper).props()
+ assert.equal(label, 'from_t:')
+ })
+
+ it('should render the FromDropdown with the correct props', () => {
+ const { account } = wrapper.find(AccountListItem).props()
+ assert.deepEqual(account, { address: 'mockAddress' })
+ })
+ })
+})
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..fd771ea77
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js
@@ -0,0 +1,26 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../send-from-row.container.js', {
+ 'react-redux': {
+ connect: ms => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+ '../../send.selectors.js': {
+ getSendFromObject: (s) => `mockFrom:${s}`,
+ },
+})
+
+describe('send-from-row container', () => {
+ describe('mapStateToProps()', () => {
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ from: 'mockFrom:mockState',
+ })
+ })
+ })
+})
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/app/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
new file mode 100644
index 000000000..cb4180508
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
@@ -0,0 +1,61 @@
+import React from 'react'
+import assert from 'assert'
+import {shallow} from 'enzyme'
+import GasFeeDisplay from '../gas-fee-display.component'
+import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display'
+import sinon from 'sinon'
+
+
+const propsMethodSpies = {
+ showCustomizeGasModal: sinon.spy(),
+ onReset: sinon.spy(),
+}
+
+describe('GasFeeDisplay Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasFeeDisplay
+ conversionRate={20}
+ gasTotal={'mockGasTotal'}
+ primaryCurrency={'mockPrimaryCurrency'}
+ convertedCurrency={'mockConvertedCurrency'}
+ showGasButtonGroup={propsMethodSpies.showCustomizeGasModal}
+ onReset={propsMethodSpies.onReset}
+ />, {context: {t: str => str + '_t'}})
+ })
+
+ afterEach(() => {
+ propsMethodSpies.showCustomizeGasModal.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a CurrencyDisplay component', () => {
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
+ })
+
+ it('should render the CurrencyDisplay with the correct props', () => {
+ const {
+ type,
+ value,
+ } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props()
+ assert.equal(type, 'PRIMARY')
+ assert.equal(value, 'mockGasTotal')
+ })
+
+ it('should render the reset button with the correct props', () => {
+ const {
+ onClick,
+ className,
+ } = wrapper.find('button').props()
+ assert.equal(className, 'gas-fee-reset')
+ assert.equal(propsMethodSpies.onReset.callCount, 0)
+ onClick()
+ assert.equal(propsMethodSpies.onReset.callCount, 1)
+ })
+
+ it('should render the reset button with the correct text', () => {
+ assert.equal(wrapper.find('button').text(), 'reset_t')
+ })
+ })
+})
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/app/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
new file mode 100644
index 000000000..79c838543
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js
@@ -0,0 +1,19 @@
+const selectors = {
+ gasFeeIsInError,
+ getGasLoadingError,
+ getGasButtonGroupShown,
+}
+
+module.exports = selectors
+
+function getGasLoadingError (state) {
+ return state.send.errors.gasLoading
+}
+
+function gasFeeIsInError (state) {
+ return Boolean(state.send.errors.gasFee)
+}
+
+function getGasButtonGroupShown (state) {
+ return state.send.gasButtonGroupShown
+}
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..08f26854e
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
@@ -0,0 +1,104 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import SendGasRow from '../send-gas-row.component.js'
+
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
+import GasPriceButtonGroup from '../../../../gas-customization/gas-price-button-group'
+
+const propsMethodSpies = {
+ showCustomizeGasModal: sinon.spy(),
+ resetGasButtons: sinon.spy(),
+}
+
+describe('SendGasRow Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendGasRow
+ conversionRate={20}
+ convertedCurrency={'mockConvertedCurrency'}
+ gasFeeError={'mockGasFeeError'}
+ gasLoadingError={false}
+ gasTotal={'mockGasTotal'}
+ gasButtonGroupShown={false}
+ showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
+ resetGasButtons={propsMethodSpies.resetGasButtons}
+ gasPriceButtonGroupProps={{
+ someGasPriceButtonGroupProp: 'foo',
+ anotherGasPriceButtonGroupProp: 'bar',
+ }}
+ />, { context: { t: str => str + '_t', metricsEvent: () => ({}) } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.resetGasButtons.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a SendRowWrapper component', () => {
+ assert.equal(wrapper.find(SendRowWrapper).length, 1)
+ })
+
+ it('should pass the correct props to SendRowWrapper', () => {
+ const {
+ label,
+ showError,
+ errorType,
+ } = wrapper.find(SendRowWrapper).props()
+
+ assert.equal(label, 'transactionFee_t:')
+ assert.equal(showError, 'mockGasFeeError')
+ assert.equal(errorType, 'gasFee')
+ })
+
+ it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => {
+ assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay))
+ })
+
+ it('should render the GasFeeDisplay with the correct props', () => {
+ const {
+ conversionRate,
+ convertedCurrency,
+ gasLoadingError,
+ gasTotal,
+ onReset,
+ } = wrapper.find(SendRowWrapper).childAt(0).props()
+ assert.equal(conversionRate, 20)
+ assert.equal(convertedCurrency, 'mockConvertedCurrency')
+ assert.equal(gasLoadingError, false)
+ assert.equal(gasTotal, 'mockGasTotal')
+ assert.equal(propsMethodSpies.resetGasButtons.callCount, 0)
+ onReset()
+ assert.equal(propsMethodSpies.resetGasButtons.callCount, 1)
+ })
+
+ it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => {
+ wrapper.setProps({ gasButtonGroupShown: true })
+ const rendered = wrapper.find(SendRowWrapper).childAt(0)
+ assert.equal(rendered.children().length, 2)
+
+ const gasPriceButtonGroup = rendered.childAt(0)
+ assert(gasPriceButtonGroup.is(GasPriceButtonGroup))
+ assert(gasPriceButtonGroup.hasClass('gas-price-button-group--small'))
+ assert.equal(gasPriceButtonGroup.props().showCheck, false)
+ assert.equal(gasPriceButtonGroup.props().someGasPriceButtonGroupProp, 'foo')
+ assert.equal(gasPriceButtonGroup.props().anotherGasPriceButtonGroupProp, 'bar')
+ })
+
+ it('should render an advanced options button if gasButtonGroupShown is true', () => {
+ wrapper.setProps({ gasButtonGroupShown: true })
+ const rendered = wrapper.find(SendRowWrapper).childAt(0)
+ assert.equal(rendered.children().length, 2)
+
+ const advancedOptionsButton = rendered.childAt(1)
+ assert.equal(advancedOptionsButton.text(), 'advancedOptions_t')
+
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
+ advancedOptionsButton.props().onClick()
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
+ })
+ })
+})
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/app/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
new file mode 100644
index 000000000..bd3c9a257
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
@@ -0,0 +1,62 @@
+import assert from 'assert'
+import {
+ gasFeeIsInError,
+ getGasLoadingError,
+ getGasButtonGroupShown,
+} from '../send-gas-row.selectors.js'
+
+describe('send-gas-row selectors', () => {
+
+ describe('getGasLoadingError()', () => {
+ it('should return send.errors.gasLoading', () => {
+ const state = {
+ send: {
+ errors: {
+ gasLoading: 'abc',
+ },
+ },
+ }
+
+ assert.equal(getGasLoadingError(state), 'abc')
+ })
+ })
+
+ describe('gasFeeIsInError()', () => {
+ it('should return true if send.errors.gasFee is truthy', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: 'def',
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), true)
+ })
+
+ it('should return false send.errors.gasFee is falsely', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: null,
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), false)
+ })
+ })
+
+ describe('getGasButtonGroupShown()', () => {
+ it('should return send.gasButtonGroupShown', () => {
+ const state = {
+ send: {
+ gasButtonGroupShown: 'foobar',
+ },
+ }
+
+ assert.equal(getGasButtonGroupShown(state), 'foobar')
+ })
+ })
+
+})
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/app/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
new file mode 100644
index 000000000..fd4d19ef7
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js
@@ -0,0 +1 @@
+export { default } from './send-row-warning-message.container'
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..f1caa8f99
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class SendRowWarningMessage extends Component {
+
+ static propTypes = {
+ warnings: PropTypes.object,
+ warningType: PropTypes.string,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { warnings, warningType } = this.props
+
+ const warningMessage = warningType in warnings && warnings[warningType]
+
+ return (
+ warningMessage
+ ? <div className="send-v2__warning">{this.context.t(warningMessage)}</div>
+ : null
+ )
+ }
+
+}
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..7df14fd96
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import { getSendWarnings } from '../../../send.selectors'
+import SendRowWarningMessage from './send-row-warning-message.component'
+
+export default connect(mapStateToProps)(SendRowWarningMessage)
+
+function mapStateToProps (state, ownProps) {
+ return {
+ warnings: getSendWarnings(state),
+ warningType: ownProps.warningType,
+ }
+}
diff --git a/ui/app/components/page-container/tests/page-container.component.test.js 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/page-container/tests/page-container.component.test.js
+++ 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/app/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
new file mode 100644
index 000000000..bd803d833
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendRowWarningMessage from '../send-row-warning-message.component.js'
+
+describe('SendRowWarningMessage Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendRowWarningMessage
+ warnings={{ warning1: 'abc', warning2: 'def' }}
+ warningType={'warning3'}
+ />, { context: { t: str => str + '_t' } })
+ })
+
+ describe('render', () => {
+ it('should render null if the passed warnings do not contain a warning of warningType', () => {
+ assert.equal(wrapper.find('.send-v2__warning').length, 0)
+ assert.equal(wrapper.html(), null)
+ })
+
+ it('should render a warning message if the passed warnings contain a warning of warningType', () => {
+ wrapper.setProps({ warnings: { warning1: 'abc', warning2: 'def', warning3: 'xyz' } })
+ assert.equal(wrapper.find('.send-v2__warning').length, 1)
+ assert.equal(wrapper.find('.send-v2__warning').text(), 'xyz_t')
+ })
+ })
+})
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..225bf056c
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
@@ -0,0 +1,28 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../send-row-warning-message.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+ '../../../send.selectors': { getSendWarnings: (s) => `mockWarnings:${s}` },
+})
+
+describe('send-row-warning-message container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState', { warningType: 'someType' }), {
+ warnings: 'mockWarnings:mockState',
+ warningType: 'someType' })
+ })
+
+ })
+
+})
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/app/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
new file mode 100644
index 000000000..a6160d335
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js
@@ -0,0 +1,24 @@
+const selectors = {
+ getToDropdownOpen,
+ getTokens,
+ sendToIsInError,
+ sendToIsInWarning,
+}
+
+module.exports = selectors
+
+function getToDropdownOpen (state) {
+ return state.send.toDropdownOpen
+}
+
+function sendToIsInError (state) {
+ return Boolean(state.send.errors.to)
+}
+
+function sendToIsInWarning (state) {
+ return Boolean(state.send.warnings.to)
+}
+
+function getTokens (state) {
+ return state.metamask.tokens
+}
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/app/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
new file mode 100644
index 000000000..d4d054057
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js
@@ -0,0 +1,166 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import proxyquire from 'proxyquire'
+
+const SendToRow = proxyquire('../send-to-row.component.js', {
+ './send-to-row.utils.js': {
+ getToErrorObject: (to, toError) => ({
+ to: to === false ? null : `mockToErrorObject:${to}${toError}`,
+ }),
+ getToWarningObject: (to, toWarning) => ({
+ to: to === false ? null : `mockToWarningObject:${to}${toWarning}`,
+ }),
+ },
+}).default
+
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+import EnsInput from '../../../../ens-input'
+
+const propsMethodSpies = {
+ closeToDropdown: sinon.spy(),
+ openToDropdown: sinon.spy(),
+ updateGas: sinon.spy(),
+ updateSendTo: sinon.spy(),
+ updateSendToError: sinon.spy(),
+ updateSendToWarning: sinon.spy(),
+}
+
+sinon.spy(SendToRow.prototype, 'handleToChange')
+
+describe('SendToRow Component', function () {
+ let wrapper
+ let instance
+
+ beforeEach(() => {
+ wrapper = shallow(<SendToRow
+ closeToDropdown={propsMethodSpies.closeToDropdown}
+ inError={false}
+ inWarning={false}
+ network={'mockNetwork'}
+ openToDropdown={propsMethodSpies.openToDropdown}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ toDropdownOpen={false}
+ updateGas={propsMethodSpies.updateGas}
+ updateSendTo={propsMethodSpies.updateSendTo}
+ updateSendToError={propsMethodSpies.updateSendToError}
+ updateSendToWarning={propsMethodSpies.updateSendToWarning}
+ />, { context: { t: str => str + '_t' } })
+ instance = wrapper.instance()
+ })
+
+ afterEach(() => {
+ propsMethodSpies.closeToDropdown.resetHistory()
+ propsMethodSpies.openToDropdown.resetHistory()
+ propsMethodSpies.updateSendTo.resetHistory()
+ propsMethodSpies.updateSendToError.resetHistory()
+ propsMethodSpies.updateSendToWarning.resetHistory()
+ SendToRow.prototype.handleToChange.resetHistory()
+ })
+
+ describe('handleToChange', () => {
+
+ it('should call updateSendTo', () => {
+ assert.equal(propsMethodSpies.updateSendTo.callCount, 0)
+ instance.handleToChange('mockTo2', 'mockNickname')
+ assert.equal(propsMethodSpies.updateSendTo.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendTo.getCall(0).args,
+ ['mockTo2', 'mockNickname']
+ )
+ })
+
+ it('should call updateSendToError', () => {
+ assert.equal(propsMethodSpies.updateSendToError.callCount, 0)
+ instance.handleToChange('mockTo2', '', 'mockToError')
+ assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendToError.getCall(0).args,
+ [{ to: 'mockToErrorObject:mockTo2mockToError' }]
+ )
+ })
+
+ it('should call updateSendToWarning', () => {
+ assert.equal(propsMethodSpies.updateSendToWarning.callCount, 0)
+ instance.handleToChange('mockTo2', '', '', 'mockToWarning')
+ assert.equal(propsMethodSpies.updateSendToWarning.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendToWarning.getCall(0).args,
+ [{ to: 'mockToWarningObject:mockTo2mockToWarning' }]
+ )
+ })
+
+ it('should not call updateGas if there is a to error', () => {
+ assert.equal(propsMethodSpies.updateGas.callCount, 0)
+ instance.handleToChange('mockTo2')
+ assert.equal(propsMethodSpies.updateGas.callCount, 0)
+ })
+
+ it('should call updateGas if there is no to error', () => {
+ assert.equal(propsMethodSpies.updateGas.callCount, 0)
+ instance.handleToChange(false)
+ assert.equal(propsMethodSpies.updateGas.callCount, 1)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a SendRowWrapper component', () => {
+ assert.equal(wrapper.find(SendRowWrapper).length, 1)
+ })
+
+ it('should pass the correct props to SendRowWrapper', () => {
+ const {
+ errorType,
+ label,
+ showError,
+ } = wrapper.find(SendRowWrapper).props()
+
+ assert.equal(errorType, 'to')
+
+ assert.equal(label, 'to_t: ')
+
+ assert.equal(showError, false)
+ })
+
+ it('should render an EnsInput as a child of the SendRowWrapper', () => {
+ assert(wrapper.find(SendRowWrapper).childAt(0).is(EnsInput))
+ })
+
+ it('should render the EnsInput with the correct props', () => {
+ const {
+ accounts,
+ closeDropdown,
+ dropdownOpen,
+ inError,
+ name,
+ network,
+ onChange,
+ openDropdown,
+ placeholder,
+ to,
+ } = wrapper.find(SendRowWrapper).childAt(0).props()
+ assert.deepEqual(accounts, ['mockAccount'])
+ assert.equal(dropdownOpen, false)
+ assert.equal(inError, false)
+ assert.equal(name, 'address')
+ assert.equal(network, 'mockNetwork')
+ assert.equal(placeholder, 'recipientAddress_t')
+ assert.equal(to, 'mockTo')
+ assert.equal(propsMethodSpies.closeToDropdown.callCount, 0)
+ closeDropdown()
+ assert.equal(propsMethodSpies.closeToDropdown.callCount, 1)
+ assert.equal(propsMethodSpies.openToDropdown.callCount, 0)
+ openDropdown()
+ assert.equal(propsMethodSpies.openToDropdown.callCount, 1)
+ assert.equal(SendToRow.prototype.handleToChange.callCount, 0)
+ onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError', toWarning: 'mockToWarning' })
+ assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
+ assert.deepEqual(
+ SendToRow.prototype.handleToChange.getCall(0).args,
+ ['mockNewTo', 'mockNewNickname', 'mockToError', 'mockToWarning', 'mockNetwork' ]
+ )
+ })
+ })
+})
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/app/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
new file mode 100644
index 000000000..0fa342d1e
--- /dev/null
+++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
@@ -0,0 +1,59 @@
+import assert from 'assert'
+import {
+ getToDropdownOpen,
+ getTokens,
+ sendToIsInError,
+} from '../send-to-row.selectors.js'
+
+describe('send-to-row selectors', () => {
+
+ describe('getToDropdownOpen()', () => {
+ it('should return send.getToDropdownOpen', () => {
+ const state = {
+ send: {
+ toDropdownOpen: false,
+ },
+ }
+
+ assert.equal(getToDropdownOpen(state), false)
+ })
+ })
+
+ describe('sendToIsInError()', () => {
+ it('should return true if send.errors.to is truthy', () => {
+ const state = {
+ send: {
+ errors: {
+ to: 'abc',
+ },
+ },
+ }
+
+ assert.equal(sendToIsInError(state), true)
+ })
+
+ it('should return false if send.errors.to is falsy', () => {
+ const state = {
+ send: {
+ errors: {
+ to: null,
+ },
+ },
+ }
+
+ assert.equal(sendToIsInError(state), false)
+ })
+ })
+
+ describe('getTokens()', () => {
+ it('should return empty array if no tokens in state', () => {
+ const state = {
+ metamask: {
+ tokens: [],
+ },
+ }
+
+ assert.deepStrictEqual(getTokens(state), [])
+ })
+ })
+})
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/app/send/tests/send-selectors-test-data.js b/ui/app/components/app/send/tests/send-selectors-test-data.js
new file mode 100644
index 000000000..d43d7c650
--- /dev/null
+++ b/ui/app/components/app/send/tests/send-selectors-test-data.js
@@ -0,0 +1,232 @@
+module.exports = {
+ 'metamask': {
+ 'isInitialized': true,
+ 'isUnlocked': true,
+ 'featureFlags': {'sendHexData': true},
+ 'rpcTarget': 'https://rawtestrpc.metamask.io/',
+ 'identities': {
+ '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
+ 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
+ 'name': 'Send Account 1',
+ },
+ '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
+ 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ 'name': 'Send Account 2',
+ },
+ '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
+ 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
+ 'name': 'Send Account 3',
+ },
+ '0xd85a4b6a394794842887b8284293d69163007bbb': {
+ 'address': '0xd85a4b6a394794842887b8284293d69163007bbb',
+ 'name': 'Send Account 4',
+ },
+ },
+ 'cachedBalances': {},
+ 'currentBlockGasLimit': '0x4c1878',
+ 'currentCurrency': 'USD',
+ 'conversionRate': 1200.88200327,
+ 'conversionDate': 1489013762,
+ 'nativeCurrency': 'ETH',
+ 'noActiveNotices': true,
+ 'frequentRpcList': [],
+ 'network': '3',
+ 'accounts': {
+ '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
+ 'code': '0x',
+ 'balance': '0x47c9d71831c76efe',
+ 'nonce': '0x1b',
+ 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
+ },
+ '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
+ 'code': '0x',
+ 'balance': '0x37452b1315889f80',
+ 'nonce': '0xa',
+ 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ },
+ '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
+ 'code': '0x',
+ 'balance': '0x30c9d71831c76efe',
+ 'nonce': '0x1c',
+ 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
+ },
+ '0xd85a4b6a394794842887b8284293d69163007bbb': {
+ 'code': '0x',
+ 'balance': '0x0',
+ 'nonce': '0x0',
+ 'address': '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ },
+ 'addressBook': [
+ {
+ 'address': '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ 'name': 'Address Book Account 1',
+ },
+ ],
+ 'tokens': [
+ {
+ 'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896',
+ 'decimals': 18,
+ 'symbol': 'ABC',
+ },
+ {
+ 'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ 'decimals': 4,
+ 'symbol': 'DEF',
+ },
+ {
+ 'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b',
+ 'decimals': 18,
+ 'symbol': 'GHI',
+ },
+ ],
+ 'tokenExchangeRates': {
+ 'def_eth': {
+ rate: 2.0,
+ },
+ 'ghi_eth': {
+ rate: 31.01,
+ },
+ },
+ 'transactions': {},
+ 'selectedAddressTxList': [
+ {
+ 'id': 'mockTokenTx1',
+ 'txParams': {
+ 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ 'time': 1700000000000,
+ },
+ {
+ 'id': 'mockTokenTx2',
+ 'txParams': {
+ 'to': '0xafaketokenaddress',
+ },
+ 'time': 1600000000000,
+ },
+ {
+ 'id': 'mockTokenTx3',
+ 'txParams': {
+ 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ 'time': 1500000000000,
+ },
+ {
+ 'id': 'mockEthTx1',
+ 'txParams': {
+ 'to': '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ 'time': 1400000000000,
+ },
+ ],
+ 'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ 'unapprovedMsgs': {
+ '0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 },
+ '0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 },
+ '0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 },
+ },
+ 'unapprovedMsgCount': 0,
+ 'unapprovedPersonalMsgs': {},
+ 'unapprovedPersonalMsgCount': 0,
+ 'keyringTypes': [
+ 'Simple Key Pair',
+ 'HD Key Tree',
+ ],
+ 'keyrings': [
+ {
+ 'type': 'HD Key Tree',
+ 'accounts': [
+ 'fdea65c8e26263f6d9a1b5de9555d2931a33b825',
+ 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ '2f8d4a878cfa04a6e60d46362f5644deab66572d',
+ ],
+ },
+ {
+ 'type': 'Simple Key Pair',
+ 'accounts': [
+ '0xd85a4b6a394794842887b8284293d69163007bbb',
+ ],
+ },
+ ],
+ 'selectedAddress': '0xd85a4b6a394794842887b8284293d69163007bbb',
+ 'provider': {
+ 'type': 'testnet',
+ },
+ 'shapeShiftTxList': [
+ { id: 'shapeShiftTx1', 'time': 1675000000000 },
+ { id: 'shapeShiftTx2', 'time': 1575000000000 },
+ { id: 'shapeShiftTx3', 'time': 1475000000000 },
+ ],
+ 'lostAccounts': [],
+ 'send': {
+ 'gasLimit': '0xFFFF',
+ 'gasPrice': '0xaa',
+ 'gasTotal': '0xb451dc41b578',
+ 'tokenBalance': 3434,
+ 'from': {
+ 'address': '0xabcdefg',
+ 'balance': '0x5f4e3d2c1',
+ },
+ 'to': '0x987fedabc',
+ 'amount': '0x080',
+ 'memo': '',
+ 'errors': {
+ 'someError': null,
+ },
+ 'maxModeOn': false,
+ 'editingTransactionId': 97531,
+ 'forceGasMin': true,
+ },
+ 'unapprovedTxs': {
+ '4768706228115573': {
+ 'id': 4768706228115573,
+ 'time': 1487363153561,
+ 'status': 'unapproved',
+ 'gasMultiplier': 1,
+ 'metamaskNetworkId': '3',
+ 'txParams': {
+ 'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ 'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
+ 'value': '0xde0b6b3a7640000',
+ 'metamaskId': 4768706228115573,
+ 'metamaskNetworkId': '3',
+ 'gas': '0x5209',
+ },
+ 'gasLimitSpecified': false,
+ 'estimatedGas': '0x5209',
+ 'txFee': '17e0186e60800',
+ 'txValue': 'de0b6b3a7640000',
+ 'maxCost': 'de234b52e4a0800',
+ 'gasPrice': '4a817c800',
+ },
+ },
+ 'currentLocale': 'en',
+ recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'],
+ },
+ 'appState': {
+ 'menuOpen': false,
+ 'currentView': {
+ 'name': 'accountDetail',
+ 'detailView': null,
+ 'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ },
+ 'accountDetail': {
+ 'subview': 'transactions',
+ },
+ 'modal': {
+ 'modalState': {},
+ 'previousModalState': {},
+ },
+ 'transForward': true,
+ 'isLoading': false,
+ 'warning': null,
+ 'scrollToBottom': false,
+ 'forgottenPassword': null,
+ },
+ 'identities': {},
+ 'send': {
+ 'fromDropdownOpen': false,
+ 'toDropdownOpen': false,
+ 'errors': { 'someError': null },
+ },
+}
diff --git a/ui/app/components/app/send/tests/send-selectors.test.js b/ui/app/components/app/send/tests/send-selectors.test.js
new file mode 100644
index 000000000..cdc86fe59
--- /dev/null
+++ b/ui/app/components/app/send/tests/send-selectors.test.js
@@ -0,0 +1,705 @@
+import assert from 'assert'
+import sinon from 'sinon'
+import selectors from '../send.selectors.js'
+const {
+ accountsWithSendEtherInfoSelector,
+ // autoAddToBetaUI,
+ getAddressBook,
+ getBlockGasLimit,
+ getAmountConversionRate,
+ getConversionRate,
+ getCurrentAccountWithSendEtherInfo,
+ getCurrentCurrency,
+ getCurrentNetwork,
+ getCurrentViewContext,
+ getNativeCurrency,
+ getForceGasMin,
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getPrimaryCurrency,
+ getRecentBlocks,
+ getSelectedAccount,
+ getSelectedAddress,
+ getSelectedIdentity,
+ getSelectedToken,
+ getSelectedTokenContract,
+ getSelectedTokenExchangeRate,
+ getSelectedTokenToFiatRate,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendErrors,
+ getSendFrom,
+ getSendFromBalance,
+ getSendFromObject,
+ getSendHexDataFeatureFlagState,
+ getSendMaxModeState,
+ getSendTo,
+ getSendToAccounts,
+ getTokenBalance,
+ getTokenExchangeRate,
+ getUnapprovedTxs,
+ transactionsSelector,
+} = selectors
+import mockState from './send-selectors-test-data'
+
+describe('send selectors', () => {
+ const tempGlobalEth = Object.assign({}, global.eth)
+ beforeEach(() => {
+ global.eth = {
+ contract: sinon.stub().returns({
+ at: address => 'mockAt:' + address,
+ }),
+ }
+ })
+
+ afterEach(() => {
+ global.eth = tempGlobalEth
+ })
+
+ describe('accountsWithSendEtherInfoSelector()', () => {
+ it('should return an array of account objects with name info from identities', () => {
+ assert.deepEqual(
+ accountsWithSendEtherInfoSelector(mockState),
+ [
+ {
+ code: '0x',
+ balance: '0x47c9d71831c76efe',
+ nonce: '0x1b',
+ address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
+ name: 'Send Account 1',
+ },
+ {
+ code: '0x',
+ balance: '0x37452b1315889f80',
+ nonce: '0xa',
+ address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ name: 'Send Account 2',
+ },
+ {
+ code: '0x',
+ balance: '0x30c9d71831c76efe',
+ nonce: '0x1c',
+ address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
+ name: 'Send Account 3',
+ },
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ },
+ ]
+ )
+ })
+ })
+
+ // describe('autoAddToBetaUI()', () => {
+ // it('should', () => {
+ // assert.deepEqual(
+ // autoAddToBetaUI(mockState),
+
+ // )
+ // })
+ // })
+
+ describe('getAddressBook()', () => {
+ it('should return the address book', () => {
+ assert.deepEqual(
+ getAddressBook(mockState),
+ [
+ {
+ address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ name: 'Address Book Account 1',
+ },
+ ],
+ )
+ })
+ })
+
+ describe('getAmountConversionRate()', () => {
+ it('should return the token conversion rate if a token is selected', () => {
+ assert.equal(
+ getAmountConversionRate(mockState),
+ 2401.76400654
+ )
+ })
+
+ it('should return the eth conversion rate if no token is selected', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, { selectedTokenAddress: null }),
+ }
+ assert.equal(
+ getAmountConversionRate(editedMockState),
+ 1200.88200327
+ )
+ })
+ })
+
+ describe('getBlockGasLimit', () => {
+ it('should return the current block gas limit', () => {
+ assert.deepEqual(
+ getBlockGasLimit(mockState),
+ '0x4c1878'
+ )
+ })
+ })
+
+ describe('getConversionRate()', () => {
+ it('should return the eth conversion rate', () => {
+ assert.deepEqual(
+ getConversionRate(mockState),
+ 1200.88200327
+ )
+ })
+ })
+
+ describe('getCurrentAccountWithSendEtherInfo()', () => {
+ it('should return the currently selected account with identity info', () => {
+ assert.deepEqual(
+ getCurrentAccountWithSendEtherInfo(mockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getCurrentCurrency()', () => {
+ it('should return the currently selected currency', () => {
+ assert.equal(
+ getCurrentCurrency(mockState),
+ 'USD'
+ )
+ })
+ })
+
+ describe('getNativeCurrency()', () => {
+ it('should return the ticker symbol of the selected network', () => {
+ assert.equal(
+ getNativeCurrency(mockState),
+ 'ETH'
+ )
+ })
+ })
+
+ describe('getCurrentNetwork()', () => {
+ it('should return the id of the currently selected network', () => {
+ assert.equal(
+ getCurrentNetwork(mockState),
+ '3'
+ )
+ })
+ })
+
+ describe('getCurrentViewContext()', () => {
+ it('should return the context of the current view', () => {
+ assert.equal(
+ getCurrentViewContext(mockState),
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
+ )
+ })
+ })
+
+ describe('getForceGasMin()', () => {
+ it('should get the send.forceGasMin property', () => {
+ assert.equal(
+ getForceGasMin(mockState),
+ true
+ )
+ })
+ })
+
+ describe('getGasLimit()', () => {
+ it('should return the send.gasLimit', () => {
+ assert.equal(
+ getGasLimit(mockState),
+ '0xFFFF'
+ )
+ })
+ })
+
+ describe('getGasPrice()', () => {
+ it('should return the send.gasPrice', () => {
+ assert.equal(
+ getGasPrice(mockState),
+ '0xaa'
+ )
+ })
+ })
+
+ describe('getGasTotal()', () => {
+ it('should return the send.gasTotal', () => {
+ assert.equal(
+ getGasTotal(mockState),
+ 'a9ff56'
+ )
+ })
+ })
+
+ describe('getPrimaryCurrency()', () => {
+ it('should return the symbol of the selected token', () => {
+ assert.equal(
+ getPrimaryCurrency(mockState),
+ 'DEF'
+ )
+ })
+ })
+
+ describe('getRecentBlocks()', () => {
+ it('should return the recent blocks', () => {
+ assert.deepEqual(
+ getRecentBlocks(mockState),
+ ['mockBlock1', 'mockBlock2', 'mockBlock3']
+ )
+ })
+ })
+
+ describe('getSelectedAccount()', () => {
+ it('should return the currently selected account', () => {
+ assert.deepEqual(
+ getSelectedAccount(mockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ }
+ )
+ })
+ })
+
+ describe('getSelectedAddress()', () => {
+ it('should', () => {
+ assert.equal(
+ getSelectedAddress(mockState),
+ '0xd85a4b6a394794842887b8284293d69163007bbb'
+ )
+ })
+ })
+
+ describe('getSelectedIdentity()', () => {
+ it('should return the identity object of the currently selected address', () => {
+ assert.deepEqual(
+ getSelectedIdentity(mockState),
+ {
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getSelectedToken()', () => {
+ it('should return the currently selected token if selected', () => {
+ assert.deepEqual(
+ getSelectedToken(mockState),
+ {
+ address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ decimals: 4,
+ symbol: 'DEF',
+ }
+ )
+ })
+
+ it('should return the send token if none is currently selected, but a send token exists', () => {
+ const mockSendToken = {
+ address: '0x123456708414189a58339873ab429b6c47ab92d3',
+ decimals: 4,
+ symbol: 'JKL',
+ }
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ selectedTokenAddress: null,
+ send: {
+ token: mockSendToken,
+ },
+ }),
+ }
+ assert.deepEqual(
+ getSelectedToken(editedMockState),
+ Object.assign({}, mockSendToken)
+ )
+ })
+ })
+
+ describe('getSelectedTokenContract()', () => {
+ it('should return the contract at the selected token address', () => {
+ assert.equal(
+ getSelectedTokenContract(mockState),
+ 'mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3'
+ )
+ })
+
+ it('should return null if no token is selected', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
+ assert.equal(
+ getSelectedTokenContract(Object.assign({}, mockState, { metamask: modifiedMetamaskState })),
+ null
+ )
+ })
+ })
+
+ describe('getSelectedTokenExchangeRate()', () => {
+ it('should return the exchange rate for the selected token', () => {
+ assert.equal(
+ getSelectedTokenExchangeRate(mockState),
+ 2.0
+ )
+ })
+ })
+
+ describe('getSelectedTokenToFiatRate()', () => {
+ it('should return rate for converting the selected token to fiat', () => {
+ assert.equal(
+ getSelectedTokenToFiatRate(mockState),
+ 2401.76400654
+ )
+ })
+ })
+
+ describe('getSendAmount()', () => {
+ it('should return the send.amount', () => {
+ assert.equal(
+ getSendAmount(mockState),
+ '0x080'
+ )
+ })
+ })
+
+ describe('getSendEditingTransactionId()', () => {
+ it('should return the send.editingTransactionId', () => {
+ assert.equal(
+ getSendEditingTransactionId(mockState),
+ 97531
+ )
+ })
+ })
+
+ describe('getSendErrors()', () => {
+ it('should return the send.errors', () => {
+ assert.deepEqual(
+ getSendErrors(mockState),
+ { someError: null }
+ )
+ })
+ })
+
+ describe('getSendHexDataFeatureFlagState()', () => {
+ it('should return the sendHexData feature flag state', () => {
+ assert.deepEqual(
+ getSendHexDataFeatureFlagState(mockState),
+ true
+ )
+ })
+ })
+
+ describe('getSendFrom()', () => {
+ it('should return the send.from', () => {
+ assert.deepEqual(
+ getSendFrom(mockState),
+ {
+ address: '0xabcdefg',
+ balance: '0x5f4e3d2c1',
+ }
+ )
+ })
+ })
+
+ describe('getSendFromBalance()', () => {
+ it('should get the send.from balance if it exists', () => {
+ assert.equal(
+ getSendFromBalance(mockState),
+ '0x5f4e3d2c1'
+ )
+ })
+
+ it('should get the selected account balance if the send.from does not exist', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ send: {
+ from: null,
+ },
+ }),
+ }
+ assert.equal(
+ getSendFromBalance(editedMockState),
+ '0x0'
+ )
+ })
+ })
+
+ describe('getSendFromObject()', () => {
+ it('should return send.from if it exists', () => {
+ assert.deepEqual(
+ getSendFromObject(mockState),
+ {
+ address: '0xabcdefg',
+ balance: '0x5f4e3d2c1',
+ }
+ )
+ })
+
+ it('should return the current account with send ether info if send.from does not exist', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ send: {
+ from: null,
+ },
+ }),
+ }
+ assert.deepEqual(
+ getSendFromObject(editedMockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getSendMaxModeState()', () => {
+ it('should return send.maxModeOn', () => {
+ assert.equal(
+ getSendMaxModeState(mockState),
+ false
+ )
+ })
+ })
+
+ describe('getSendTo()', () => {
+ it('should return send.to', () => {
+ assert.equal(
+ getSendTo(mockState),
+ '0x987fedabc'
+ )
+ })
+ })
+
+ describe('getSendToAccounts()', () => {
+ it('should return an array including all the users accounts and the address book', () => {
+ assert.deepEqual(
+ getSendToAccounts(mockState),
+ [
+ {
+ code: '0x',
+ balance: '0x47c9d71831c76efe',
+ nonce: '0x1b',
+ address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
+ name: 'Send Account 1',
+ },
+ {
+ code: '0x',
+ balance: '0x37452b1315889f80',
+ nonce: '0xa',
+ address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ name: 'Send Account 2',
+ },
+ {
+ code: '0x',
+ balance: '0x30c9d71831c76efe',
+ nonce: '0x1c',
+ address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
+ name: 'Send Account 3',
+ },
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ },
+ {
+ address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ name: 'Address Book Account 1',
+ },
+ ]
+ )
+ })
+ })
+
+ describe('getTokenBalance()', () => {
+ it('should', () => {
+ assert.equal(
+ getTokenBalance(mockState),
+ 3434
+ )
+ })
+ })
+
+ describe('getTokenExchangeRate()', () => {
+ it('should return the passed tokens exchange rates', () => {
+ assert.equal(
+ getTokenExchangeRate(mockState, 'GHI'),
+ 31.01
+ )
+ })
+ })
+
+ describe('getUnapprovedTxs()', () => {
+ it('should return the unapproved txs', () => {
+ assert.deepEqual(
+ getUnapprovedTxs(mockState),
+ {
+ 4768706228115573: {
+ id: 4768706228115573,
+ time: 1487363153561,
+ status: 'unapproved',
+ gasMultiplier: 1,
+ metamaskNetworkId: '3',
+ txParams: {
+ from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
+ value: '0xde0b6b3a7640000',
+ metamaskId: 4768706228115573,
+ metamaskNetworkId: '3',
+ gas: '0x5209',
+ },
+ gasLimitSpecified: false,
+ estimatedGas: '0x5209',
+ txFee: '17e0186e60800',
+ txValue: 'de0b6b3a7640000',
+ maxCost: 'de234b52e4a0800',
+ gasPrice: '4a817c800',
+ },
+ }
+ )
+ })
+ })
+
+ describe('transactionsSelector()', () => {
+ it('should return the selected addresses selected token transactions', () => {
+ assert.deepEqual(
+ transactionsSelector(mockState),
+ [
+ {
+ id: 'mockTokenTx1',
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ time: 1700000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ time: 1500000000000,
+ },
+ ]
+ )
+ })
+
+ it('should return all transactions if no token is selected', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
+ const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
+ assert.deepEqual(
+ transactionsSelector(modifiedState),
+ [
+ {
+ id: 'mockTokenTx1',
+ time: 1700000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ {
+ id: 'unapprovedMessage1',
+ time: 1650000000000,
+ },
+ {
+ id: 'mockTokenTx2',
+ time: 1600000000000,
+ txParams: {
+ to: '0xafaketokenaddress',
+ },
+ },
+ {
+ id: 'unapprovedMessage2',
+ time: 1550000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ time: 1500000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ {
+ id: 'unapprovedMessage3',
+ time: 1450000000000,
+ },
+ {
+ id: 'mockEthTx1',
+ time: 1400000000000,
+ txParams: {
+ to: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ },
+ ]
+ )
+ })
+
+ it('should return shapeshift transactions if current network is 1', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false, network: '1' })
+ const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
+ assert.deepEqual(
+ transactionsSelector(modifiedState),
+ [
+ {
+ id: 'mockTokenTx1',
+ time: 1700000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ { id: 'shapeShiftTx1', 'time': 1675000000000 },
+ {
+ id: 'unapprovedMessage1',
+ time: 1650000000000,
+ },
+ {
+ id: 'mockTokenTx2',
+ time: 1600000000000,
+ txParams: {
+ to: '0xafaketokenaddress',
+ },
+ },
+ { id: 'shapeShiftTx2', 'time': 1575000000000 },
+ {
+ id: 'unapprovedMessage2',
+ time: 1550000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ time: 1500000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ { id: 'shapeShiftTx3', 'time': 1475000000000 },
+ {
+ id: 'unapprovedMessage3',
+ time: 1450000000000,
+ },
+ {
+ id: 'mockEthTx1',
+ time: 1400000000000,
+ txParams: {
+ to: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ },
+ ]
+ )
+ })
+ })
+
+})
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/app/sidebars/sidebar-content.scss b/ui/app/components/app/sidebars/sidebar-content.scss
new file mode 100644
index 000000000..ca6b0a458
--- /dev/null
+++ b/ui/app/components/app/sidebars/sidebar-content.scss
@@ -0,0 +1,112 @@
+.sidebar-left {
+ display: flex;
+
+ .gas-modal-page-container {
+ display: flex;
+
+ .page-container {
+ flex: 1;
+ max-width: 100%;
+
+ &__content {
+ display: flex;
+ overflow-y: initial;
+ }
+
+ @media screen and (max-width: $break-small) {
+ max-width: 344px;
+ min-height: auto;
+ }
+
+ @media screen and (min-width: $break-small) {
+ max-height: none;
+ }
+ }
+
+ .gas-price-chart {
+ margin-left: 10px;
+
+ &__root {
+ max-height: 160px !important;
+ }
+ }
+
+ .page-container__bottom {
+ display: flex;
+ flex-direction: column;
+ flex-flow: space-between;
+ height: 100%;
+ }
+
+ .page-container__content {
+ overflow-y: inherit;
+ }
+
+ .basic-tab-content {
+ height: auto;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #d2d8dd;
+ flex: 1 1 70%;
+
+ @media screen and (max-width: $break-small) {
+ padding-left: 14px;
+ padding-bottom: 21px;
+ }
+
+ .gas-price-button-group--alt {
+ @media screen and (max-width: $break-small) {
+ max-width: 318px;
+
+ &__time-estimate {
+ font-size: 12px;
+ }
+ }
+ }
+ }
+
+ .advanced-tab {
+ @media screen and (min-width: $break-small) {
+ flex: 1 1 70%;
+ }
+
+ &__fee-chart {
+ height: 320px;
+
+ @media screen and (max-width: $break-small) {
+ height: initial;
+ }
+ }
+
+ &__fee-chart__speed-buttons {
+ bottom: 77px;
+
+ @media screen and (max-width: $break-small) {
+ display: none;
+ }
+ }
+ }
+
+ .gas-modal-content {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ &__info-row-wrapper {
+ display: flex;
+ @media screen and (min-width: $break-small) {
+ flex: 1 1 30%;
+ }
+ }
+
+ &__info-row {
+ height: 170px;
+
+ @media screen and (max-width: $break-small) {
+ height: initial;
+ display: flex;
+ justify-content: center;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/app/sidebars/sidebar.component.js b/ui/app/components/app/sidebars/sidebar.component.js
new file mode 100644
index 000000000..b9e0f9e81
--- /dev/null
+++ b/ui/app/components/app/sidebars/sidebar.component.js
@@ -0,0 +1,69 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import WalletView from '../wallet-view'
+import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
+import CustomizeGas from '../gas-customization/gas-modal-page-container/'
+
+export default class Sidebar extends Component {
+
+ static propTypes = {
+ sidebarOpen: PropTypes.bool,
+ hideSidebar: PropTypes.func,
+ sidebarShouldClose: PropTypes.bool,
+ transitionName: PropTypes.string,
+ type: PropTypes.string,
+ sidebarProps: PropTypes.object,
+ onOverlayClose: PropTypes.func,
+ };
+
+ renderOverlay () {
+ const { onOverlayClose } = this.props
+
+ return <div
+ className="sidebar-overlay"
+ onClick={() => {
+ onOverlayClose && onOverlayClose()
+ this.props.hideSidebar()
+ }
+ } />
+ }
+
+ renderSidebarContent () {
+ const { type, sidebarProps = {} } = this.props
+ const { transaction = {} } = sidebarProps
+ switch (type) {
+ case WALLET_VIEW_SIDEBAR:
+ return <WalletView responsiveDisplayClassname={'sidebar-right' } />
+ case 'customize-gas':
+ return <div className={'sidebar-left'}><CustomizeGas transaction={transaction} /></div>
+ default:
+ return null
+ }
+
+ }
+
+ componentDidUpdate (prevProps) {
+ if (!prevProps.sidebarShouldClose && this.props.sidebarShouldClose) {
+ this.props.hideSidebar()
+ }
+ }
+
+ render () {
+ const { transitionName, sidebarOpen, sidebarShouldClose } = this.props
+
+ return (
+ <div>
+ <ReactCSSTransitionGroup
+ transitionName={transitionName}
+ transitionEnterTimeout={300}
+ transitionLeaveTimeout={200}
+ >
+ { sidebarOpen && !sidebarShouldClose ? this.renderSidebarContent() : null }
+ </ReactCSSTransitionGroup>
+ { sidebarOpen && !sidebarShouldClose ? this.renderOverlay() : null }
+ </div>
+ )
+ }
+
+}
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/app/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/tests/sidebars-component.test.js
new file mode 100644
index 000000000..cee22aca8
--- /dev/null
+++ b/ui/app/components/app/sidebars/tests/sidebars-component.test.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import Sidebar from '../sidebar.component.js'
+
+import WalletView from '../../wallet-view'
+import CustomizeGas from '../../gas-customization/gas-modal-page-container/'
+
+const propsMethodSpies = {
+ hideSidebar: sinon.spy(),
+}
+
+describe('Sidebar Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<Sidebar
+ sidebarOpen={false}
+ hideSidebar={propsMethodSpies.hideSidebar}
+ transitionName={'someTransition'}
+ type={'wallet-view'}
+ />)
+ })
+
+ afterEach(() => {
+ propsMethodSpies.hideSidebar.resetHistory()
+ })
+
+ describe('renderOverlay', () => {
+ let renderOverlay
+
+ beforeEach(() => {
+ renderOverlay = shallow(wrapper.instance().renderOverlay())
+ })
+
+ it('should render a overlay element', () => {
+ assert(renderOverlay.hasClass('sidebar-overlay'))
+ })
+
+ it('should pass the correct onClick function to the element', () => {
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 0)
+ renderOverlay.props().onClick()
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 1)
+ })
+ })
+
+ describe('renderSidebarContent', () => {
+ let renderSidebarContent
+
+ beforeEach(() => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ })
+
+ it('should render sidebar content with the correct props', () => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
+ })
+
+ it('should render sidebar content with the correct props', () => {
+ wrapper.setProps({ type: 'customize-gas' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ const renderedSidebarContent = shallow(renderSidebarContent)
+ assert(renderedSidebarContent.hasClass('sidebar-left'))
+ assert(renderedSidebarContent.childAt(0).is(CustomizeGas))
+ })
+
+ it('should not render with an unrecognized type', () => {
+ wrapper.setProps({ type: 'foobar' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent, undefined)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with one child', () => {
+ assert(wrapper.is('div'))
+ assert.equal(wrapper.children().length, 1)
+ })
+
+ it('should render the ReactCSSTransitionGroup without any children', () => {
+ assert(wrapper.children().at(0).is(ReactCSSTransitionGroup))
+ assert.equal(wrapper.children().at(0).children().length, 0)
+ })
+
+ it('should render sidebar content and the overlay if sidebarOpen is true', () => {
+ wrapper.setProps({ sidebarOpen: true })
+ assert.equal(wrapper.children().length, 2)
+ assert(wrapper.children().at(1).hasClass('sidebar-overlay'))
+ assert.equal(wrapper.children().at(0).children().length, 1)
+ assert(wrapper.children().at(0).children().at(0).is(WalletView))
+ })
+ })
+})
diff --git a/ui/app/components/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/app/transaction-activity-log/index.scss b/ui/app/components/app/transaction-activity-log/index.scss
new file mode 100644
index 000000000..00c17e6aa
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/index.scss
@@ -0,0 +1,84 @@
+.transaction-activity-log {
+ &__title {
+ border-bottom: 1px solid #d8d8d8;
+ padding-bottom: 4px;
+ text-transform: capitalize;
+ }
+
+ &__activities-container {
+ padding-top: 8px;
+ }
+
+ &__activity {
+ padding: 4px 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 7px;
+ border-right: 1px solid #909090;
+ }
+
+ &:first-child::after {
+ height: 50%;
+ top: 50%;
+ }
+
+ &:last-child::after {
+ height: 50%;
+ }
+
+ &:first-child:last-child::after {
+ display: none;
+ }
+ }
+
+ &__activity-icon {
+ width: 15px;
+ height: 15px;
+ margin-right: 6px;
+ border-radius: 50%;
+ background: #909090;
+ flex: 0 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1;
+ }
+
+ &__activity-text {
+ color: $dusty-gray;
+ font-size: .75rem;
+ cursor: pointer;
+
+ &:hover {
+ color: $black;
+ }
+ }
+
+ &__value {
+ display: inline;
+ font-weight: 500;
+ }
+
+ &__entry-container {
+ min-width: 0;
+ }
+
+ &__action-link {
+ font-size: .75rem;
+ cursor: pointer;
+ color: $curious-blue;
+ }
+
+ b {
+ font-weight: 500;
+ }
+}
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..a2946e53d
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js
@@ -0,0 +1,101 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionActivityLog from '../transaction-activity-log.component'
+
+describe('TransactionActivityLog Component', () => {
+ it('should render properly', () => {
+ const activities = [
+ {
+ eventKey: 'transactionCreated',
+ hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63',
+ id: 2005383477493174,
+ timestamp: 1543957986150,
+ value: '0x2386f26fc10000',
+ }, {
+ eventKey: 'transactionSubmitted',
+ hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63',
+ id: 2005383477493174,
+ timestamp: 1543957987853,
+ value: '0x1319718a5000',
+ }, {
+ eventKey: 'transactionResubmitted',
+ hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
+ id: 2005383477493175,
+ timestamp: 1543957991563,
+ value: '0x1502634b5800',
+ }, {
+ eventKey: 'transactionConfirmed',
+ hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
+ id: 2005383477493175,
+ timestamp: 1543958029960,
+ value: '0x1502634b5800',
+ },
+ ]
+
+ const wrapper = shallow(
+ <TransactionActivityLog
+ activities={activities}
+ className="test-class"
+ inlineRetryIndex={-1}
+ inlineCancelIndex={-1}
+ nativeCurrency="ETH"
+ onCancel={() => {}}
+ onRetry={() => {}}
+ primaryTransactionStatus="confirmed"
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-activity-log'))
+ assert.ok(wrapper.hasClass('test-class'))
+ })
+
+ it('should render inline retry and cancel buttons', () => {
+ const activities = [
+ {
+ eventKey: 'transactionCreated',
+ hash: '0xa',
+ id: 1,
+ timestamp: 1,
+ value: '0x1',
+ }, {
+ eventKey: 'transactionSubmitted',
+ hash: '0xa',
+ id: 1,
+ timestamp: 2,
+ value: '0x1',
+ }, {
+ eventKey: 'transactionResubmitted',
+ hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
+ id: 2,
+ timestamp: 3,
+ value: '0x1',
+ }, {
+ eventKey: 'transactionCancelAttempted',
+ hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
+ id: 3,
+ timestamp: 4,
+ value: '0x1',
+ },
+ ]
+
+ const wrapper = shallow(
+ <TransactionActivityLog
+ activities={activities}
+ className="test-class"
+ inlineRetryIndex={2}
+ inlineCancelIndex={3}
+ nativeCurrency="ETH"
+ onCancel={() => {}}
+ onRetry={() => {}}
+ primaryTransactionStatus="pending"
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-activity-log'))
+ assert.ok(wrapper.hasClass('test-class'))
+ assert.equal(wrapper.find('.transaction-activity-log__action-link').length, 2)
+ })
+})
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/app/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
new file mode 100644
index 000000000..d014b8886
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js
@@ -0,0 +1,335 @@
+import assert from 'assert'
+import { combineTransactionHistories, getActivities } from '../transaction-activity-log.util'
+
+describe('combineTransactionHistories', () => {
+ it('should return no activites for an empty list of transactions', () => {
+ assert.deepEqual(combineTransactionHistories([]), [])
+ })
+
+ it('should return activities for an array of transactions', () => {
+ const transactions = [
+ {
+ estimatedGas: '0x5208',
+ hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
+ history: [
+ {
+ 'id': 6400627574331058,
+ 'time': 1543958845581,
+ 'status': 'unapproved',
+ 'metamaskNetworkId': '3',
+ 'loadingDefaults': true,
+ 'txParams': {
+ 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ 'value': '0x2386f26fc10000',
+ 'gas': '0x5208',
+ 'gasPrice': '0x3b9aca00',
+ },
+ 'type': 'standard',
+ },
+ [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958847813 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958848147 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'dropped', 'note': 'txStateManager: setting status to dropped', 'timestamp': 1543958897181 }, { 'op': 'add', 'path': '/replacedBy', 'value': '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33' }],
+ ],
+ id: 6400627574331058,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ status: 'dropped',
+ submittedTime: 1543958848135,
+ time: 1543958845581,
+ txParams: {
+ from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x32',
+ to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ value: '0x2386f26fc10000',
+ },
+ type: 'standard',
+ }, {
+ hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
+ history: [
+ {
+ 'id': 6400627574331060,
+ 'time': 1543958857697,
+ 'status': 'unapproved',
+ 'metamaskNetworkId': '3',
+ 'loadingDefaults': false,
+ 'txParams': {
+ 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ 'value': '0x2386f26fc10000',
+ 'gas': '0x5208',
+ 'gasPrice': '0x3b9aca00',
+ 'nonce': '0x32',
+ },
+ 'lastGasPrice': '0x4190ab00',
+ 'type': 'retry',
+ },
+ [{ 'op': 'replace', 'path': '/txParams/gasPrice', 'value': '0x481f2280', 'note': 'confTx: user approved transaction', 'timestamp': 1543958859470 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958859485 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'signed', 'note': 'transactions#publishTransaction', 'timestamp': 1543958859889 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958860061 }], [{ 'op': 'add', 'path': '/firstRetryBlockNumber', 'value': '0x45a0fd', 'note': 'transactions/pending-tx-tracker#event: tx:block-update', 'timestamp': 1543958896466 }],
+ [{ 'op': 'replace', 'path': '/status', 'value': 'confirmed', 'timestamp': 1543958897165 }],
+ ],
+ id: 6400627574331060,
+ lastGasPrice: '0x4190ab00',
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ status: 'confirmed',
+ submittedTime: 1543958860054,
+ time: 1543958857697,
+ txParams: {
+ from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ gas: '0x5208',
+ gasPrice: '0x481f2280',
+ nonce: '0x32',
+ to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ value: '0x2386f26fc10000',
+ },
+ txReceipt: {
+ status: '0x1',
+ },
+ type: 'retry',
+ },
+ ]
+
+ const expected = [
+ {
+ id: 6400627574331058,
+ hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
+ eventKey: 'transactionCreated',
+ timestamp: 1543958845581,
+ value: '0x2386f26fc10000',
+ }, {
+ id: 6400627574331058,
+ hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
+ eventKey: 'transactionSubmitted',
+ timestamp: 1543958848147,
+ value: '0x1319718a5000',
+ }, {
+ id: 6400627574331060,
+ hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
+ eventKey: 'transactionResubmitted',
+ timestamp: 1543958860061,
+ value: '0x171c3a061400',
+ }, {
+ id: 6400627574331060,
+ hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
+ eventKey: 'transactionConfirmed',
+ timestamp: 1543958897165,
+ value: '0x171c3a061400',
+ },
+ ]
+
+ assert.deepEqual(combineTransactionHistories(transactions), expected)
+ })
+})
+
+describe('getActivities', () => {
+ it('should return no activities for an empty history', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ assert.deepEqual(getActivities(transaction), [])
+ })
+
+ it('should return activities for a transaction\'s history', () => {
+ const transaction = {
+ history: [
+ {
+ id: 5559712943815343,
+ loadingDefaults: true,
+ metamaskNetworkId: '3',
+ status: 'unapproved',
+ time: 1535507561452,
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ },
+ [
+ {
+ op: 'replace',
+ path: '/loadingDefaults',
+ timestamp: 1535507561515,
+ value: false,
+ },
+ {
+ op: 'add',
+ path: '/gasPriceSpecified',
+ value: true,
+ },
+ {
+ op: 'add',
+ path: '/gasLimitSpecified',
+ value: true,
+ },
+ {
+ op: 'add',
+ path: '/estimatedGas',
+ value: '0x5208',
+ },
+ ],
+ [
+ {
+ note: '#newUnapprovedTransaction - adding the origin',
+ op: 'add',
+ path: '/origin',
+ timestamp: 1535507561516,
+ value: 'MetaMask',
+ },
+ [],
+ ],
+ [
+ {
+ note: 'confTx: user approved transaction',
+ op: 'replace',
+ path: '/txParams/gasPrice',
+ timestamp: 1535664571504,
+ value: '0x77359400',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to approved',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564302,
+ value: 'approved',
+ },
+ ],
+ [
+ {
+ note: 'transactions#approveTransaction',
+ op: 'add',
+ path: '/txParams/nonce',
+ timestamp: 1535507564439,
+ value: '0xa4',
+ },
+ {
+ op: 'add',
+ path: '/nonceDetails',
+ value: {
+ local: {},
+ network: {},
+ params: {},
+ },
+ },
+ ],
+ [
+ {
+ note: 'transactions#publishTransaction',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564518,
+ value: 'signed',
+ },
+ {
+ op: 'add',
+ path: '/rawTx',
+ value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf',
+ },
+ ],
+ [],
+ [
+ {
+ note: 'transactions#setTxHash',
+ op: 'add',
+ path: '/hash',
+ timestamp: 1535507564658,
+ value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager - add submitted time stamp',
+ op: 'add',
+ path: '/submittedTime',
+ timestamp: 1535507564660,
+ value: 1535507564660,
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to submitted',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507564665,
+ value: 'submitted',
+ },
+ ],
+ [
+ {
+ note: 'transactions/pending-tx-tracker#event: tx:block-update',
+ op: 'add',
+ path: '/firstRetryBlockNumber',
+ timestamp: 1535507575476,
+ value: '0x3bf624',
+ },
+ ],
+ [
+ {
+ note: 'txStateManager: setting status to confirmed',
+ op: 'replace',
+ path: '/status',
+ timestamp: 1535507615993,
+ value: 'confirmed',
+ },
+ ],
+ ],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ hash: '0xabc',
+ }
+
+ const expectedResult = [
+ {
+ 'eventKey': 'transactionCreated',
+ 'timestamp': 1535507561452,
+ 'value': '0x2386f26fc10000',
+ 'id': 1,
+ 'hash': '0xabc',
+ },
+ {
+ 'eventKey': 'transactionSubmitted',
+ 'timestamp': 1535507564665,
+ 'value': '0x2632e314a000',
+ 'id': 1,
+ 'hash': '0xabc',
+ },
+ {
+ 'eventKey': 'transactionConfirmed',
+ 'timestamp': 1535507615993,
+ 'value': '0x2632e314a000',
+ 'id': 1,
+ 'hash': '0xabc',
+ },
+ ]
+
+ assert.deepEqual(getActivities(transaction, true), expectedResult)
+ })
+})
diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js
new file mode 100644
index 000000000..86b12360a
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-activity-log-icon.component'
diff --git a/ui/app/components/app/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
new file mode 100644
index 000000000..871716002
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js
@@ -0,0 +1,55 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+import {
+ TRANSACTION_CREATED_EVENT,
+ TRANSACTION_SUBMITTED_EVENT,
+ TRANSACTION_RESUBMITTED_EVENT,
+ TRANSACTION_CONFIRMED_EVENT,
+ TRANSACTION_DROPPED_EVENT,
+ TRANSACTION_ERRORED_EVENT,
+ TRANSACTION_CANCEL_ATTEMPTED_EVENT,
+ TRANSACTION_CANCEL_SUCCESS_EVENT,
+} from '../transaction-activity-log.constants'
+
+const imageHash = {
+ [TRANSACTION_CREATED_EVENT]: '/images/icons/new.svg',
+ [TRANSACTION_SUBMITTED_EVENT]: '/images/icons/submitted.svg',
+ [TRANSACTION_RESUBMITTED_EVENT]: '/images/icons/retry.svg',
+ [TRANSACTION_CONFIRMED_EVENT]: '/images/icons/confirm.svg',
+ [TRANSACTION_DROPPED_EVENT]: '/images/icons/cancelled.svg',
+ [TRANSACTION_ERRORED_EVENT]: '/images/icons/error.svg',
+ [TRANSACTION_CANCEL_ATTEMPTED_EVENT]: '/images/icons/cancelled.svg',
+ [TRANSACTION_CANCEL_SUCCESS_EVENT]: '/images/icons/cancelled.svg',
+}
+
+export default class TransactionActivityLogIcon extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ className: PropTypes.string,
+ eventKey: PropTypes.oneOf(Object.keys(imageHash)),
+ }
+
+ render () {
+ const { className, eventKey } = this.props
+ const imagePath = imageHash[eventKey]
+
+ return (
+ <div className={classnames('transaction-activity-log-icon', className)}>
+ {
+ imagePath && (
+ <img
+ src={imagePath}
+ height={9}
+ width={9}
+ />
+ )
+ }
+ </div>
+ )
+ }
+}
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/app/transaction-activity-log/transaction-activity-log.constants.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js
new file mode 100644
index 000000000..72e63d85c
--- /dev/null
+++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js
@@ -0,0 +1,13 @@
+export const TRANSACTION_CREATED_EVENT = 'transactionCreated'
+export const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
+export const TRANSACTION_RESUBMITTED_EVENT = 'transactionResubmitted'
+export const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
+export const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
+export const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
+export const TRANSACTION_ERRORED_EVENT = 'transactionErrored'
+export const TRANSACTION_CANCEL_ATTEMPTED_EVENT = 'transactionCancelAttempted'
+export const TRANSACTION_CANCEL_SUCCESS_EVENT = 'transactionCancelSuccess'
+
+export const SUBMITTED_STATUS = 'submitted'
+export const CONFIRMED_STATUS = 'confirmed'
+export const DROPPED_STATUS = 'dropped'
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/app/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js
new file mode 100644
index 000000000..4512b84f0
--- /dev/null
+++ b/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import TransactionBreakdown from '../transaction-breakdown.component'
+
+describe('TransactionBreakdown Component', () => {
+ it('should render properly', () => {
+ const transaction = {
+ history: [],
+ id: 1,
+ status: 'confirmed',
+ txParams: {
+ from: '0x1',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0xa4',
+ to: '0x2',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(
+ <TransactionBreakdown
+ transaction={transaction}
+ className="test-class"
+ />,
+ { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
+ )
+
+ assert.ok(wrapper.hasClass('transaction-breakdown'))
+ assert.ok(wrapper.hasClass('test-class'))
+ })
+})
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/app/transaction-list-item-details/index.scss b/ui/app/components/app/transaction-list-item-details/index.scss
new file mode 100644
index 000000000..7cb253e4e
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item-details/index.scss
@@ -0,0 +1,63 @@
+.transaction-list-item-details {
+ &__header {
+ margin: 8px 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__body {
+ background: #fafbfc;
+ padding: 8px 16px;
+ }
+
+ &__header-buttons {
+ display: flex;
+ flex-direction: row;
+ }
+
+ &__header-button {
+ font-size: .625rem;
+
+ &:not(:last-child) {
+ margin-right: 8px;
+ }
+
+ &__copy-icon {
+ width: 10px;
+ height: 10px;
+ }
+ }
+
+ &__sender-to-recipient-container {
+ margin-bottom: 8px;
+ }
+
+ &__cards-container {
+ display: flex;
+ flex-direction: row;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ &__transaction-breakdown {
+ flex: 1;
+ margin-right: 8px;
+ min-width: 0;
+
+ @media screen and (max-width: $break-small) {
+ margin: 0 0 8px 0;
+ }
+ }
+
+ &__transaction-activity-log {
+ flex: 2;
+ min-width: 0;
+
+ @media screen and (min-width: $break-large) {
+ padding-left: 12px;
+ }
+ }
+}
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/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss
new file mode 100644
index 000000000..9e73a546c
--- /dev/null
+++ b/ui/app/components/app/transaction-list-item/index.scss
@@ -0,0 +1,129 @@
+.transaction-list-item {
+ box-sizing: border-box;
+ min-height: 74px;
+ border-bottom: 1px solid $geyser;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ background: $white;
+
+ &__grid {
+ cursor: pointer;
+ width: 100%;
+ padding: 16px 20px;
+ display: grid;
+ grid-template-columns: 45px 1fr 1fr 1fr;
+ grid-template-areas:
+ "identicon action status primary-amount"
+ "identicon nonce status secondary-amount";
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 20px 12px;
+ grid-template-columns: 45px 5fr 3fr;
+ grid-template-areas:
+ "nonce nonce nonce"
+ "identicon action primary-amount"
+ "identicon status secondary-amount";
+ }
+
+ &:hover {
+ background: rgba($alto, .2);
+ }
+ }
+
+ &__identicon {
+ grid-area: identicon;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 2 / span 2;
+ }
+ }
+
+ &__action {
+ text-transform: capitalize;
+ padding: 0 8px 2px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: action;
+ align-self: end;
+ }
+
+ &__status {
+ grid-area: status;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 3;
+ }
+ }
+
+ &__nonce {
+ font-size: .75rem;
+ color: #5e6064;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: nonce;
+ align-self: start;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 4px;
+ }
+ }
+
+ &__amount {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ max-width: 100%;
+
+ &--primary {
+ text-align: end;
+ grid-area: primary-amount;
+ align-self: end;
+ justify-self: end;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 2px;
+ }
+ }
+
+ &--secondary {
+ text-align: end;
+ font-size: .75rem;
+ color: #5e6064;
+ grid-area: secondary-amount;
+ align-self: start;
+ justify-self: end;
+ }
+ }
+
+ &__retry {
+ background: #d1edff;
+ border-radius: 12px;
+ font-size: .75rem;
+ padding: 4px 12px;
+ cursor: pointer;
+ margin-top: 8px;
+
+ @media screen and (max-width: $break-small) {
+ font-size: .5rem;
+ }
+ }
+
+ &__expander {
+ max-height: 0px;
+ width: 100%;
+
+ &--show {
+ max-height: 1000px;
+ transition: max-height 700ms ease-out;
+ }
+ }
+}
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/app/transaction-list/index.scss b/ui/app/components/app/transaction-list/index.scss
new file mode 100644
index 000000000..a486f4112
--- /dev/null
+++ b/ui/app/components/app/transaction-list/index.scss
@@ -0,0 +1,46 @@
+.transaction-list {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ margin-top: 8px;
+
+ &__completed-transactions {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ }
+
+ &__header {
+ flex: 0 0 auto;
+ font-size: .875rem;
+ color: $dusty-gray;
+ border-bottom: 1px solid $geyser;
+ padding: 8px 0 8px 20px;
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 0 8px 16px;
+ }
+ }
+
+ &__transactions {
+ flex: 1;
+ }
+
+ &__pending-transactions {
+ margin-bottom: 16px;
+ }
+
+ &__empty {
+ flex: 1;
+ display: grid;
+ grid-template-rows: auto;
+ padding-top: 8px;
+ }
+
+ &__empty-text {
+ grid-row-start: 2;
+ display: flex;
+ justify-content: center;
+ color: $silver;
+ }
+}
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/app/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss
new file mode 100644
index 000000000..e7daafeef
--- /dev/null
+++ b/ui/app/components/app/transaction-status/index.scss
@@ -0,0 +1,45 @@
+.transaction-status {
+ height: 26px;
+ width: 84px;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ color: #5e6064;
+ font-size: .625rem;
+ text-transform: uppercase;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ height: 16px;
+ width: 72px;
+ font-size: .5rem;
+ }
+
+ &--confirmed {
+ background-color: #eafad7;
+ color: #609a1c;
+
+ .transaction-status__transaction-count {
+ border: 1px solid #609a1c;
+ }
+ }
+
+ &--approved, &--submitted {
+ background-color: #FFF2DB;
+ color: #CA810A;
+
+ .transaction-status__transaction-count {
+ border: 1px solid #CA810A;
+ }
+ }
+
+ &--failed {
+ background: lighten($monzo, 56%);
+ color: $monzo;
+
+ .transaction-status__transaction-count {
+ border: 1px solid $monzo;
+ }
+ }
+}
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/app/transaction-view-balance/index.scss b/ui/app/components/app/transaction-view-balance/index.scss
new file mode 100644
index 000000000..bdcd536b0
--- /dev/null
+++ b/ui/app/components/app/transaction-view-balance/index.scss
@@ -0,0 +1,92 @@
+.transaction-view-balance {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex: 1;
+ height: 54px;
+ min-width: 0;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ height: initial;
+ width: 100%;
+ }
+
+ &__balance {
+ margin: 0 12px;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ position: relative;
+
+ @media screen and (max-width: $break-small) {
+ align-items: center;
+ margin: 16px 0;
+ padding: 0 16px;
+ max-width: 100%;
+ }
+ }
+
+ &__primary-container {
+ display: flex;
+ }
+
+ &__primary-balance {
+ font-size: 1.5rem;
+
+ @media screen and (max-width: $break-small) {
+ font-size: 1.75rem;
+ width: 100%;
+ justify-content: center;
+ }
+ }
+
+ &__cached-star {
+ margin-left: 4px;
+ }
+
+ &__cached-balance, &__cached-star {
+ color: $web-orange;
+ }
+
+ &__cached-secondary-balance {
+ color: rgba(220, 153, 18, 0.6901960784313725);
+ font-size: 1.15rem;
+ }
+
+ &__secondary-balance {
+ font-size: 1.15rem;
+ color: #a0a0a0;
+ }
+
+ &__balance-container {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-width: 0;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__button {
+ min-width: initial;
+ width: 100px;
+
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+}
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/app/ui-migration-annoucement/index.js b/ui/app/components/app/ui-migration-annoucement/index.js
new file mode 100644
index 000000000..c6c8cc619
--- /dev/null
+++ b/ui/app/components/app/ui-migration-annoucement/index.js
@@ -0,0 +1 @@
+export {default} from './ui-migration-announcement.container'
diff --git a/ui/app/components/app/ui-migration-annoucement/index.scss b/ui/app/components/app/ui-migration-annoucement/index.scss
new file mode 100644
index 000000000..6138a3079
--- /dev/null
+++ b/ui/app/components/app/ui-migration-annoucement/index.scss
@@ -0,0 +1,22 @@
+.ui-migration-announcement {
+ position: absolute;
+ z-index: 9999;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: $white;
+
+ p {
+ box-sizing: border-box;
+ padding: 1em;
+ font-size: 12pt;
+ }
+
+ p:last-of-type {
+ cursor: pointer;
+ text-decoration: underline;
+ font-weight: bold;
+ }
+}
diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js
new file mode 100644
index 000000000..7a4124972
--- /dev/null
+++ b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types'
+import React, {PureComponent} from 'react'
+
+export default class UiMigrationAnnouncement extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func.isRequired,
+ }
+
+ static defaultProps = {
+ shouldShowAnnouncement: true,
+ };
+
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ shouldShowAnnouncement: PropTypes.bool,
+ }
+
+ render () {
+ const { t } = this.context
+ const { onClose, shouldShowAnnouncement } = this.props
+
+ if (!shouldShowAnnouncement) {
+ return null
+ }
+
+ return (
+ <div className="ui-migration-announcement">
+ <p>{t('uiMigrationAnnouncement')}</p>
+ <p onClick={onClose}>{t('close')}</p>
+ </div>
+ )
+ }
+}
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/app/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
new file mode 100644
index 000000000..88d63baae
--- /dev/null
+++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
@@ -0,0 +1,202 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps, mergeProps
+
+proxyquire('../user-preferenced-currency-display.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+})
+
+describe('UserPreferencedCurrencyDisplay container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props', () => {
+ const mockState = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ nativeCurrency: 'ETH',
+ useNativeCurrencyAsPrimaryCurrency: true,
+ isMainnet: true,
+ showFiatInTestnets: false,
+ })
+ })
+
+ it('should return the correct props when not in mainnet and showFiatInTestnets is true', () => {
+ const mockState = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ nativeCurrency: 'ETH',
+ useNativeCurrencyAsPrimaryCurrency: true,
+ isMainnet: false,
+ showFiatInTestnets: true,
+ })
+ })
+ })
+
+ describe('mergeProps()', () => {
+ it('should return the correct props', () => {
+ const mockDispatchProps = {}
+
+ const tests = [
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ nativeCurrency: 'ETH',
+ isMainnet: true,
+ showFiatInTestnets: false,
+ },
+ ownProps: {
+ type: 'PRIMARY',
+ },
+ result: {
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 6,
+ prefix: undefined,
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ nativeCurrency: 'ETH',
+ isMainnet: true,
+ showFiatInTestnets: false,
+ },
+ ownProps: {
+ type: 'PRIMARY',
+ },
+ result: {
+ currency: undefined,
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 2,
+ prefix: undefined,
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ nativeCurrency: 'ETH',
+ isMainnet: true,
+ showFiatInTestnets: false,
+ },
+ ownProps: {
+ type: 'SECONDARY',
+ fiatNumberOfDecimals: 4,
+ fiatPrefix: '-',
+ },
+ result: {
+ nativeCurrency: 'ETH',
+ currency: undefined,
+ numberOfDecimals: 4,
+ prefix: '-',
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ nativeCurrency: 'ETH',
+ isMainnet: true,
+ showFiatInTestnets: false,
+ },
+ ownProps: {
+ type: 'SECONDARY',
+ fiatNumberOfDecimals: 4,
+ numberOfDecimals: 3,
+ fiatPrefix: 'a',
+ prefix: 'b',
+ },
+ result: {
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 3,
+ prefix: 'b',
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ nativeCurrency: 'ETH',
+ isMainnet: false,
+ showFiatInTestnets: false,
+ },
+ ownProps: {
+ type: 'PRIMARY',
+ },
+ result: {
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 6,
+ prefix: undefined,
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ nativeCurrency: 'ETH',
+ isMainnet: false,
+ showFiatInTestnets: true,
+ },
+ ownProps: {
+ type: 'PRIMARY',
+ },
+ result: {
+ currency: undefined,
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 2,
+ prefix: undefined,
+ },
+ },
+ {
+ stateProps: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ nativeCurrency: 'ETH',
+ isMainnet: true,
+ showFiatInTestnets: true,
+ },
+ ownProps: {
+ type: 'PRIMARY',
+ },
+ result: {
+ currency: undefined,
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 2,
+ prefix: undefined,
+ },
+ },
+ ]
+
+ tests.forEach(({ stateProps, ownProps, result }) => {
+ assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), {
+ ...result,
+ })
+ })
+ })
+ })
+})
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-component.js b/ui/app/components/balance-component.js
deleted file mode 100644
index 4e2769ee8..000000000
--- a/ui/app/components/balance-component.js
+++ /dev/null
@@ -1,111 +0,0 @@
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-import TokenBalance from './token-balance'
-import Identicon from './identicon'
-import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../constants/common'
-const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
-
-const { formatBalance } = require('../util')
-
-module.exports = connect(mapStateToProps)(BalanceComponent)
-
-function mapStateToProps (state) {
- const accounts = state.metamask.accounts
- 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),
- }
-}
-
-inherits(BalanceComponent, Component)
-function BalanceComponent () {
- Component.call(this)
-}
-
-BalanceComponent.prototype.render = function () {
- const props = this.props
- const { token, network, assetImages } = props
- const address = token && token.address
- const image = assetImages && address ? assetImages[token.address] : undefined
-
- return h('div.balance-container', {}, [
-
- // TODO: balance icon needs to be passed in
- // h('img.balance-icon', {
- // src: '../images/eth_logo.svg',
- // style: {},
- // }),
- h(Identicon, {
- diameter: 50,
- address,
- network,
- image,
- }),
-
- token ? this.renderTokenBalance() : this.renderBalance(),
- ])
-}
-
-BalanceComponent.prototype.renderTokenBalance = function () {
- const { token } = this.props
-
- return h('div.flex-column.balance-display', [
- h('div.token-amount', [ h(TokenBalance, { token }) ]),
- ])
-}
-
-BalanceComponent.prototype.renderBalance = function () {
- const props = this.props
- const { account, nativeCurrency } = props
- const balanceValue = account && account.balance
- const needsParse = 'needsParse' in props ? props.needsParse : true
- const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) : '...'
- const showFiat = 'showFiat' in props ? props.showFiat : true
-
- if (formattedBalance === 'None' || formattedBalance === '...') {
- return h('div.flex-column.balance-display', {}, [
- h('div.token-amount', {
- style: {},
- }, formattedBalance),
- ])
- }
-
- return h('div.flex-column.balance-display', {}, [
- h(UserPreferencedCurrencyDisplay, {
- className: 'token-amount',
- value: balanceValue,
- type: PRIMARY,
- ethNumberOfDecimals: 4,
- }),
-
- showFiat && h(UserPreferencedCurrencyDisplay, {
- value: balanceValue,
- type: SECONDARY,
- ethNumberOfDecimals: 4,
- }),
- ])
-}
-
-BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) {
- if (formattedBalance === 'None') return formattedBalance
- if (conversionRate === 0) return 'N/A'
-
- const splitBalance = formattedBalance.split(' ')
-
- const convertedNumber = (Number(splitBalance[0]) * conversionRate)
- const wholePart = Math.floor(convertedNumber)
- const decimalPart = convertedNumber - wholePart
-
- return wholePart + Number(decimalPart.toPrecision(2))
-}
diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js
deleted file mode 100644
index f99f710ce..000000000
--- a/ui/app/components/button-group/button-group.component.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-
-export default class ButtonGroup extends PureComponent {
- static propTypes = {
- defaultActiveButtonIndex: PropTypes.number,
- disabled: PropTypes.bool,
- children: PropTypes.array,
- className: PropTypes.string,
- style: PropTypes.object,
- }
-
- static defaultProps = {
- className: 'button-group',
- }
-
- state = {
- activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
- }
-
- handleButtonClick (activeButtonIndex) {
- this.setState({ activeButtonIndex })
- }
-
- renderButtons () {
- const { children, disabled } = this.props
-
- return React.Children.map(children, (child, index) => {
- return child && (
- <button
- className={classnames(
- 'button-group__button',
- { 'button-group__button--active': index === this.state.activeButtonIndex },
- )}
- onClick={() => {
- this.handleButtonClick(index)
- child.props.onClick && child.props.onClick()
- }}
- disabled={disabled || child.props.disabled}
- key={index}
- >
- { child.props.children }
- </button>
- )
- })
- }
-
- render () {
- const { className, style } = this.props
-
- return (
- <div
- className={className}
- style={style}
- >
- { this.renderButtons() }
- </div>
- )
- }
-}
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-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js
deleted file mode 100644
index f07bb97c8..000000000
--- a/ui/app/components/button-group/tests/button-group-component.test.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import ButtonGroup from '../button-group.component.js'
-
-const childButtonSpies = {
- onClick: sinon.spy(),
-}
-
-sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
-sinon.spy(ButtonGroup.prototype, 'renderButtons')
-
-const mockButtons = [
- <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
- <button onClick={childButtonSpies.onClick} key={'b'}></button>,
- <button onClick={childButtonSpies.onClick} key={'c'}></button>,
-]
-
-describe('ButtonGroup Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<ButtonGroup
- defaultActiveButtonIndex={1}
- disabled={false}
- className="someClassName"
- style={ { color: 'red' } }
- >{mockButtons}</ButtonGroup>)
- })
-
- afterEach(() => {
- childButtonSpies.onClick.resetHistory()
- ButtonGroup.prototype.handleButtonClick.resetHistory()
- ButtonGroup.prototype.renderButtons.resetHistory()
- })
-
- describe('handleButtonClick', () => {
- it('should set the activeButtonIndex', () => {
- assert.equal(wrapper.state('activeButtonIndex'), 1)
- wrapper.instance().handleButtonClick(2)
- assert.equal(wrapper.state('activeButtonIndex'), 2)
- })
- })
-
- describe('renderButtons', () => {
- it('should render a button for each child', () => {
- const childButtons = wrapper.find('.button-group__button')
- assert.equal(childButtons.length, 3)
- })
-
- it('should render the correct button with an active state', () => {
- const childButtons = wrapper.find('.button-group__button')
- const activeChildButton = wrapper.find('.button-group__button--active')
- assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
- })
-
- it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
- assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
- assert.equal(childButtonSpies.onClick.callCount, 0)
- const childButtons = wrapper.find('.button-group__button')
- childButtons.at(0).props().onClick()
- childButtons.at(1).props().onClick()
- childButtons.at(2).props().onClick()
- assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
- assert.equal(childButtonSpies.onClick.callCount, 3)
- })
-
- it('should render all child buttons as disabled if props.disabled is true', () => {
- const childButtons = wrapper.find('.button-group__button')
- childButtons.forEach(button => {
- assert.equal(button.props().disabled, undefined)
- })
- wrapper.setProps({ disabled: true })
- const disabledChildButtons = wrapper.find('[disabled=true]')
- assert.equal(disabledChildButtons.length, 3)
- })
-
- it('should render the children of the button', () => {
- const mockClass = wrapper.find('.mockClass')
- assert.equal(mockClass.length, 1)
- })
- })
-
- describe('render', () => {
- it('should render a div with the expected class and style', () => {
- assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
- assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
- })
-
- it('should call renderButtons when rendering', () => {
- assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
- wrapper.instance().render()
- assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
- })
- })
-})
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
deleted file mode 100644
index 4a06333e7..000000000
--- a/ui/app/components/button/button.component.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-
-const CLASSNAME_DEFAULT = 'btn-default'
-const CLASSNAME_PRIMARY = 'btn-primary'
-const CLASSNAME_SECONDARY = 'btn-secondary'
-const CLASSNAME_CONFIRM = 'btn-confirm'
-const CLASSNAME_RAISED = 'btn-raised'
-const CLASSNAME_LARGE = 'btn--large'
-
-const typeHash = {
- default: CLASSNAME_DEFAULT,
- primary: CLASSNAME_PRIMARY,
- secondary: CLASSNAME_SECONDARY,
- confirm: CLASSNAME_CONFIRM,
- raised: CLASSNAME_RAISED,
-}
-
-export default class Button extends Component {
- static propTypes = {
- type: PropTypes.string,
- large: PropTypes.bool,
- className: PropTypes.string,
- children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- }
-
- render () {
- const { type, large, className, ...buttonProps } = this.props
-
- return (
- <button
- className={classnames(
- 'button',
- typeHash[type],
- large && CLASSNAME_LARGE,
- className
- )}
- { ...buttonProps }
- >
- { this.props.children }
- </button>
- )
- }
-}
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-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
deleted file mode 100644
index 580a41fde..000000000
--- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
+++ /dev/null
@@ -1,46 +0,0 @@
-.confirm-detail-row {
- padding: 14px 0;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- &__label {
- font-size: .75rem;
- font-weight: 500;
- color: $scorpion;
- text-transform: uppercase;
- }
-
- &__details {
- flex: 1;
- text-align: end;
- min-width: 0;
- }
-
- &__primary {
- font-size: 1.5rem;
- justify-content: flex-end;
- }
-
- &__secondary {
- color: $oslo-gray;
- justify-content: flex-end;
- }
-
- &__header-text {
- font-size: .75rem;
- text-transform: uppercase;
- margin-bottom: 6px;
- color: $scorpion;
-
- &--edit {
- color: $curious-blue;
- cursor: pointer;
- }
-
- &--total {
- font-size: .625rem;
- }
- }
-}
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 698e624f4..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
+++ /dev/null
@@ -1,64 +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;
- }
-
- &__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-header/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
deleted file mode 100644
index 43e1e4427..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-.confirm-page-container-header {
- display: flex;
- flex-direction: column;
- flex: 0 0 auto;
-
- &__row {
- display: flex;
- justify-content: space-between;
- border-bottom: 1px solid $geyser;
- padding: 13px 13px 13px 24px;
- flex: 0 0 auto;
- }
-
- &__back-button-container {
- display: flex;
- justify-content: center;
- align-items: center;
- }
-
- &__back-button {
- color: #2f9ae0;
- font-size: 1rem;
- cursor: pointer;
- font-weight: 400;
- padding-left: 5px;
- }
-}
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 8b2e47cbb..000000000
--- a/ui/app/components/confirm-page-container/confirm-page-container.component.js
+++ /dev/null
@@ -1,137 +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 } 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,
- // 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,
- } = this.props
- const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
-
- return (
- <div className="page-container">
- <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/confirm-page-container/index.js
deleted file mode 100644
index ee88aa5d3..000000000
--- a/ui/app/components/confirm-page-container/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export { default } from './confirm-page-container.component'
-export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
-export { default as ConfirmDetailRow } from './confirm-detail-row'
-export {
- default as ConfirmPageContainerContent,
- ConfirmPageContainerSummary,
- ConfirmPageContainerError,
-} from './confirm-page-container-content'
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 af7a5b555..000000000
--- a/ui/app/components/confirm-page-container/index.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@import './confirm-page-container-content/index';
-
-@import './confirm-page-container-header/index';
-
-@import './confirm-detail-row/index';
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 2d7413b57..000000000
--- a/ui/app/components/currency-display/currency-display.component.js
+++ /dev/null
@@ -1,45 +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,
- }
-
- render () {
- const { className, displayValue, prefix, prefixComponent, style, suffix } = this.props
- const text = `${prefix || ''}${displayValue}`
- const title = `${text} ${suffix}`
-
- return (
- <div
- className={classnames('currency-display-component', className)}
- style={style}
- title={title}
- >
- { 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 6ddf07172..000000000
--- a/ui/app/components/currency-display/currency-display.container.js
+++ /dev/null
@@ -1,42 +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,
- ...restOwnProps
- } = ownProps
-
- const toCurrency = currency || currentCurrency
- const convertedValue = getValueFromWeiHex({
- value, fromCurrency: nativeCurrency, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
- })
- const displayValue = formatCurrency(convertedValue, toCurrency)
- const suffix = 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/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js
deleted file mode 100644
index 0c886af50..000000000
--- a/ui/app/components/currency-display/tests/currency-display.container.test.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-let mapStateToProps, mergeProps
-
-proxyquire('../currency-display.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mergeProps = mp
- return () => ({})
- },
- },
-})
-
-describe('CurrencyDisplay container', () => {
- describe('mapStateToProps()', () => {
- it('should return the correct props', () => {
- const mockState = {
- metamask: {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- })
- })
- })
-
- describe('mergeProps()', () => {
- it('should return the correct props', () => {
- const mockStateProps = {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- }
-
- const tests = [
- {
- props: {
- value: '0x2386f26fc10000',
- numberOfDecimals: 2,
- currency: 'usd',
- nativeCurrency: 'ETH',
- },
- result: {
- displayValue: '$2.80',
- suffix: 'USD',
- nativeCurrency: 'ETH',
- },
- },
- {
- props: {
- value: '0x2386f26fc10000',
- currency: 'usd',
- nativeCurrency: 'ETH',
- },
- result: {
- displayValue: '$2.80',
- suffix: 'USD',
- nativeCurrency: 'ETH',
- },
- },
- {
- props: {
- value: '0x1193461d01595930',
- currency: 'ETH',
- nativeCurrency: 'ETH',
- numberOfDecimals: 3,
- },
- result: {
- displayValue: '1.266',
- suffix: 'ETH',
- nativeCurrency: 'ETH',
- },
- },
- {
- props: {
- value: '0x1193461d01595930',
- currency: 'ETH',
- nativeCurrency: 'ETH',
- numberOfDecimals: 3,
- hideLabel: true,
- },
- result: {
- nativeCurrency: 'ETH',
- displayValue: '1.266',
- suffix: undefined,
- },
- },
- {
- props: {
- value: '0x3b9aca00',
- currency: 'ETH',
- nativeCurrency: 'ETH',
- denomination: 'GWEI',
- hideLabel: true,
- },
- result: {
- nativeCurrency: 'ETH',
- displayValue: '1',
- suffix: undefined,
- },
- },
- {
- props: {
- value: '0x3b9aca00',
- currency: 'ETH',
- nativeCurrency: 'ETH',
- denomination: 'WEI',
- hideLabel: true,
- },
- result: {
- nativeCurrency: 'ETH',
- displayValue: '1000000000',
- suffix: undefined,
- },
- },
- {
- props: {
- value: '0x3b9aca00',
- currency: 'ETH',
- nativeCurrency: 'ETH',
- numberOfDecimals: 100,
- hideLabel: true,
- },
- result: {
- nativeCurrency: 'ETH',
- displayValue: '1e-9',
- suffix: undefined,
- },
- },
- ]
-
- tests.forEach(({ props, result }) => {
- assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result)
- })
- })
- })
-})
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 0761a75c5..000000000
--- a/ui/app/components/currency-input/currency-input.component.js
+++ /dev/null
@@ -1,121 +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 propTypes = {
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- nativeCurrency: PropTypes.string,
- onChange: PropTypes.func,
- onBlur: PropTypes.func,
- suffix: PropTypes.string,
- useFiat: PropTypes.bool,
- value: PropTypes.string,
- }
-
- constructor (props) {
- super(props)
-
- const { value: hexValue } = props
- const decimalValue = hexValue ? this.getDecimalValue(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.getDecimalValue(this.props)
- this.setState({ hexValue: propsHexValue, decimalValue })
- }
- }
-
- getDecimalValue (props) {
- const { value: hexValue, useFiat, currentCurrency, conversionRate } = props
- const decimalValueString = useFiat
- ? getValueFromWeiHex({
- value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
- })
- : getValueFromWeiHex({
- value: hexValue, toCurrency: ETH, numberOfDecimals: 6,
- })
-
- return Number(decimalValueString) || 0
- }
-
- handleChange = decimalValue => {
- const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props
-
- const hexValue = useFiat
- ? 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 { useFiat, currentCurrency, nativeCurrency } = this.props
- const { hexValue } = this.state
- let currency, numberOfDecimals
-
- if (useFiat) {
- // 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 { 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/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js
deleted file mode 100644
index 1d1ed7b41..000000000
--- a/ui/app/components/currency-input/currency-input.container.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { connect } from 'react-redux'
-import CurrencyInput from './currency-input.component'
-import { ETH } from '../../constants/common'
-
-const mapStateToProps = state => {
- const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
-
- return {
- nativeCurrency,
- currentCurrency,
- conversionRate,
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { nativeCurrency, currentCurrency } = stateProps
- const { useFiat } = ownProps
- const suffix = useFiat ? currentCurrency.toUpperCase() : nativeCurrency || ETH
-
- return {
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- suffix,
- }
-}
-
-export default connect(mapStateToProps, null, mergeProps)(CurrencyInput)
diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss
deleted file mode 100644
index fcb2db461..000000000
--- a/ui/app/components/currency-input/index.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.currency-input {
- &__conversion-component {
- font-size: 12px;
- line-height: 12px;
- padding-left: 1px;
- }
-}
diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/currency-input/tests/currency-input.component.test.js
deleted file mode 100644
index a33889f94..000000000
--- a/ui/app/components/currency-input/tests/currency-input.component.test.js
+++ /dev/null
@@ -1,251 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow, mount } from 'enzyme'
-import sinon from 'sinon'
-import { Provider } from 'react-redux'
-import configureMockStore from 'redux-mock-store'
-import CurrencyInput from '../currency-input.component'
-import UnitInput from '../../unit-input'
-import CurrencyDisplay from '../../currency-display'
-
-describe('CurrencyInput Component', () => {
- describe('rendering', () => {
- it('should render properly without a suffix', () => {
- const wrapper = shallow(
- <CurrencyInput />
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(UnitInput).length, 1)
- })
-
- it('should render properly with a suffix', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <CurrencyInput
- suffix="ETH"
- nativeCurrency="ETH"
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
- assert.equal(wrapper.find(CurrencyDisplay).length, 1)
- })
-
- it('should render properly with an ETH value', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <CurrencyInput
- value="de0b6b3a7640000"
- suffix="ETH"
- nativeCurrency="ETH"
- currentCurrency="usd"
- conversionRate={231.06}
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
- assert.equal(currencyInputInstance.state.decimalValue, 1)
- assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
- assert.equal(wrapper.find('.unit-input__input').props().value, '1')
- assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
- })
-
- it('should render properly with a fiat value', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <CurrencyInput
- value="f602f2234d0ea"
- suffix="USD"
- useFiat
- nativeCurrency="ETH"
- currentCurrency="usd"
- conversionRate={231.06}
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
- assert.equal(currencyInputInstance.state.decimalValue, 1)
- assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD')
- assert.equal(wrapper.find('.unit-input__input').props().value, '1')
- assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
- })
- })
-
- describe('handling actions', () => {
- const handleChangeSpy = sinon.spy()
- const handleBlurSpy = sinon.spy()
-
- afterEach(() => {
- handleChangeSpy.resetHistory()
- handleBlurSpy.resetHistory()
- })
-
- it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = mount(
- <Provider store={store}>
- <CurrencyInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- suffix="ETH"
- nativeCurrency="ETH"
- currentCurrency="usd"
- conversionRate={231.06}
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- assert.equal(handleChangeSpy.callCount, 0)
- assert.equal(handleBlurSpy.callCount, 0)
-
- const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
- assert.equal(currencyInputInstance.state.decimalValue, 0)
- assert.equal(currencyInputInstance.state.hexValue, undefined)
- assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
- const input = wrapper.find('input')
- assert.equal(input.props().value, 0)
-
- input.simulate('change', { target: { value: 1 } })
- assert.equal(handleChangeSpy.callCount, 1)
- assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
- assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
- assert.equal(currencyInputInstance.state.decimalValue, 1)
- assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
-
- assert.equal(handleBlurSpy.callCount, 0)
- input.simulate('blur')
- assert.equal(handleBlurSpy.callCount, 1)
- assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000'))
- })
-
- it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = mount(
- <Provider store={store}>
- <CurrencyInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- suffix="USD"
- nativeCurrency="ETH"
- currentCurrency="usd"
- conversionRate={231.06}
- useFiat
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- assert.equal(handleChangeSpy.callCount, 0)
- assert.equal(handleBlurSpy.callCount, 0)
-
- const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
- assert.equal(currencyInputInstance.state.decimalValue, 0)
- assert.equal(currencyInputInstance.state.hexValue, undefined)
- assert.equal(wrapper.find('.currency-display-component').text(), '0ETH')
- const input = wrapper.find('input')
- assert.equal(input.props().value, 0)
-
- input.simulate('change', { target: { value: 1 } })
- assert.equal(handleChangeSpy.callCount, 1)
- assert.ok(handleChangeSpy.calledWith('f602f2234d0ea'))
- assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
- assert.equal(currencyInputInstance.state.decimalValue, 1)
- assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
-
- assert.equal(handleBlurSpy.callCount, 0)
- input.simulate('blur')
- assert.equal(handleBlurSpy.callCount, 1)
- assert.ok(handleBlurSpy.calledWith('f602f2234d0ea'))
- })
-
- it('should change the state and pass in a new decimalValue when props.value changes', () => {
- const mockStore = {
- metamask: {
- nativeCurrency: 'ETH',
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = shallow(
- <Provider store={store}>
- <CurrencyInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- suffix="USD"
- nativeCurrency="ETH"
- currentCurrency="usd"
- conversionRate={231.06}
- useFiat
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const currencyInputInstance = wrapper.find(CurrencyInput).dive()
- assert.equal(currencyInputInstance.state('decimalValue'), 0)
- assert.equal(currencyInputInstance.state('hexValue'), undefined)
- assert.equal(currencyInputInstance.find(UnitInput).props().value, 0)
-
- currencyInputInstance.setProps({ value: '1ec05e43e72400' })
- currencyInputInstance.update()
- assert.equal(currencyInputInstance.state('decimalValue'), 2)
- assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400')
- assert.equal(currencyInputInstance.find(UnitInput).props().value, 2)
- })
- })
-})
diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/currency-input/tests/currency-input.container.test.js
deleted file mode 100644
index 5d72958e6..000000000
--- a/ui/app/components/currency-input/tests/currency-input.container.test.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-let mapStateToProps, mergeProps
-
-proxyquire('../currency-input.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mergeProps = mp
- return () => ({})
- },
- },
-})
-
-describe('CurrencyInput container', () => {
- describe('mapStateToProps()', () => {
- it('should return the correct props', () => {
- const mockState = {
- metamask: {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- })
- })
- })
-
- describe('mergeProps()', () => {
- it('should return the correct props', () => {
- const mockStateProps = {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- }
- const mockDispatchProps = {}
-
- assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- useFiat: true,
- suffix: 'USD',
- })
-
- assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), {
- conversionRate: 280.45,
- currentCurrency: 'usd',
- nativeCurrency: 'ETH',
- suffix: 'ETH',
- })
- })
- })
-})
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 e67fbe45b..000000000
--- a/ui/app/components/customize-gas-modal/index.js
+++ /dev/null
@@ -1,374 +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 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,
-}
-
-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 {
- setGasPrice,
- setGasLimit,
- hideModal,
- setGasTotal,
- maxModeOn,
- selectedToken,
- balance,
- updateSendAmount,
- updateSendErrors,
- } = this.props
-
- if (maxModeOn && !selectedToken) {
- const maxAmount = subtractCurrencies(
- ethUtil.addHexPrefix(balance),
- ethUtil.addHexPrefix(gasTotal),
- { toNumericBase: 'hex' }
- )
- updateSendAmount(maxAmount)
- }
-
- 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 7476cfdd9..000000000
--- a/ui/app/components/dropdowns/account-details-dropdown.js
+++ /dev/null
@@ -1,109 +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,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsDropdown)
-
-function mapStateToProps (state) {
- return {
- selectedIdentity: getSelectedIdentity(state),
- network: state.metamask.network,
- keyrings: state.metamask.keyrings,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showAccountDetailModal: () => {
- dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
- },
- viewOnEtherscan: (address, network) => {
- global.platform.openWindow({ url: genAccountLink(address, network) })
- },
- showRemoveAccountConfirmationModal: (identity) => {
- return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
- },
- }
-}
-
-inherits(AccountDetailsDropdown, Component)
-function AccountDetailsDropdown () {
- Component.call(this)
-
- this.onClose = this.onClose.bind(this)
-}
-
-AccountDetailsDropdown.prototype.onClose = function (e) {
- e.stopPropagation()
- this.props.onClose()
-}
-
-AccountDetailsDropdown.prototype.render = function () {
- const {
- selectedIdentity,
- network,
- keyrings,
- showAccountDetailModal,
- viewOnEtherscan,
- showRemoveAccountConfirmationModal } = this.props
-
- const address = selectedIdentity.address
-
- const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(address)
- })
-
- const isRemovable = keyring.type !== 'HD Key Tree'
-
- return h(Menu, { className: 'account-details-dropdown', isShowing: true }, [
- h(CloseArea, {
- onClick: this.onClose,
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- global.platform.openExtensionInBrowser()
- this.props.onClose()
- },
- text: this.context.t('expandView'),
- icon: h(`img`, { src: 'images/expand.svg', style: { height: '15px' } }),
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- showAccountDetailModal()
- this.props.onClose()
- },
- text: this.context.t('accountDetails'),
- icon: h(`img`, { src: 'images/info.svg', style: { height: '15px' } }),
- }),
- h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- viewOnEtherscan(address, network)
- this.props.onClose()
- },
- text: this.context.t('viewOnEtherscan'),
- icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }),
- }),
- isRemovable ? h(Item, {
- onClick: (e) => {
- e.stopPropagation()
- showRemoveAccountConfirmationModal(selectedIdentity)
- this.props.onClose()
- },
- text: this.context.t('removeAccount'),
- icon: h(`img`, { src: 'images/hide.svg', style: { height: '15px' } }),
- }) : null,
- ])
-}
diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js
deleted file mode 100644
index 261eb0aa2..000000000
--- a/ui/app/components/dropdowns/account-dropdown-mini.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const AccountListItem = require('../send/account-list-item/account-list-item.component').default
-
-module.exports = AccountDropdownMini
-
-inherits(AccountDropdownMini, Component)
-function AccountDropdownMini () {
- Component.call(this)
-}
-
-AccountDropdownMini.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
- const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
-
- return currentAccount.address === selectedAccount.address
- ? listItemIcon
- : null
-}
-
-AccountDropdownMini.prototype.renderDropdown = function () {
- const {
- accounts,
- selectedAccount,
- closeDropdown,
- onSelect,
- } = this.props
-
- return h('div', {}, [
-
- h('div.account-dropdown-mini__close-area', {
- onClick: closeDropdown,
- }),
-
- h('div.account-dropdown-mini__list', {}, [
-
- ...accounts.map(account => h(AccountListItem, {
- account,
- displayBalance: false,
- displayAddress: false,
- handleClick: () => {
- onSelect(account)
- closeDropdown()
- },
- icon: this.getListItemIcon(account, selectedAccount),
- })),
-
- ]),
-
- ])
-}
-
-AccountDropdownMini.prototype.render = function () {
- const {
- selectedAccount,
- openDropdown,
- dropdownOpen,
- } = this.props
-
- return h('div.account-dropdown-mini', {}, [
-
- h(AccountListItem, {
- account: selectedAccount,
- handleClick: openDropdown,
- displayBalance: false,
- displayAddress: false,
- icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }),
- }),
-
- dropdownOpen && this.renderDropdown(),
-
- ])
-
-}
-
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/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js
deleted file mode 100644
index a45da4c10..000000000
--- a/ui/app/components/dropdowns/components/network-dropdown-icon.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-
-
-inherits(NetworkDropdownIcon, Component)
-module.exports = NetworkDropdownIcon
-
-function NetworkDropdownIcon () {
- Component.call(this)
-}
-
-NetworkDropdownIcon.prototype.render = function () {
- const {
- backgroundColor,
- isSelected,
- innerBorder = 'none',
- diameter = '12',
- } = this.props
-
- return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
- h('div', {
- style: {
- background: backgroundColor,
- border: innerBorder,
- height: `${diameter}px`,
- width: `${diameter}px`,
- },
- })
- )
-}
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
deleted file mode 100644
index d4cc695a6..000000000
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ /dev/null
@@ -1,361 +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,
-}
-
-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: () => props.setProviderType('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: () => props.setProviderType('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: () => props.setProviderType('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: () => props.setProviderType('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: () => props.setProviderType('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.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()
- const network = props.network
-
- 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 || network
- 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 f538fd555..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 })
- }
-}
-
-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/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 e27b0f182..000000000
--- a/ui/app/components/index.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-@import './app-header/index';
-
-@import './add-token-button/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 './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';
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/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js
deleted file mode 100644
index 7460e8dd5..000000000
--- a/ui/app/components/menu-bar/menu-bar.component.js
+++ /dev/null
@@ -1,63 +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,
- }
-
- static propTypes = {
- hideSidebar: PropTypes.func,
- isMascara: PropTypes.bool,
- sidebarOpen: PropTypes.bool,
- showSidebar: PropTypes.func,
- }
-
- state = { accountDetailsMenuOpen: false }
-
- render () {
- const { t } = this.context
- const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
- const { accountDetailsMenuOpen } = this.state
-
- return (
- <div className="menu-bar">
- <Tooltip
- title={t('menu')}
- position="bottom"
- >
- <div
- className="fa fa-bars menu-bar__sidebar-button"
- onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
- />
- </Tooltip>
- <SelectedAccount />
- {
- !isMascara && (
- <Tooltip
- title={t('accountOptions')}
- position="bottom"
- >
- <div
- className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser"
- onClick={() => this.setState({ accountDetailsMenuOpen: true })}
- >
- </div>
- </Tooltip>
- )
- }
- {
- accountDetailsMenuOpen && (
- <AccountDetailsDropdown
- className="menu-bar__account-details-dropdown"
- onClose={() => this.setState({ accountDetailsMenuOpen: false })}
- />
- )
- }
- </div>
- )
- }
-}
diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/menu-bar/menu-bar.container.js
deleted file mode 100644
index ae32882ae..000000000
--- a/ui/app/components/menu-bar/menu-bar.container.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { connect } from 'react-redux'
-import MenuBar from './menu-bar.component'
-import { showSidebar, hideSidebar } from '../../actions'
-
-const mapStateToProps = state => {
- const { appState: { sidebar: { isOpen }, isMascara } } = state
-
- return {
- sidebarOpen: isOpen,
- isMascara,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showSidebar: () => {
- dispatch(showSidebar({
- transitionName: 'sidebar-right',
- type: 'wallet-view',
- }))
- },
- hideSidebar: () => dispatch(hideSidebar()),
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
diff --git a/ui/app/components/modal/index.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 2a75b559b..000000000
--- a/ui/app/components/modal/modal.component.js
+++ /dev/null
@@ -1,80 +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,
- // Cancel button (left button)
- onCancel: PropTypes.func,
- cancelType: PropTypes.string,
- cancelText: PropTypes.string,
- }
-
- static defaultProps = {
- submitType: 'primary',
- cancelType: 'default',
- }
-
- render () {
- const {
- children,
- headerText,
- onClose,
- onSubmit,
- submitType,
- submitText,
- onCancel,
- cancelType,
- cancelText,
- } = this.props
-
- return (
- <div className="modal-container">
- {
- headerText && (
- <div className="modal-container__header">
- <div className="modal-container__header-text">
- { headerText }
- </div>
- <div
- className="modal-container__header-close"
- onClick={onClose}
- />
- </div>
- )
- }
- <div className="modal-container__content">
- { children }
- </div>
- <div className="modal-container__footer">
- {
- onCancel && (
- <Button
- type={cancelType}
- onClick={onCancel}
- className="modal-container__footer-button"
- >
- { cancelText }
- </Button>
- )
- }
- <Button
- type={submitType}
- onClick={onSubmit}
- className="modal-container__footer-button"
- >
- { submitText }
- </Button>
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/modal/tests/modal.component.test.js b/ui/app/components/modal/tests/modal.component.test.js
deleted file mode 100644
index 8cce1a808..000000000
--- a/ui/app/components/modal/tests/modal.component.test.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import Modal from '../modal.component'
-import Button from '../../button'
-
-describe('Modal Component', () => {
- it('should render a modal with a submit button', () => {
- const wrapper = shallow(<Modal />)
-
- assert.equal(wrapper.find('.modal-container').length, 1)
- const buttons = wrapper.find(Button)
- assert.equal(buttons.length, 1)
- assert.equal(buttons.at(0).props().type, 'primary')
- })
-
- it('should render a modal with a cancel and a submit button', () => {
- const handleCancel = sinon.spy()
- const handleSubmit = sinon.spy()
- const wrapper = shallow(
- <Modal
- onCancel={handleCancel}
- cancelText="Cancel"
- onSubmit={handleSubmit}
- submitText="Submit"
- />
- )
-
- const buttons = wrapper.find(Button)
- assert.equal(buttons.length, 2)
- const cancelButton = buttons.at(0)
- const submitButton = buttons.at(1)
-
- assert.equal(cancelButton.props().type, 'default')
- assert.equal(cancelButton.props().children, 'Cancel')
- assert.equal(handleCancel.callCount, 0)
- cancelButton.simulate('click')
- assert.equal(handleCancel.callCount, 1)
-
- assert.equal(submitButton.props().type, 'primary')
- assert.equal(submitButton.props().children, 'Submit')
- assert.equal(handleSubmit.callCount, 0)
- submitButton.simulate('click')
- assert.equal(handleSubmit.callCount, 1)
- })
-
- it('should render a modal with different button types', () => {
- const wrapper = shallow(
- <Modal
- onCancel={() => {}}
- cancelText="Cancel"
- cancelType="secondary"
- onSubmit={() => {}}
- submitText="Submit"
- submitType="confirm"
- />
- )
-
- const buttons = wrapper.find(Button)
- assert.equal(buttons.length, 2)
- assert.equal(buttons.at(0).props().type, 'secondary')
- assert.equal(buttons.at(1).props().type, 'confirm')
- })
-
- it('should render a modal with children', () => {
- const wrapper = shallow(
- <Modal
- onCancel={() => {}}
- cancelText="Cancel"
- onSubmit={() => {}}
- submitText="Submit"
- >
- <div className="test-child" />
- </Modal>
- )
-
- assert.ok(wrapper.find('.test-class'))
- })
-
- it('should render a modal with a header', () => {
- const handleCancel = sinon.spy()
- const handleSubmit = sinon.spy()
- const wrapper = shallow(
- <Modal
- onCancel={handleCancel}
- cancelText="Cancel"
- onSubmit={handleSubmit}
- submitText="Submit"
- headerText="My Header"
- onClose={handleCancel}
- />
- )
-
- assert.ok(wrapper.find('.modal-container__header'))
- assert.equal(wrapper.find('.modal-container__header-text').text(), 'My Header')
- assert.equal(handleCancel.callCount, 0)
- assert.equal(handleSubmit.callCount, 0)
- wrapper.find('.modal-container__header-close').simulate('click')
- assert.equal(handleCancel.callCount, 1)
- assert.equal(handleSubmit.callCount, 0)
- })
-})
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
deleted file mode 100644
index 248ffe008..000000000
--- a/ui/app/components/modals/account-details-modal.js
+++ /dev/null
@@ -1,100 +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,
- },
- }),
-
- 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 8b00cb9b9..000000000
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js
+++ /dev/null
@@ -1,68 +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,
- }
-
- componentDidUpdate () {
- const { transactionStatus, showTransactionConfirmedModal } = this.props
-
- if (transactionStatus !== SUBMITTED_STATUS) {
- showTransactionConfirmedModal()
- return
- }
- }
-
- handleSubmit = async () => {
- const { createCancelTransaction, hideModal } = this.props
-
- await createCancelTransaction()
- hideModal()
- }
-
- handleCancel = () => {
- this.props.hideModal()
- }
-
- render () {
- const { t } = this.context
- const { newGasFee } = this.props
-
- return (
- <Modal
- headerText={t('attemptToCancel')}
- onClose={this.handleCancel}
- onSubmit={this.handleSubmit}
- onCancel={this.handleCancel}
- submitText={t('yesLetsTry')}
- cancelText={t('nevermind')}
- submitType="secondary"
- >
- <div>
- <div className="cancel-transaction__title">
- { t('cancellationGasFee') }
- </div>
- <div className="cancel-transaction__cancel-transaction-gas-fee-container">
- <CancelTransactionGasFee value={newGasFee} />
- </div>
- <div className="cancel-transaction__description">
- { t('attemptToCancelDescription') }
- </div>
- </div>
- </Modal>
- )
- }
-}
diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js
deleted file mode 100644
index eede8b1ee..000000000
--- a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js
+++ /dev/null
@@ -1,62 +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,
- newGasFee,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- createCancelTransaction: txId => dispatch(createCancelTransaction(txId)),
- showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { transactionId, ...restStateProps } = stateProps
- const {
- createCancelTransaction: dispatchCreateCancelTransaction,
- ...restDispatchProps
- } = dispatchProps
-
- return {
- ...restStateProps,
- ...restDispatchProps,
- ...ownProps,
- createCancelTransaction: newGasPrice => {
- return dispatchCreateCancelTransaction(transactionId, newGasPrice)
- },
- }
-}
-
-export default compose(
- withModalProps,
- connect(mapStateToProps, mapDispatchToProps, mergeProps),
-)(CancelTransaction)
diff --git a/ui/app/components/modals/cancel-transaction/index.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/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js
deleted file mode 100644
index 858fb01a8..000000000
--- a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import CancelTransaction from '../cancel-transaction.component'
-import CancelTransactionGasFee from '../cancel-transaction-gas-fee'
-import Modal from '../../../modal'
-
-describe('CancelTransaction Component', () => {
- const t = key => key
-
- it('should render a CancelTransaction modal', () => {
- const wrapper = shallow(
- <CancelTransaction
- newGasFee="0x1319718a5000"
- />,
- { context: { t }}
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(Modal).length, 1)
- assert.equal(wrapper.find(CancelTransactionGasFee).length, 1)
- assert.equal(wrapper.find(CancelTransactionGasFee).props().value, '0x1319718a5000')
- assert.equal(wrapper.find('.cancel-transaction__title').text(), 'cancellationGasFee')
- assert.equal(wrapper.find('.cancel-transaction__description').text(), 'attemptToCancelDescription')
- })
-
- it('should pass the correct props to the Modal component', async () => {
- const createCancelTransactionSpy = sinon.stub().callsFake(() => Promise.resolve())
- const hideModalSpy = sinon.spy()
-
- const wrapper = shallow(
- <CancelTransaction
- defaultNewGasPrice="0x3b9aca00"
- createCancelTransaction={createCancelTransactionSpy}
- hideModal={hideModalSpy}
- />,
- { context: { t }}
- )
-
- assert.equal(wrapper.find(Modal).length, 1)
- const modalProps = wrapper.find(Modal).props()
-
- assert.equal(modalProps.headerText, 'attemptToCancel')
- assert.equal(modalProps.submitText, 'yesLetsTry')
- assert.equal(modalProps.cancelText, 'nevermind')
-
- assert.equal(createCancelTransactionSpy.callCount, 0)
- assert.equal(hideModalSpy.callCount, 0)
- await modalProps.onSubmit()
- assert.equal(createCancelTransactionSpy.callCount, 1)
- assert.equal(hideModalSpy.callCount, 1)
- modalProps.onCancel()
- assert.equal(hideModalSpy.callCount, 2)
- })
-})
diff --git a/ui/app/components/modals/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 3f526bd43..000000000
--- a/ui/app/components/modals/customize-gas/customize-gas.component.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import GasModalCard from '../../customize-gas-modal/gas-modal-card'
-import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
-import Button from '../../button'
-
-import {
- getDecimalGasLimit,
- getDecimalGasPrice,
- getPrefixedHexGasLimit,
- getPrefixedHexGasPrice,
-} from './customize-gas.util'
-
-export default class CustomizeGas extends Component {
- static contextTypes = {
- t: 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 } = this.context
- const { hideModal } = this.props
- const { gasPrice, gasLimit } = 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={() => 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 a64b3282e..000000000
--- a/ui/app/components/modals/deposit-ether-modal.js
+++ /dev/null
@@ -1,241 +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
-let COINSWITCH_ROW_TITLE
-let COINSWITCH_ROW_TEXT
-
-function mapStateToProps (state) {
- return {
- network: state.metamask.network,
- address: state.metamask.selectedAddress,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- toCoinbase: (address) => {
- dispatch(actions.buyEth({ service: 'coinbase', address, amount: 0 }))
- },
- toCoinSwitch: (address) => {
- dispatch(actions.buyEth({ service: 'coinswitch', address }))
- },
- 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')
- COINSWITCH_ROW_TITLE = context.t('buyCoinSwitch')
- COINSWITCH_ROW_TEXT = context.t('buyCoinSwitchExplainer')
-
- 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, toCoinSwitch, 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/coinswitch_logo.png\')',
- height: '40px',
- },
- }),
- title: COINSWITCH_ROW_TITLE,
- text: COINSWITCH_ROW_TEXT,
- buttonLabel: this.context.t('continueToCoinSwitch'),
- onButtonClick: () => toCoinSwitch(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 45453a582..000000000
--- a/ui/app/components/modals/index.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-@import './cancel-transaction/index';
-
-@import './confirm-remove-account/index';
-
-@import './customize-gas/index';
-
-@import './qr-scanner/index';
-
-@import './transaction-confirmed/index';
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
deleted file mode 100644
index 5aff4f5e1..000000000
--- a/ui/app/components/modals/modal.js
+++ /dev/null
@@ -1,483 +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 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 CustomizeGasModal = require('../customize-gas-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 ConfirmCustomizeGasModal from './customize-gas'
-import CancelTransaction from './cancel-transaction'
-import WelcomeBeta from './welcome-beta'
-import RejectTransactions from './reject-transactions'
-import ClearApprovedOrigins from './clear-approved-origins'
-
-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: '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)',
- },
- },
-
- BETA_UI_NOTIFICATION_MODAL: {
- contents: h(WelcomeBeta),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
- CLEAR_APPROVED_ORIGINS: {
- contents: h(ClearApprovedOrigins),
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- 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)',
- },
- },
-
- 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(CustomizeGasModal),
- ],
- mobileModalStyle: {
- width: '100vw',
- height: '100vh',
- top: '0',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: '720px',
- height: '377px',
- top: '80px',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- },
-
- CONFIRM_CUSTOMIZE_GAS: {
- contents: h(ConfirmCustomizeGasModal),
- mobileModalStyle: {
- width: '100vw',
- height: '100vh',
- top: '0',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- laptopModalStyle: {
- width: '720px',
- height: '377px',
- top: '80px',
- transform: 'none',
- left: '0',
- right: '0',
- margin: '0 auto',
- },
- },
-
- 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: () => {
- dispatch(actions.hideModal())
- },
- 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()
- },
- 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 () {
- if (this.props.onHideCallback) {
- this.props.onHideCallback()
- }
- this.props.hideModal()
-}
-
-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/modals/welcome-beta/index.js b/ui/app/components/modals/welcome-beta/index.js
deleted file mode 100644
index 49e45b9d7..000000000
--- a/ui/app/components/modals/welcome-beta/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './welcome-beta.container'
diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.component.js b/ui/app/components/modals/welcome-beta/welcome-beta.component.js
deleted file mode 100644
index ef1799164..000000000
--- a/ui/app/components/modals/welcome-beta/welcome-beta.component.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import Modal, { ModalContent } from '../../modal'
-
-const TransactionConfirmed = (props, context) => {
- const { t } = context
- const { hideModal } = props
-
- return (
- <Modal
- onSubmit={() => hideModal()}
- submitText={t('ok')}
- >
- <ModalContent
- title={t('uiWelcome')}
- description={t('uiWelcomeMessage')}
- />
- </Modal>
- )
-}
-
-TransactionConfirmed.contextTypes = {
- t: PropTypes.func,
-}
-
-TransactionConfirmed.propTypes = {
- hideModal: PropTypes.func,
-}
-
-export default TransactionConfirmed
diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.container.js b/ui/app/components/modals/welcome-beta/welcome-beta.container.js
deleted file mode 100644
index c5123ad47..000000000
--- a/ui/app/components/modals/welcome-beta/welcome-beta.container.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import WelcomeBeta from './welcome-beta.component'
-import withModalProps from '../../../higher-order-components/with-modal-props'
-
-export default withModalProps(WelcomeBeta)
diff --git a/ui/app/components/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/network.js b/ui/app/components/network.js
deleted file mode 100644
index 611aadb7b..000000000
--- a/ui/app/components/network.js
+++ /dev/null
@@ -1,142 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const classnames = require('classnames')
-const inherits = require('util').inherits
-const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
-
-Network.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(Network)
-
-
-inherits(Network, Component)
-
-function Network () {
- Component.call(this)
-}
-
-Network.prototype.render = function () {
- const props = this.props
- const context = this.context
- const networkNumber = props.network
- let providerName, providerNick
- try {
- providerName = props.provider.type
- providerNick = props.provider.nickname || ''
- } catch (e) {
- providerName = null
- }
- let iconName, hoverText
-
- if (networkNumber === 'loading') {
- return h('span.pointer.network-indicator', {
- style: {
- display: 'flex',
- alignItems: 'center',
- flexDirection: 'row',
- },
- onClick: (event) => this.props.onClick(event),
- }, [
- h('img', {
- title: context.t('attemptingConnect'),
- style: {
- width: '27px',
- },
- src: 'images/loading.svg',
- }),
- ])
- } else if (providerName === 'mainnet') {
- hoverText = context.t('mainnet')
- iconName = 'ethereum-network'
- } else if (providerName === 'ropsten') {
- hoverText = context.t('ropsten')
- iconName = 'ropsten-test-network'
- } else if (parseInt(networkNumber) === 3) {
- hoverText = context.t('ropsten')
- iconName = 'ropsten-test-network'
- } else if (providerName === 'kovan') {
- hoverText = context.t('kovan')
- iconName = 'kovan-test-network'
- } else if (providerName === 'rinkeby') {
- hoverText = context.t('rinkeby')
- iconName = 'rinkeby-test-network'
- } else {
- hoverText = context.t('unknownNetwork')
- iconName = 'unknown-private-network'
- }
-
- return (
- h('div.network-component.pointer', {
- className: classnames({
- 'network-component--disabled': this.props.disabled,
- 'ethereum-network': providerName === 'mainnet',
- 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3,
- 'kovan-test-network': providerName === 'kovan',
- 'rinkeby-test-network': providerName === 'rinkeby',
- }),
- title: hoverText,
- onClick: (event) => {
- if (!this.props.disabled) {
- this.props.onClick(event)
- }
- },
- }, [
- (function () {
- switch (iconName) {
- case 'ethereum-network':
- return h('.network-indicator', [
- h(NetworkDropdownIcon, {
- backgroundColor: '#038789', // $blue-lagoon
- nonSelectBackgroundColor: '#15afb2',
- }),
- h('.network-name', context.t('mainnet')),
- h('i.fa.fa-chevron-down.fa-lg.network-caret'),
- ])
- case 'ropsten-test-network':
- return h('.network-indicator', [
- h(NetworkDropdownIcon, {
- backgroundColor: '#e91550', // $crimson
- nonSelectBackgroundColor: '#ec2c50',
- }),
- h('.network-name', context.t('ropsten')),
- h('i.fa.fa-chevron-down.fa-lg.network-caret'),
- ])
- case 'kovan-test-network':
- return h('.network-indicator', [
- h(NetworkDropdownIcon, {
- backgroundColor: '#690496', // $purple
- nonSelectBackgroundColor: '#b039f3',
- }),
- h('.network-name', context.t('kovan')),
- h('i.fa.fa-chevron-down.fa-lg.network-caret'),
- ])
- case 'rinkeby-test-network':
- return h('.network-indicator', [
- h(NetworkDropdownIcon, {
- backgroundColor: '#ebb33f', // $tulip-tree
- nonSelectBackgroundColor: '#ecb23e',
- }),
- h('.network-name', context.t('rinkeby')),
- h('i.fa.fa-chevron-down.fa-lg.network-caret'),
- ])
- default:
- return h('.network-indicator', [
- h('i.fa.fa-question-circle.fa-lg', {
- style: {
- margin: '10px',
- color: 'rgb(125, 128, 130)',
- },
- }),
-
- h('.network-name', providerNick || context.t('privateNetwork')),
- h('i.fa.fa-chevron-down.fa-lg.network-caret'),
- ])
- }
- })(),
- ])
- )
-}
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss
deleted file mode 100644
index ba1215e84..000000000
--- a/ui/app/components/page-container/index.scss
+++ /dev/null
@@ -1,212 +0,0 @@
-.page-container {
- width: 408px;
- background-color: $white;
- box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
- z-index: 25;
- display: flex;
- flex-flow: column;
- border-radius: 8px;
-
- &__header {
- display: flex;
- flex-flow: column;
- border-bottom: 1px solid $geyser;
- padding: 16px;
- flex: 0 0 auto;
- position: relative;
-
- &--no-padding-bottom {
- padding-bottom: 0;
- }
- }
-
- &__header-close {
- color: $tundora;
- position: absolute;
- top: 16px;
- right: 16px;
- cursor: pointer;
- overflow: hidden;
-
- &::after {
- content: '\00D7';
- font-size: 40px;
- line-height: 20px;
- }
- }
-
- &__header-row {
- padding-bottom: 10px;
- display: flex;
- justify-content: space-between;
- }
-
- &__footer {
- display: flex;
- flex-flow: column;
- justify-content: center;
- border-top: 1px solid $geyser;
- flex: 0 0 auto;
-
- .btn-default,
- .btn-confirm {
- font-size: 1rem;
- }
-
- header {
- display: flex;
- flex-flow: row;
- justify-content: center;
- padding: 16px;
- flex: 0 0 auto;
- }
-
- footer {
- display: flex;
- flex-flow: row;
- justify-content: space-around;
- padding: 0 16px 16px;
- flex: 0 0 auto;
-
- a, a:hover {
- text-decoration: none;
- cursor: pointer;
- font-size: 0.75rem;
- text-transform: uppercase;
- color: #2f9ae0;
- }
- }
- }
-
- &__footer-button {
- height: 55px;
- font-size: 1rem;
- text-transform: uppercase;
- margin-right: 16px;
-
- &:last-of-type {
- margin-right: 0;
- }
- }
-
- &__back-button {
- color: #2f9ae0;
- font-size: 1rem;
- cursor: pointer;
- font-weight: 400;
- }
-
- &__title {
- color: $black;
- font-size: 2rem;
- font-weight: 500;
- line-height: 2rem;
- margin-right: 1.5rem;
- }
-
- &__subtitle {
- padding-top: .5rem;
- line-height: initial;
- font-size: .9rem;
- color: $gray;
- }
-
- &__tabs {
- display: flex;
- margin-top: 16px;
- }
-
- &__tab {
- min-width: 5rem;
- padding: 8px;
- color: $dusty-gray;
- font-family: Roboto;
- font-size: 1rem;
- text-align: center;
- cursor: pointer;
- border-bottom: none;
- margin-right: 16px;
-
- &:last-of-type {
- margin-right: 0;
- }
-
- &--selected {
- color: $curious-blue;
- border-bottom: 2px solid $curious-blue;
- }
- }
-
- &--full-width {
- width: 100% !important;
- }
-
- &--full-height {
- height: 100% !important;
- max-height: initial !important;
- min-height: initial !important;
- }
-
- &__content {
- overflow-y: auto;
- flex: 1;
- }
-
- &__warning-container {
- background: $linen;
- padding: 20px;
- display: flex;
- align-items: start;
- }
-
- &__warning-message {
- padding-left: 15px;
- }
-
- &__warning-title {
- font-weight: 500;
- }
-
- &__warning-icon {
- padding-top: 5px;
- }
-}
-
-@media screen and (max-width: 250px) {
- .page-container {
- &__footer {
- flex-flow: column-reverse;
- }
-
- &__footer-button {
- width: 100%;
- margin-bottom: 1rem;
- margin-right: 0;
-
- &:first-of-type {
- margin-bottom: 0;
- }
- }
- }
-}
-
-@media screen and (max-width: 575px) {
- .page-container {
- height: 100%;
- width: 100%;
- overflow-y: auto;
- background-color: $white;
- border-radius: 0;
- flex: 1;
- }
-}
-
-@media screen and (min-width: 576px) {
- .page-container {
- max-height: 82vh;
- min-height: 570px;
- flex: 0 0 auto;
- margin-right: auto;
- margin-left: auto;
- }
-}
diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
deleted file mode 100644
index 773fe1f56..000000000
--- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import Button from '../../button'
-
-export default class PageContainerFooter extends Component {
-
- static propTypes = {
- children: PropTypes.node,
- onCancel: PropTypes.func,
- cancelText: PropTypes.string,
- onSubmit: PropTypes.func,
- submitText: PropTypes.string,
- disabled: PropTypes.bool,
- submitButtonType: PropTypes.string,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- render () {
- const {
- children,
- onCancel,
- cancelText,
- onSubmit,
- submitText,
- disabled,
- submitButtonType,
- } = this.props
-
- return (
- <div className="page-container__footer">
-
- <header>
- <Button
- type="default"
- large
- className="page-container__footer-button"
- onClick={e => onCancel(e)}
- >
- { cancelText || this.context.t('cancel') }
- </Button>
-
- <Button
- type={submitButtonType || 'primary'}
- large
- className="page-container__footer-button"
- disabled={disabled}
- onClick={e => onSubmit(e)}
- >
- { submitText || this.context.t('next') }
- </Button>
- </header>
-
- {children && (
- <footer>
- {children}
- </footer>
- )}
-
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js
deleted file mode 100644
index a8458604e..000000000
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-
-export default class PageContainerHeader extends Component {
- static propTypes = {
- title: PropTypes.string,
- subtitle: PropTypes.string,
- onClose: PropTypes.func,
- showBackButton: PropTypes.bool,
- onBackButtonClick: PropTypes.func,
- backButtonStyles: PropTypes.object,
- backButtonString: PropTypes.string,
- tabs: PropTypes.node,
- }
-
- renderTabs () {
- const { tabs } = this.props
-
- return tabs && (
- <ul className="page-container__tabs">
- { tabs }
- </ul>
- )
- }
-
- renderHeaderRow () {
- const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
-
- return showBackButton && (
- <div className="page-container__header-row">
- <span
- className="page-container__back-button"
- onClick={onBackButtonClick}
- style={backButtonStyles}
- >
- { backButtonString || 'Back' }
- </span>
- </div>
- )
- }
-
- render () {
- const { title, subtitle, onClose, tabs } = this.props
-
- return (
- <div className={
- classnames(
- 'page-container__header',
- { 'page-container__header--no-padding-bottom': Boolean(tabs) }
- )
- }>
-
- { this.renderHeaderRow() }
-
- {
- title && <div className="page-container__title">
- { title }
- </div>
- }
-
- {
- subtitle && <div className="page-container__subtitle">
- { subtitle }
- </div>
- }
-
- {
- onClose && <div
- className="page-container__header-close"
- onClick={() => onClose()}
- />
- }
-
- { this.renderTabs() }
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js
deleted file mode 100644
index 3a2274a29..000000000
--- a/ui/app/components/page-container/page-container.component.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-
-import PageContainerHeader from './page-container-header'
-import PageContainerFooter from './page-container-footer'
-
-export default class PageContainer extends PureComponent {
- static propTypes = {
- // PageContainerHeader props
- backButtonString: PropTypes.string,
- backButtonStyles: PropTypes.object,
- onBackButtonClick: PropTypes.func,
- onClose: PropTypes.func,
- showBackButton: PropTypes.bool,
- subtitle: PropTypes.string,
- title: PropTypes.string.isRequired,
- // Tabs-related props
- defaultActiveTabIndex: PropTypes.number,
- tabsComponent: PropTypes.node,
- // Content props
- contentComponent: PropTypes.node,
- // PageContainerFooter props
- cancelText: PropTypes.string,
- disabled: PropTypes.bool,
- onCancel: PropTypes.func,
- onSubmit: PropTypes.func,
- submitText: PropTypes.string,
- }
-
- state = {
- activeTabIndex: this.props.defaultActiveTabIndex || 0,
- }
-
- handleTabClick (activeTabIndex) {
- this.setState({ activeTabIndex })
- }
-
- renderTabs () {
- const { tabsComponent } = this.props
-
- if (!tabsComponent) {
- return
- }
-
- const numberOfTabs = React.Children.count(tabsComponent.props.children)
-
- return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
- return child && React.cloneElement(child, {
- onClick: index => this.handleTabClick(index),
- tabIndex,
- isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
- key: tabIndex,
- className: 'page-container__tab',
- activeClassName: 'page-container__tab--selected',
- })
- })
- }
-
- renderActiveTabContent () {
- const { tabsComponent } = this.props
- const { children } = tabsComponent.props
- const { activeTabIndex } = this.state
-
- return children[activeTabIndex]
- ? children[activeTabIndex].props.children
- : children.props.children
- }
-
- renderContent () {
- const { contentComponent, tabsComponent } = this.props
-
- if (contentComponent) {
- return contentComponent
- } else if (tabsComponent) {
- return this.renderActiveTabContent()
- } else {
- return null
- }
- }
-
- render () {
- const {
- title,
- subtitle,
- onClose,
- showBackButton,
- onBackButtonClick,
- backButtonStyles,
- backButtonString,
- onCancel,
- cancelText,
- onSubmit,
- submitText,
- disabled,
- } = this.props
-
- return (
- <div className="page-container">
- <PageContainerHeader
- title={title}
- subtitle={subtitle}
- onClose={onClose}
- showBackButton={showBackButton}
- onBackButtonClick={onBackButtonClick}
- backButtonStyles={backButtonStyles}
- backButtonString={backButtonString}
- tabs={this.renderTabs()}
- />
- <div className="page-container__content">
- { this.renderContent() }
- </div>
- <PageContainerFooter
- onCancel={onCancel}
- cancelText={cancelText}
- onSubmit={onSubmit}
- submitText={submitText}
- disabled={disabled}
- />
- </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 3612e676c..000000000
--- a/ui/app/components/pages/add-token/add-token.component.js
+++ /dev/null
@@ -1,319 +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,
- }
- }
-
- 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 >= 10) {
- customSymbolError = this.context.t('symbolBetweenZeroTen')
- }
-
- 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,
- } = this.state
-
- return (
- <div className="add-token__custom-token-form">
- <TextField
- id="custom-address"
- label={this.context.t('tokenAddress')}
- type="text"
- value={customAddress}
- onChange={e => this.handleCustomAddressChange(e.target.value)}
- error={customAddressError}
- fullWidth
- margin="normal"
- />
- <TextField
- id="custom-symbol"
- label={this.context.t('tokenSymbol')}
- type="text"
- value={customSymbol}
- onChange={e => this.handleCustomSymbolChange(e.target.value)}
- error={customSymbolError}
- fullWidth
- margin="normal"
- disabled={autoFilled}
- />
- <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 39e86b97b..000000000
--- a/ui/app/components/pages/add-token/index.scss
+++ /dev/null
@@ -1,25 +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;
- }
-}
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/authenticated.js b/ui/app/components/pages/authenticated.js
deleted file mode 100644
index 1f6b0be49..000000000
--- a/ui/app/components/pages/authenticated.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const { Redirect } = require('react-router-dom')
-const h = require('react-hyperscript')
-const MetamaskRoute = require('./metamask-route')
-const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes')
-
-const Authenticated = props => {
- const { isUnlocked, isInitialized } = props
-
- switch (true) {
- case isUnlocked && isInitialized:
- return h(MetamaskRoute, { ...props })
- case !isInitialized:
- return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
- default:
- return h(Redirect, { to: { pathname: UNLOCK_ROUTE } })
- }
-}
-
-Authenticated.propTypes = {
- isUnlocked: PropTypes.bool,
- isInitialized: PropTypes.bool,
-}
-
-const mapStateToProps = state => {
- const { metamask: { isUnlocked, isInitialized } } = state
- return {
- isUnlocked,
- isInitialized,
- }
-}
-
-module.exports = connect(mapStateToProps)(Authenticated)
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 7d01aaffb..000000000
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ /dev/null
@@ -1,412 +0,0 @@
-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 } 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'
-
-export default class ConfirmTransactionBase extends Component {
- static contextTypes = {
- t: 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,
- // 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,
- subtitle: PropTypes.string,
- subtitleComponent: PropTypes.node,
- summaryComponent: PropTypes.node,
- title: PropTypes.string,
- titleComponent: PropTypes.node,
- valid: PropTypes.bool,
- warning: PropTypes.string,
- }
-
- 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 } = this.props
-
- if (onEditGas) {
- onEditGas()
- } else {
- showCustomizeGasModal()
- }
- }
-
- renderDetails () {
- const {
- detailsComponent,
- primaryTotalTextOverride,
- secondaryTotalTextOverride,
- hexTransactionFee,
- hexTransactionTotal,
- hideDetails,
- } = 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()}
- />
- </div>
- <div>
- <ConfirmDetailRow
- label="Total"
- value={hexTransactionTotal}
- primaryText={primaryTotalTextOverride}
- secondaryText={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')}:`}
- </div>
- <div className="confirm-page-container-content__data-box">
- { data }
- </div>
- </div>
- )
- }
-
- handleEdit () {
- const { txData, tokenData, tokenProps, onEdit } = this.props
- onEdit({ txData, tokenData, tokenProps })
- }
-
- handleCancelAll () {
- const {
- cancelAllTransactions,
- clearConfirmTransaction,
- history,
- showRejectTransactionsConfirmationModal,
- unapprovedTxCount,
- } = this.props
-
- showRejectTransactionsConfirmationModal({
- unapprovedTxCount,
- async onSubmit () {
- await cancelAllTransactions()
- clearConfirmTransaction()
- history.push(DEFAULT_ROUTE)
- },
- })
- }
-
- handleCancel () {
- const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
-
- if (onCancel) {
- onCancel(txData)
- } else {
- cancelTransaction(txData)
- .then(() => {
- clearConfirmTransaction()
- history.push(DEFAULT_ROUTE)
- })
- }
- }
-
- handleSubmit () {
- const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
- const { submitting } = this.state
-
- if (submitting) {
- return
- }
-
- this.setState({ submitting: true, submitError: null })
-
- 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
- />
- )
- }
-
- 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()
-
- return (
- <ConfirmPageContainer
- fromName={fromName}
- fromAddress={fromAddress}
- toName={toName}
- toAddress={toAddress}
- showEdit={onEdit && !isTxReprice}
- action={action || name || this.context.t('unknownFunction')}
- 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}
- disabled={!propsValid || !valid || submitting}
- onEdit={() => this.handleEdit()}
- onCancelAll={() => this.handleCancelAll()}
- onCancel={() => this.handleCancel()}
- onSubmit={() => this.handleSubmit()}
- />
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
deleted file mode 100644
index c366d5137..000000000
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ /dev/null
@@ -1,203 +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 } 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 } from '../../send/send.utils'
-import { conversionGreaterThan } from '../../../conversion-util'
-import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
-import { addressSlicer, valuesFor } from '../../../util'
-
-const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
- return {
- ...acc,
- [base.toLowerCase()]: contractMap[base],
- }
-}, {})
-
-const mapStateToProps = (state, props) => {
- const { toAddress: propsToAddress } = props
- const { confirmTransaction, metamask } = 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 } = txParams
- const {
- conversionRate,
- identities,
- currentCurrency,
- accounts,
- selectedAddress,
- selectedAddressTxList,
- assetImages,
- network,
- unapprovedTxs,
- } = metamask
- const assetImage = assetImages[txParamsToAddress]
- 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(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,
- valuesFor(unapprovedTxs),
- )
- const unapprovedTxCount = currentNetworkUnapprovedTxs.length
-
- 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,
- }
-}
-
-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: 'CONFIRM_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)),
- }
-}
-
-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: txData => dispatchUpdateGasAndCalculate(txData),
- validate: validateEditGas,
- }),
- cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
- }
-}
-
-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 2c44b6094..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 (isConfirmDeployContract(txData)) {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- if (fetchingData) {
- return <Loading />
- }
-
- if (isEtherTransaction) {
- const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
- return <Redirect to={{ pathname }} />
- }
-
- if (data) {
- const methodName = name && name.toLowerCase()
-
- 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 3ac656d73..000000000
--- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
+++ /dev/null
@@ -1,157 +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,
- }
-
- getParamsTransactionId () {
- const { match: { params: { id } = {} } } = this.props
- return id || null
- }
-
- componentDidMount () {
- const {
- totalUnapprovedCount = 0,
- send = {},
- history,
- confirmTransaction: { txData: { id: transactionId } = {} },
- } = this.props
-
- if (!totalUnapprovedCount && !send.to) {
- history.replace(DEFAULT_ROUTE)
- return
- }
-
- if (!transactionId) {
- 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 1bc2f1efb..000000000
--- a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
+++ /dev/null
@@ -1,33 +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 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()),
- }
-}
-
-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 2767b2e1f..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: 'primary',
- 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 d3abf3119..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.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`,
- { onClick: this.connect },
- 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 547df5223..000000000
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ /dev/null
@@ -1,274 +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 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) => {
- // None of the hardware wallets are supported
- // At least for now
- if (getPlatform() === PLATFORM_FIREFOX) {
- 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') {
- 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.props.history.push(DEFAULT_ROUTE)
- }).catch(e => {
- 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 = {}, accounts = [] },
- } = 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,
-}
-
-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 90279bbbd..000000000
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ /dev/null
@@ -1,159 +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 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
-
- if (!password) {
- const message = this.context.t('needImportPassword')
- return displayWarning(message)
- }
-
- importNewJsonAccount([ fileContents, password ])
- .then(({ selectedAddress }) => {
- if (selectedAddress) {
- history.push(DEFAULT_ROUTE)
- displayWarning(null)
- } else {
- displayWarning('Error importing account.')
- 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(state.metamask.accounts)[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,
-}
-
-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 8db1bfbdd..000000000
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ /dev/null
@@ -1,112 +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')
-import Button from '../../../button'
-
-PrivateKeyImportView.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(PrivateKeyImportView)
-
-
-function mapStateToProps (state) {
- return {
- error: state.appState.warning,
- firstAddress: Object.keys(state.metamask.accounts)[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) {
- history.push(DEFAULT_ROUTE)
- displayWarning(null)
- } else {
- displayWarning('Error importing account.')
- 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 94a5fa487..000000000
--- a/ui/app/components/pages/create-account/new-account.js
+++ /dev/null
@@ -1,108 +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(() => history.push(DEFAULT_ROUTE))
- },
- }, [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,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)
-
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 b9ec3c258..000000000
--- a/ui/app/components/pages/home/home.component.js
+++ /dev/null
@@ -1,87 +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_BACKUP_PHRASE_ROUTE,
- RESTORE_VAULT_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- NOTICE_ROUTE,
- CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
-} from '../../../routes'
-
-export default class Home extends PureComponent {
- static propTypes = {
- history: PropTypes.object,
- noActiveNotices: PropTypes.bool,
- lostAccounts: PropTypes.array,
- forgottenPassword: PropTypes.bool,
- seedWords: PropTypes.string,
- suggestedTokens: PropTypes.object,
- unconfirmedTransactionsCount: PropTypes.number,
- 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 {
- noActiveNotices,
- lostAccounts,
- forgottenPassword,
- seedWords,
- providerRequests,
- } = this.props
-
- // notices
- if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
- return <Redirect to={{ pathname: NOTICE_ROUTE }} />
- }
-
- // seed words
- if (seedWords) {
- return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
- }
-
- if (forgottenPassword) {
- return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
- }
-
- 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 6551278f5..000000000
--- a/ui/app/components/pages/index.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@import './unlock-page/index';
-
-@import './add-token/index';
-
-@import './confirm-add-token/index';
-
-@import './settings/index';
diff --git a/ui/app/components/pages/initialized.js b/ui/app/components/pages/initialized.js
deleted file mode 100644
index 3adf67b28..000000000
--- a/ui/app/components/pages/initialized.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const { Redirect } = require('react-router-dom')
-const h = require('react-hyperscript')
-const { INITIALIZE_ROUTE } = require('../../routes')
-const MetamaskRoute = require('./metamask-route')
-
-const Initialized = props => {
- return props.isInitialized
- ? h(MetamaskRoute, { ...props })
- : h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
-}
-
-Initialized.propTypes = {
- isInitialized: PropTypes.bool,
-}
-
-const mapStateToProps = state => {
- const { metamask: { isInitialized } } = state
- return {
- isInitialized,
- }
-}
-
-module.exports = connect(mapStateToProps)(Initialized)
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 d90a33e49..000000000
--- a/ui/app/components/pages/keychains/restore-vault.js
+++ /dev/null
@@ -1,189 +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'
-
-class RestoreVaultPage extends Component {
- static contextTypes = {
- t: 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(() => 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
- className="first-time-flow__button"
- onClick={() => !disabled && this.onClick()}
- disabled={disabled}
- >
- {this.context.t('restore')}
- </button>
- </div>
- </div>
- </div>
- )
- }
-}
-
-RestoreVaultPage.contextTypes = {
- t: PropTypes.func,
-}
-
-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/metamask-route.js b/ui/app/components/pages/metamask-route.js
deleted file mode 100644
index 23c5b5199..000000000
--- a/ui/app/components/pages/metamask-route.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const { Route } = require('react-router-dom')
-const h = require('react-hyperscript')
-
-const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => {
- return (
- h(Route, {
- ...props,
- component: isMascara && mascaraComponent ? mascaraComponent : component,
- })
- )
-}
-
-MetamaskRoute.propTypes = {
- component: PropTypes.func,
- mascaraComponent: PropTypes.func,
- isMascara: PropTypes.bool,
-}
-
-const mapStateToProps = state => {
- const { metamask: { isMascara } } = state
- return {
- isMascara,
- }
-}
-
-module.exports = connect(mapStateToProps)(MetamaskRoute)
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 da98bc3fc..000000000
--- a/ui/app/components/pages/provider-approval/provider-approval.component.js
+++ /dev/null
@@ -1,28 +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}
- 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 b223244a1..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: origin => dispatch(approveProviderRequest(origin)),
- rejectProviderRequest: origin => dispatch(rejectProviderRequest(origin)),
- }
-}
-
-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 a0a8ed47e..000000000
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
+++ /dev/null
@@ -1,544 +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 } from '../../../../routes'
-import locales from '../../../../../../app/_locales/index.json'
-import TextField from '../../../text-field'
-import Button from '../../../button'
-
-const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
- return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
-})
-
-const infuraCurrencyOptions = sortedCurrencies.map(({ quote: { code, name } }) => {
- return {
- displayValue: `${code.toUpperCase()} - ${name}`,
- key: code,
- value: code,
- }
-})
-
-const localeOptions = locales.map(locale => {
- return {
- displayValue: `${locale.name}`,
- key: locale.code,
- value: locale.code,
- }
-})
-
-export default class SettingsTab extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- metamask: PropTypes.object,
- setUseBlockie: PropTypes.func,
- setHexDataFeatureFlag: PropTypes.func,
- 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,
- isMascara: PropTypes.bool,
- 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,
- }
-
- 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('currentConversion') }</span>
- <span className="settings-page__content-description">
- { t('updatedWithDate', [Date(conversionDate)]) }
- </span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <SimpleDropdown
- placeholder={t('selectCurrency')}
- options={infuraCurrencyOptions}
- selectedOption={currentCurrency}
- onSelect={newCurrency => setCurrentCurrency(newCurrency)}
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderCurrentLocale () {
- const { t } = this.context
- const { updateCurrentLocale, currentLocale } = this.props
- const currentLocaleMeta = locales.find(locale => locale.code === currentLocale)
- const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span className="settings-page__content-label">
- { t('currentLanguage') }
- </span>
- <span className="settings-page__content-description">
- { currentLocaleName }
- </span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <SimpleDropdown
- placeholder={t('selectLocale')}
- options={localeOptions}
- selectedOption={currentLocale}
- onSelect={async newLocale => updateCurrentLocale(newLocale)}
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderNewRpcUrl () {
- const { t } = this.context
- const { newRpc, 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)) {
- setRpcTarget(newRpc, chainId, ticker, nickname)
- } else {
- const appendedRpc = `http://${newRpc}`
-
- if (validUrl.isWebUri(appendedRpc)) {
- displayWarning(this.context.t('uriErrorMsg'))
- } else {
- displayWarning(this.context.t('invalidRPC'))
- }
- }
- }
-
- renderStateLogs () {
- const { t } = this.context
- const { displayWarning } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('stateLogs') }</span>
- <span className="settings-page__content-description">
- { t('stateLogsDescription') }
- </span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <Button
- type="primary"
- large
- onClick={() => {
- window.logStateString((err, result) => {
- if (err) {
- displayWarning(t('stateLogError'))
- } else {
- exportAsFile('MetaMask State Logs.json', result)
- }
- })
- }}
- >
- { t('downloadStateLogs') }
- </Button>
- </div>
- </div>
- </div>
- )
- }
-
- 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()
- history.push(REVEAL_SEED_ROUTE)
- }}
- >
- { t('revealSeedWords') }
- </Button>
- </div>
- </div>
- </div>
- )
- }
-
- renderOldUI () {
- const { t } = this.context
- const { setFeatureFlagToBeta } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('useOldUI') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <Button
- type="secondary"
- large
- className="settings-tab__button--orange"
- onClick={event => {
- event.preventDefault()
- setFeatureFlagToBeta()
- }}
- >
- { t('useOldUI') }
- </Button>
- </div>
- </div>
- </div>
- )
- }
-
- renderResetAccount () {
- const { t } = this.context
- const { showResetAccountConfirmationModal } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('resetAccount') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <Button
- type="secondary"
- large
- className="settings-tab__button--orange"
- onClick={event => {
- event.preventDefault()
- showResetAccountConfirmationModal()
- }}
- >
- { t('resetAccount') }
- </Button>
- </div>
- </div>
- </div>
- )
- }
-
- renderBlockieOptIn () {
- const { useBlockie, setUseBlockie } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ this.context.t('blockiesIdenticon') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={useBlockie}
- onToggle={value => setUseBlockie(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- renderHexDataOptIn () {
- const { t } = this.context
- const { sendHexData, setHexDataFeatureFlag } = this.props
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('showHexData') }</span>
- <div className="settings-page__content-description">
- { t('showHexDataDescription') }
- </div>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <ToggleButton
- value={sendHexData}
- onToggle={value => setHexDataFeatureFlag(!value)}
- activeLabel=""
- inactiveLabel=""
- />
- </div>
- </div>
- </div>
- )
- }
-
- 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>
- )
- }
-
- 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>
- )
- }
-
- render () {
- const { warning, isMascara } = this.props
-
- return (
- <div className="settings-page__content">
- { warning && <div className="settings-tab__error">{ warning }</div> }
- { this.renderCurrentConversion() }
- { this.renderUsePrimaryCurrencyOptions() }
- { this.renderCurrentLocale() }
- { this.renderNewRpcUrl() }
- { this.renderStateLogs() }
- { this.renderSeedWords() }
- { !isMascara && this.renderOldUI() }
- { this.renderResetAccount() }
- { this.renderClearApproval() }
- { this.renderPrivacyOptIn() }
- { this.renderHexDataOptIn() }
- { this.renderBlockieOptIn() }
- </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 b6c33a5b2..000000000
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
+++ /dev/null
@@ -1,74 +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,
- setRpcTarget,
- displayWarning,
- revealSeedConfirmation,
- setUseBlockie,
- updateCurrentLocale,
- setFeatureFlag,
- showModal,
- setUseNativeCurrencyAsPrimaryCurrencyPreference,
-} from '../../../../actions'
-import { preferencesSelector } from '../../../../selectors'
-
-const mapStateToProps = state => {
- const { appState: { warning }, metamask } = state
- const {
- currentCurrency,
- conversionDate,
- nativeCurrency,
- useBlockie,
- featureFlags: {
- sendHexData,
- privacyMode,
- } = {},
- provider = {},
- isMascara,
- currentLocale,
- } = metamask
- const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
-
- return {
- warning,
- isMascara,
- currentLocale,
- currentCurrency,
- conversionDate,
- nativeCurrency,
- useBlockie,
- sendHexData,
- privacyMode,
- provider,
- useNativeCurrencyAsPrimaryCurrency,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)),
- setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(setRpcTarget(newRpc, chainId, ticker, nickname)),
- displayWarning: warning => dispatch(displayWarning(warning)),
- revealSeedConfirmation: () => dispatch(revealSeedConfirmation()),
- setUseBlockie: value => dispatch(setUseBlockie(value)),
- updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
- setFeatureFlagToBeta: () => {
- return dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- },
- setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
- setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)),
- showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
- setUseNativeCurrencyAsPrimaryCurrencyPreference: value => {
- return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value))
- },
- showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })),
- }
-}
-
-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/index.scss b/ui/app/components/pages/unlock-page/index.scss
deleted file mode 100644
index 6bd52282d..000000000
--- a/ui/app/components/pages/unlock-page/index.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-.unlock-page {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
- width: 357px;
- padding: 30px;
- font-weight: 400;
- color: $silver-chalice;
-
- &__container {
- background: $white;
- display: flex;
- align-self: stretch;
- justify-content: center;
- flex: 1 0 auto;
- height: 100vh;
- }
-
- &__mascot-container {
- margin-top: 24px;
- }
-
- &__title {
- margin-top: 5px;
- font-size: 2rem;
- font-weight: 800;
- color: $tundora;
- }
-
- &__form {
- width: 100%;
- margin: 56px 0 8px;
- }
-
- &__links {
- margin-top: 25px;
- width: 100%;
- }
-
- &__link {
- cursor: pointer;
-
- &--import {
- color: $ecstasy;
- }
-
- &--use-classic {
- margin-top: 10px;
- }
- }
-}
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 94915df76..000000000
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ /dev/null
@@ -1,179 +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 { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
-import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
-import getCaretCoordinates from 'textarea-caret'
-import { EventEmitter } from 'events'
-import Mascot from '../../mascot'
-import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
-
-export default class UnlockPage extends Component {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- forgotPassword: PropTypes.func,
- tryUnlockMetamask: PropTypes.func,
- markPasswordForgotten: PropTypes.func,
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- useOldInterface: 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)
- }
- }
-
- async handleSubmit (event) {
- event.preventDefault()
- event.stopPropagation()
-
- const { password } = this.state
- const { tryUnlockMetamask, history } = this.props
-
- if (password === '' || this.submitting) {
- return
- }
-
- this.setState({ error: null })
- this.submitting = true
-
- try {
- await tryUnlockMetamask(password)
- this.submitting = false
- history.push(DEFAULT_ROUTE)
- } catch ({ message }) {
- 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={event => this.handleSubmit(event)}
- disableRipple
- >
- { this.context.t('login') }
- </Button>
- )
- }
-
- render () {
- const { password, error } = this.state
- const { t } = this.context
- const { markPasswordForgotten, history } = 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={event => this.handleSubmit(event)}
- >
- <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={() => {
- markPasswordForgotten()
- history.push(RESTORE_VAULT_ROUTE)
-
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
- }}
- >
- { t('restoreFromSeed') }
- </div>
- <div
- className="unlock-page__link unlock-page__link--import"
- onClick={() => {
- markPasswordForgotten()
- history.push(RESTORE_VAULT_ROUTE)
-
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
- }}
- >
- { 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 18fed9b2e..000000000
--- a/ui/app/components/pages/unlock-page/unlock-page.container.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-
-const {
- tryUnlockMetamask,
- forgotPassword,
- markPasswordForgotten,
-} = require('../../../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()),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(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 902733616..000000000
--- a/ui/app/components/provider-page-container/provider-page-container.component.js
+++ /dev/null
@@ -1,50 +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,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- onCancel = () => {
- const { origin, rejectProviderRequest } = this.props
- rejectProviderRequest(origin)
- }
-
- onSubmit = () => {
- const { approveProviderRequest, origin } = this.props
- approveProviderRequest(origin)
- }
-
- 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 d3242ddf5..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-npm').qrcode
-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 } = props.Qr
- const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data)}`
- 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),
- }),
- ])
-}
-
-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 4f98df9b6..000000000
--- a/ui/app/components/selected-account/selected-account.component.js
+++ /dev/null
@@ -1,54 +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,
- }
-
- render () {
- const { t } = this.context
- const { selectedAddress, selectedIdentity } = this.props
- const checksummedAddress = checksumAddress(selectedAddress)
-
- 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 f9e061d15..000000000
--- a/ui/app/components/selected-account/selected-account.container.js
+++ /dev/null
@@ -1,13 +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),
- }
-}
-
-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 14bb7471f..000000000
--- a/ui/app/components/send/account-list-item/account-list-item.component.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import { checksumAddress } from '../../../util'
-import Identicon from '../../identicon'
-import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
-import { PRIMARY, SECONDARY } from '../../../constants/common'
-
-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,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- account,
- className,
- displayAddress = false,
- displayBalance = true,
- handleClick,
- icon = null,
- } = this.props
-
- const { name, address, balance } = account || {}
-
- return (<div
- className={`account-list-item ${className}`}
- onClick={() => 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 && (
- <div className="account-list-item__account-balances">
- <UserPreferencedCurrencyDisplay
- type={PRIMARY}
- value={balance}
- />
- <UserPreferencedCurrencyDisplay
- type={SECONDARY}
- value={balance}
- />
- </div>
- )
- }
-
- </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 f8e73d923..000000000
--- a/ui/app/components/send/account-list-item/account-list-item.container.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getConversionRate,
- getCurrentCurrency,
- getNativeCurrency,
-} from '../send.selectors.js'
-import AccountListItem from './account-list-item.component'
-
-export default connect(mapStateToProps)(AccountListItem)
-
-function mapStateToProps (state) {
- return {
- conversionRate: getConversionRate(state),
- currentCurrency: getCurrentCurrency(state),
- nativeCurrency: getNativeCurrency(state),
- }
-}
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 6ffc0b1c6..000000000
--- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
+++ /dev/null
@@ -1,133 +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',
- }
- )
- })
-
- 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 7c2f5fcb2..000000000
--- a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
+++ /dev/null
@@ -1,34 +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: (s) => `mockConversionRate:${s}`,
- getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`,
- getNativeCurrency: (s) => `mockNativeCurrency:${s}`,
- },
-})
-
-describe('account-list-item container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- conversionRate: 'mockConversionRate:mockState',
- currentCurrency: 'mockCurrentCurrency:mockState',
- nativeCurrency: 'mockNativeCurrency:mockState',
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
deleted file mode 100644
index ceb620941..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-
-export default class AmountMaxButton extends Component {
-
- static propTypes = {
- balance: PropTypes.string,
- gasTotal: PropTypes.string,
- maxModeOn: PropTypes.bool,
- selectedToken: PropTypes.object,
- setAmountToMax: PropTypes.func,
- setMaxModeTo: PropTypes.func,
- tokenBalance: PropTypes.string,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- setMaxAmount () {
- const {
- balance,
- gasTotal,
- selectedToken,
- setAmountToMax,
- tokenBalance,
- } = this.props
-
- setAmountToMax({
- balance,
- gasTotal,
- selectedToken,
- tokenBalance,
- })
- }
-
- onMaxClick = (event) => {
- const { setMaxModeTo } = this.props
-
- event.preventDefault()
- setMaxModeTo(true)
- this.setMaxAmount()
- }
-
- render () {
- return this.props.maxModeOn
- ? null
- : (
- <div>
- <span
- className="send-v2__amount-max"
- onClick={this.onMaxClick}
- >
- {this.context.t('max')}
- </span>
- </div>
- )
- }
-
-}
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 b490a7fd7..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
+++ /dev/null
@@ -1,22 +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'})
- : 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/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
deleted file mode 100644
index 816df6a12..000000000
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import assert from 'assert'
-import {
- calcMaxAmount,
-} from '../amount-max-button.utils.js'
-
-describe('amount-max-button utils', () => {
-
- describe('calcMaxAmount()', () => {
- it('should calculate the correct amount when no selectedToken defined', () => {
- assert.deepEqual(calcMaxAmount({
- balance: 'ffffff',
- gasTotal: 'ff',
- selectedToken: false,
- }), 'ffff00')
- })
-
- it('should calculate the correct amount when a selectedToken is defined', () => {
- assert.deepEqual(calcMaxAmount({
- selectedToken: {
- decimals: 10,
- },
- tokenBalance: 100,
- }), 'e8d4a51000')
- })
- })
-
-})
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 0268376bf..000000000
--- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
+++ /dev/null
@@ -1,120 +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({
- amount,
- 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 3504d1b73..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-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
deleted file mode 100644
index 56e80cb83..000000000
--- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import SendAmountRow from '../send-amount-row.component.js'
-
-import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
-import AmountMaxButton from '../amount-max-button/amount-max-button.container'
-import UserPreferencedTokenInput from '../../../../user-preferenced-token-input'
-
-const propsMethodSpies = {
- setMaxModeTo: sinon.spy(),
- updateSendAmount: sinon.spy(),
- updateSendAmountError: sinon.spy(),
- updateGas: sinon.spy(),
- updateGasFeeError: sinon.spy(),
-}
-
-sinon.spy(SendAmountRow.prototype, 'updateAmount')
-sinon.spy(SendAmountRow.prototype, 'validateAmount')
-sinon.spy(SendAmountRow.prototype, 'updateGas')
-
-describe('SendAmountRow Component', function () {
- let wrapper
- let instance
-
- beforeEach(() => {
- wrapper = shallow(<SendAmountRow
- amount={'mockAmount'}
- amountConversionRate={'mockAmountConversionRate'}
- balance={'mockBalance'}
- conversionRate={7}
- convertedCurrency={'mockConvertedCurrency'}
- gasTotal={'mockGasTotal'}
- inError={false}
- primaryCurrency={'mockPrimaryCurrency'}
- selectedToken={ { address: 'mockTokenAddress' } }
- setMaxModeTo={propsMethodSpies.setMaxModeTo}
- tokenBalance={'mockTokenBalance'}
- updateGasFeeError={propsMethodSpies.updateGasFeeError}
- updateSendAmount={propsMethodSpies.updateSendAmount}
- updateSendAmountError={propsMethodSpies.updateSendAmountError}
- updateGas={propsMethodSpies.updateGas}
- />, { context: { t: str => str + '_t' } })
- instance = wrapper.instance()
- })
-
- afterEach(() => {
- propsMethodSpies.setMaxModeTo.resetHistory()
- propsMethodSpies.updateSendAmount.resetHistory()
- propsMethodSpies.updateSendAmountError.resetHistory()
- propsMethodSpies.updateGasFeeError.resetHistory()
- SendAmountRow.prototype.validateAmount.resetHistory()
- SendAmountRow.prototype.updateAmount.resetHistory()
- })
-
- describe('validateAmount', () => {
-
- it('should call updateSendAmountError with the correct params', () => {
- assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0)
- instance.validateAmount('someAmount')
- assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendAmountError.getCall(0).args,
- [{
- amount: 'someAmount',
- amountConversionRate: 'mockAmountConversionRate',
- balance: 'mockBalance',
- conversionRate: 7,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- selectedToken: { address: 'mockTokenAddress' },
- tokenBalance: 'mockTokenBalance',
- }]
- )
- })
-
- it('should call updateGasFeeError if selectedToken is truthy', () => {
- assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
- instance.validateAmount('someAmount')
- assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateGasFeeError.getCall(0).args,
- [{
- amount: 'someAmount',
- amountConversionRate: 'mockAmountConversionRate',
- balance: 'mockBalance',
- conversionRate: 7,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- selectedToken: { address: 'mockTokenAddress' },
- tokenBalance: 'mockTokenBalance',
- }]
- )
- })
-
- it('should call not updateGasFeeError if selectedToken is falsey', () => {
- wrapper.setProps({ selectedToken: null })
- assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
- instance.validateAmount('someAmount')
- assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
- })
-
- })
-
- describe('updateAmount', () => {
-
- it('should call setMaxModeTo', () => {
- assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
- instance.updateAmount('someAmount')
- assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.setMaxModeTo.getCall(0).args,
- [false]
- )
- })
-
- it('should call updateSendAmount', () => {
- assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
- instance.updateAmount('someAmount')
- assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendAmount.getCall(0).args,
- ['someAmount']
- )
- })
-
- })
-
- describe('render', () => {
- it('should render a SendRowWrapper component', () => {
- assert.equal(wrapper.find(SendRowWrapper).length, 1)
- })
-
- it('should pass the correct props to SendRowWrapper', () => {
- const {
- errorType,
- label,
- showError,
- } = wrapper.find(SendRowWrapper).props()
-
- assert.equal(errorType, 'amount')
-
- assert.equal(label, 'amount_t:')
-
- assert.equal(showError, false)
- })
-
- it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => {
- assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton))
- })
-
- it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => {
- assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput))
- })
-
- it('should render the UserPreferencedTokenInput with the correct props', () => {
- const {
- onBlur,
- onChange,
- error,
- value,
- } = wrapper.find(SendRowWrapper).childAt(1).props()
- assert.equal(error, false)
- assert.equal(value, 'mockAmount')
- assert.equal(SendAmountRow.prototype.updateGas.callCount, 0)
- assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
- onBlur('mockNewAmount')
- assert.equal(SendAmountRow.prototype.updateGas.callCount, 1)
- assert.deepEqual(
- SendAmountRow.prototype.updateGas.getCall(0).args,
- ['mockNewAmount']
- )
- assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
- assert.deepEqual(
- SendAmountRow.prototype.updateAmount.getCall(0).args,
- ['mockNewAmount']
- )
- assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
- onChange('mockNewAmount')
- assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
- assert.deepEqual(
- SendAmountRow.prototype.validateAmount.getCall(0).args,
- ['mockNewAmount']
- )
- })
- })
-})
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-README.md b/ui/app/components/send/send-content/send-content-README.md
deleted file mode 100644
index e69de29bb..000000000
--- a/ui/app/components/send/send-content/send-content-README.md
+++ /dev/null
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 1b03ffd2b..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-content.scss b/ui/app/components/send/send-content/send-content.scss
deleted file mode 100644
index e69de29bb..000000000
--- a/ui/app/components/send/send-content/send-content.scss
+++ /dev/null
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/from-dropdown/from-dropdown-README.md b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
deleted file mode 100644
index e69de29bb..000000000
--- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
+++ /dev/null
diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
deleted file mode 100644
index 4f43a9d61..000000000
--- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import AccountListItem from '../../../account-list-item/'
-import SendDropdownList from '../../send-dropdown-list/'
-
-export default class FromDropdown extends Component {
-
- static propTypes = {
- accounts: PropTypes.array,
- closeDropdown: PropTypes.func,
- dropdownOpen: PropTypes.bool,
- onSelect: PropTypes.func,
- openDropdown: PropTypes.func,
- selectedAccount: PropTypes.object,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- accounts,
- closeDropdown,
- dropdownOpen,
- openDropdown,
- selectedAccount,
- onSelect,
- } = this.props
-
- return <div className="send-v2__from-dropdown">
- <AccountListItem
- account={selectedAccount}
- handleClick={openDropdown}
- icon={<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>}
- />
- {dropdownOpen && <SendDropdownList
- accounts={accounts}
- closeDropdown={closeDropdown}
- onSelect={onSelect}
- activeAddress={selectedAccount.address}
- />}
- </div>
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
deleted file mode 100644
index e69de29bb..000000000
--- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
+++ /dev/null
diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
deleted file mode 100644
index 2314ef4e3..000000000
--- a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './from-dropdown.component'
diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
deleted file mode 100644
index 84fcb281e..000000000
--- a/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import FromDropdown from '../from-dropdown.component.js'
-
-import AccountListItem from '../../../../account-list-item/account-list-item.container'
-import SendDropdownList from '../../../send-dropdown-list/send-dropdown-list.component'
-
-const propsMethodSpies = {
- closeDropdown: sinon.spy(),
- openDropdown: sinon.spy(),
- onSelect: sinon.spy(),
-}
-
-describe('FromDropdown Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<FromDropdown
- accounts={['mockAccount']}
- closeDropdown={propsMethodSpies.closeDropdown}
- dropdownOpen={false}
- onSelect={propsMethodSpies.onSelect}
- openDropdown={propsMethodSpies.openDropdown}
- selectedAccount={ { address: 'mockAddress' } }
- />, { context: { t: str => str + '_t' } })
- })
-
- afterEach(() => {
- propsMethodSpies.closeDropdown.resetHistory()
- propsMethodSpies.openDropdown.resetHistory()
- propsMethodSpies.onSelect.resetHistory()
- })
-
- describe('render', () => {
- it('should render a div with a .send-v2__from-dropdown class', () => {
- assert.equal(wrapper.find('.send-v2__from-dropdown').length, 1)
- })
-
- it('should render an AccountListItem as the first child of the .send-v2__from-dropdown div', () => {
- assert(wrapper.find('.send-v2__from-dropdown').childAt(0).is(AccountListItem))
- })
-
- it('should pass the correct props to AccountListItem', () => {
- const {
- account,
- handleClick,
- icon,
- } = wrapper.find('.send-v2__from-dropdown').childAt(0).props()
- assert.deepEqual(account, { address: 'mockAddress' })
- assert.deepEqual(
- icon,
- <i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>
- )
- assert.equal(propsMethodSpies.openDropdown.callCount, 0)
- handleClick()
- assert.equal(propsMethodSpies.openDropdown.callCount, 1)
- })
-
- it('should not render a SendDropdownList when dropdownOpen is false', () => {
- assert.equal(wrapper.find(SendDropdownList).length, 0)
- })
-
- it('should render a SendDropdownList when dropdownOpen is true', () => {
- wrapper.setProps({ dropdownOpen: true })
- assert(wrapper.find(SendDropdownList).length, 1)
- })
-
- it('should pass the correct props to the SendDropdownList]', () => {
- wrapper.setProps({ dropdownOpen: true })
- const {
- accounts,
- closeDropdown,
- onSelect,
- activeAddress,
- } = wrapper.find(SendDropdownList).props()
- assert.deepEqual(accounts, ['mockAccount'])
- assert.equal(activeAddress, 'mockAddress')
- assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
- closeDropdown()
- assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
- assert.equal(propsMethodSpies.onSelect.callCount, 0)
- onSelect()
- assert.equal(propsMethodSpies.onSelect.callCount, 1)
- })
- })
-})
diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
deleted file mode 100644
index e69de29bb..000000000
--- a/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
+++ /dev/null
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 3e0e0de22..000000000
--- a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowWrapper from '../send-row-wrapper/'
-import FromDropdown from './from-dropdown/'
-
-export default class SendFromRow extends Component {
-
- static propTypes = {
- closeFromDropdown: PropTypes.func,
- conversionRate: PropTypes.number,
- from: PropTypes.object,
- fromAccounts: PropTypes.array,
- fromDropdownOpen: PropTypes.bool,
- openFromDropdown: PropTypes.func,
- tokenContract: PropTypes.object,
- updateSendFrom: PropTypes.func,
- setSendTokenBalance: PropTypes.func,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- async handleFromChange (newFrom) {
- const {
- updateSendFrom,
- tokenContract,
- setSendTokenBalance,
- } = this.props
-
- if (tokenContract) {
- const usersToken = await tokenContract.balanceOf(newFrom.address)
- setSendTokenBalance(usersToken)
- }
- updateSendFrom(newFrom)
- }
-
- render () {
- const {
- closeFromDropdown,
- conversionRate,
- from,
- fromAccounts,
- fromDropdownOpen,
- openFromDropdown,
- } = this.props
-
- return (
- <SendRowWrapper label={`${this.context.t('from')}:`}>
- <FromDropdown
- accounts={fromAccounts}
- closeDropdown={() => closeFromDropdown()}
- conversionRate={conversionRate}
- dropdownOpen={fromDropdownOpen}
- onSelect={newFrom => this.handleFromChange(newFrom)}
- openDropdown={() => openFromDropdown()}
- selectedAccount={from}
- />
- </SendRowWrapper>
- )
- }
-
-}
diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
deleted file mode 100644
index 33cb63b43..000000000
--- a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { connect } from 'react-redux'
-import {
- accountsWithSendEtherInfoSelector,
- getConversionRate,
- getSelectedTokenContract,
- getSendFromObject,
-} from '../../send.selectors.js'
-import {
- getFromDropdownOpen,
-} from './send-from-row.selectors.js'
-import { calcTokenBalance } from '../../send.utils.js'
-import {
- updateSendFrom,
- setSendTokenBalance,
-} from '../../../../actions'
-import {
- closeFromDropdown,
- openFromDropdown,
-} from '../../../../ducks/send.duck'
-import SendFromRow from './send-from-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow)
-
-function mapStateToProps (state) {
- return {
- conversionRate: getConversionRate(state),
- from: getSendFromObject(state),
- fromAccounts: accountsWithSendEtherInfoSelector(state),
- fromDropdownOpen: getFromDropdownOpen(state),
- tokenContract: getSelectedTokenContract(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- closeFromDropdown: () => dispatch(closeFromDropdown()),
- openFromDropdown: () => dispatch(openFromDropdown()),
- updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
- setSendTokenBalance: (usersToken, selectedToken) => {
- if (!usersToken) return
-
- const tokenBalance = calcTokenBalance({ usersToken, selectedToken })
- dispatch(setSendTokenBalance(tokenBalance))
- },
- }
-}
diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
deleted file mode 100644
index 9ba8d1739..000000000
--- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import SendFromRow from '../send-from-row.component.js'
-
-import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
-import FromDropdown from '../from-dropdown/from-dropdown.component'
-
-const propsMethodSpies = {
- closeFromDropdown: sinon.spy(),
- openFromDropdown: sinon.spy(),
- updateSendFrom: sinon.spy(),
- setSendTokenBalance: sinon.spy(),
-}
-
-sinon.spy(SendFromRow.prototype, 'handleFromChange')
-
-describe('SendFromRow Component', function () {
- let wrapper
- let instance
-
- beforeEach(() => {
- wrapper = shallow(<SendFromRow
- closeFromDropdown={propsMethodSpies.closeFromDropdown}
- conversionRate={15}
- from={ { address: 'mockAddress' } }
- fromAccounts={['mockAccount']}
- fromDropdownOpen={false}
- openFromDropdown={propsMethodSpies.openFromDropdown}
- setSendTokenBalance={propsMethodSpies.setSendTokenBalance}
- tokenContract={null}
- updateSendFrom={propsMethodSpies.updateSendFrom}
- />, { context: { t: str => str + '_t' } })
- instance = wrapper.instance()
- })
-
- afterEach(() => {
- propsMethodSpies.closeFromDropdown.resetHistory()
- propsMethodSpies.openFromDropdown.resetHistory()
- propsMethodSpies.updateSendFrom.resetHistory()
- propsMethodSpies.setSendTokenBalance.resetHistory()
- SendFromRow.prototype.handleFromChange.resetHistory()
- })
-
- describe('handleFromChange', () => {
-
- it('should call updateSendFrom', () => {
- assert.equal(propsMethodSpies.updateSendFrom.callCount, 0)
- instance.handleFromChange('mockFrom')
- assert.equal(propsMethodSpies.updateSendFrom.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendFrom.getCall(0).args,
- ['mockFrom']
- )
- })
-
- it('should call tokenContract.balanceOf and setSendTokenBalance if tokenContract is defined', async () => {
- wrapper.setProps({
- tokenContract: {
- balanceOf: () => new Promise((resolve) => resolve('mockUsersToken')),
- },
- })
- assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 0)
- await instance.handleFromChange('mockFrom')
- assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.setSendTokenBalance.getCall(0).args,
- ['mockUsersToken']
- )
- })
-
- })
-
- describe('render', () => {
- it('should render a SendRowWrapper component', () => {
- assert.equal(wrapper.find(SendRowWrapper).length, 1)
- })
-
- it('should pass the correct props to SendRowWrapper', () => {
- const {
- label,
- } = wrapper.find(SendRowWrapper).props()
-
- assert.equal(label, 'from_t:')
- })
-
- it('should render an FromDropdown as a child of the SendRowWrapper', () => {
- assert(wrapper.find(SendRowWrapper).childAt(0).is(FromDropdown))
- })
-
- it('should render the FromDropdown with the correct props', () => {
- const {
- accounts,
- closeDropdown,
- conversionRate,
- dropdownOpen,
- onSelect,
- openDropdown,
- selectedAccount,
- } = wrapper.find(SendRowWrapper).childAt(0).props()
- assert.deepEqual(accounts, ['mockAccount'])
- assert.equal(dropdownOpen, false)
- assert.equal(conversionRate, 15)
- assert.deepEqual(selectedAccount, { address: 'mockAddress' })
- assert.equal(propsMethodSpies.closeFromDropdown.callCount, 0)
- closeDropdown()
- assert.equal(propsMethodSpies.closeFromDropdown.callCount, 1)
- assert.equal(propsMethodSpies.openFromDropdown.callCount, 0)
- openDropdown()
- assert.equal(propsMethodSpies.openFromDropdown.callCount, 1)
- assert.equal(SendFromRow.prototype.handleFromChange.callCount, 0)
- onSelect('mockNewFrom')
- assert.equal(SendFromRow.prototype.handleFromChange.callCount, 1)
- assert.deepEqual(
- SendFromRow.prototype.handleFromChange.getCall(0).args,
- ['mockNewFrom']
- )
- })
- })
-})
diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
deleted file mode 100644
index e080b2fe3..000000000
--- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- updateSendFrom: sinon.spy(),
- setSendTokenBalance: sinon.spy(),
-}
-const duckActionSpies = {
- closeFromDropdown: sinon.spy(),
- openFromDropdown: sinon.spy(),
-}
-
-proxyquire('../send-from-row.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../send.selectors.js': {
- accountsWithSendEtherInfoSelector: (s) => `mockFromAccounts:${s}`,
- getConversionRate: (s) => `mockConversionRate:${s}`,
- getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
- getSendFromObject: (s) => `mockFrom:${s}`,
- },
- './send-from-row.selectors.js': { getFromDropdownOpen: (s) => `mockFromDropdownOpen:${s}` },
- '../../send.utils.js': { calcTokenBalance: ({ usersToken, selectedToken }) => usersToken + selectedToken },
- '../../../../actions': actionSpies,
- '../../../../ducks/send.duck': duckActionSpies,
-})
-
-describe('send-from-row container', () => {
-
- describe('mapStateToProps()', () => {
-
- it('should map the correct properties to props', () => {
- assert.deepEqual(mapStateToProps('mockState'), {
- conversionRate: 'mockConversionRate:mockState',
- from: 'mockFrom:mockState',
- fromAccounts: 'mockFromAccounts:mockState',
- fromDropdownOpen: 'mockFromDropdownOpen:mockState',
- tokenContract: 'mockTokenContract:mockState',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('closeFromDropdown()', () => {
- it('should dispatch a closeFromDropdown action', () => {
- mapDispatchToPropsObject.closeFromDropdown()
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.closeFromDropdown.calledOnce)
- assert.equal(
- duckActionSpies.closeFromDropdown.getCall(0).args[0],
- undefined
- )
- })
- })
-
- describe('openFromDropdown()', () => {
- it('should dispatch a openFromDropdown action', () => {
- mapDispatchToPropsObject.openFromDropdown()
- assert(dispatchSpy.calledOnce)
- assert(duckActionSpies.openFromDropdown.calledOnce)
- assert.equal(
- duckActionSpies.openFromDropdown.getCall(0).args[0],
- undefined
- )
- })
- })
-
- describe('updateSendFrom()', () => {
- it('should dispatch an updateSendFrom action', () => {
- mapDispatchToPropsObject.updateSendFrom('mockFrom')
- assert(dispatchSpy.calledOnce)
- assert.equal(
- actionSpies.updateSendFrom.getCall(0).args[0],
- 'mockFrom'
- )
- })
- })
-
- describe('setSendTokenBalance()', () => {
- it('should dispatch an setSendTokenBalance action', () => {
- mapDispatchToPropsObject.setSendTokenBalance('mockUsersToken', 'mockSelectedToken')
- assert(dispatchSpy.calledOnce)
- assert.equal(
- actionSpies.setSendTokenBalance.getCall(0).args[0],
- 'mockUsersTokenmockSelectedToken'
- )
- })
- })
-
- })
-
-})
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 9bbb67506..000000000
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
+++ /dev/null
@@ -1,58 +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,
- onClick: PropTypes.func,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const { gasTotal, onClick, gasLoadingError } = 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="sliders-icon-container"
- onClick={onClick}
- disabled={!gasTotal && !gasLoadingError}
- >
- <i className="fa fa-sliders sliders-icon" />
- </button>
- </div>
- )
- }
-}
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/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
deleted file mode 100644
index 9ff01493a..000000000
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import {shallow} from 'enzyme'
-import GasFeeDisplay from '../gas-fee-display.component'
-import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display'
-import sinon from 'sinon'
-
-
-const propsMethodSpies = {
- showCustomizeGasModal: sinon.spy(),
-}
-
-describe('SendGasRow Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<GasFeeDisplay
- conversionRate={20}
- gasTotal={'mockGasTotal'}
- onClick={propsMethodSpies.showCustomizeGasModal}
- primaryCurrency={'mockPrimaryCurrency'}
- convertedCurrency={'mockConvertedCurrency'}
- />, {context: {t: str => str + '_t'}})
- })
-
- afterEach(() => {
- propsMethodSpies.showCustomizeGasModal.resetHistory()
- })
-
- describe('render', () => {
- it('should render a CurrencyDisplay component', () => {
- assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
- })
-
- it('should render the CurrencyDisplay with the correct props', () => {
- const {
- type,
- value,
- } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props()
- assert.equal(type, 'PRIMARY')
- assert.equal(value, 'mockGasTotal')
- })
-
- it('should render the Button with the correct props', () => {
- const {
- onClick,
- } = wrapper.find('button').props()
- assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
- onClick()
- assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
- })
- })
-})
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 91b58cfd0..000000000
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
+++ /dev/null
@@ -1,48 +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'
-
-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,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- conversionRate,
- convertedCurrency,
- gasLoadingError,
- gasTotal,
- gasFeeError,
- showCustomizeGasModal,
- } = this.props
-
- return (
- <SendRowWrapper
- label={`${this.context.t('gasFee')}:`}
- showError={gasFeeError}
- errorType={'gasFee'}
- >
- <GasFeeDisplay
- conversionRate={conversionRate}
- convertedCurrency={convertedCurrency}
- gasLoadingError={gasLoadingError}
- gasTotal={gasTotal}
- onClick={() => showCustomizeGasModal()}
- />
- </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 8f8e3e4dd..000000000
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getConversionRate,
- getCurrentCurrency,
- getGasTotal,
-} from '../../send.selectors.js'
-import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js'
-import { showModal } from '../../../../actions'
-import SendGasRow from './send-gas-row.component'
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
-
-function mapStateToProps (state) {
- return {
- conversionRate: getConversionRate(state),
- convertedCurrency: getCurrentCurrency(state),
- gasTotal: getGasTotal(state),
- gasFeeError: gasFeeIsInError(state),
- gasLoadingError: getGasLoadingError(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })),
- }
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
deleted file mode 100644
index 96f6293c2..000000000
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const selectors = {
- gasFeeIsInError,
- getGasLoadingError,
-}
-
-module.exports = selectors
-
-function getGasLoadingError (state) {
- return state.send.errors.gasLoading
-}
-
-function gasFeeIsInError (state) {
- return Boolean(state.send.errors.gasFee)
-}
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
deleted file mode 100644
index 54a92bd2d..000000000
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-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 SendGasRow from '../send-gas-row.component.js'
-
-import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
-import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
-
-const propsMethodSpies = {
- showCustomizeGasModal: sinon.spy(),
-}
-
-describe('SendGasRow Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<SendGasRow
- conversionRate={20}
- convertedCurrency={'mockConvertedCurrency'}
- gasFeeError={'mockGasFeeError'}
- gasLoadingError={false}
- gasTotal={'mockGasTotal'}
- showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
- />, { context: { t: str => str + '_t' } })
- })
-
- afterEach(() => {
- propsMethodSpies.showCustomizeGasModal.resetHistory()
- })
-
- describe('render', () => {
- it('should render a SendRowWrapper component', () => {
- assert.equal(wrapper.find(SendRowWrapper).length, 1)
- })
-
- it('should pass the correct props to SendRowWrapper', () => {
- const {
- label,
- showError,
- errorType,
- } = wrapper.find(SendRowWrapper).props()
-
- assert.equal(label, 'gasFee_t:')
- assert.equal(showError, 'mockGasFeeError')
- assert.equal(errorType, 'gasFee')
- })
-
- it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => {
- assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay))
- })
-
- it('should render the GasFeeDisplay with the correct props', () => {
- const {
- conversionRate,
- convertedCurrency,
- gasLoadingError,
- gasTotal,
- onClick,
- } = wrapper.find(SendRowWrapper).childAt(0).props()
- assert.equal(conversionRate, 20)
- assert.equal(convertedCurrency, 'mockConvertedCurrency')
- assert.equal(gasLoadingError, false)
- assert.equal(gasTotal, 'mockGasTotal')
- assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
- onClick()
- assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
- })
- })
-})
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 2ce062505..000000000
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-let mapStateToProps
-let mapDispatchToProps
-
-const actionSpies = {
- showModal: sinon.spy(),
-}
-
-proxyquire('../send-gas-row.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../send.selectors.js': {
- getConversionRate: (s) => `mockConversionRate:${s}`,
- getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- },
- './send-gas-row.selectors.js': {
- getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
- gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
- },
- '../../../../actions': actionSpies,
-})
-
-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',
- })
- })
-
- })
-
- describe('mapDispatchToProps()', () => {
- let dispatchSpy
- let mapDispatchToPropsObject
-
- beforeEach(() => {
- dispatchSpy = sinon.spy()
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
- })
-
- describe('showCustomizeGasModal()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.showCustomizeGasModal()
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- actionSpies.showModal.getCall(0).args[0],
- { name: 'CUSTOMIZE_GAS' }
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
deleted file mode 100644
index d46dd9d8b..000000000
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import assert from 'assert'
-import {
- gasFeeIsInError,
- getGasLoadingError,
-} from '../send-gas-row.selectors.js'
-
-describe('send-gas-row selectors', () => {
-
- describe('getGasLoadingError()', () => {
- it('should return send.errors.gasLoading', () => {
- const state = {
- send: {
- errors: {
- gasLoading: 'abc',
- },
- },
- }
-
- assert.equal(getGasLoadingError(state), 'abc')
- })
- })
-
- describe('gasFeeIsInError()', () => {
- it('should return true if send.errors.gasFee is truthy', () => {
- const state = {
- send: {
- errors: {
- gasFee: 'def',
- },
- },
- }
-
- assert.equal(gasFeeIsInError(state), true)
- })
-
- it('should return false send.errors.gasFee is falsely', () => {
- const state = {
- send: {
- errors: {
- gasFee: null,
- },
- },
- }
-
- assert.equal(gasFeeIsInError(state), false)
- })
- })
-
-})
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 b7528a15f..000000000
--- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import SendRowErrorMessage from './send-row-error-message/'
-
-export default class SendRowWrapper extends Component {
-
- static propTypes = {
- children: PropTypes.node,
- errorType: PropTypes.string,
- label: PropTypes.string,
- showError: PropTypes.bool,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- render () {
- const {
- children,
- errorType = '',
- label,
- showError = false,
- } = 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}/>}
- {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 17c75c817..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ /dev/null
@@ -1,72 +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 } from './send-to-row.utils.js'
-
-export default class SendToRow extends Component {
-
- static propTypes = {
- closeToDropdown: PropTypes.func,
- hasHexData: PropTypes.bool.isRequired,
- inError: PropTypes.bool,
- network: PropTypes.string,
- openToDropdown: PropTypes.func,
- to: PropTypes.string,
- toAccounts: PropTypes.array,
- toDropdownOpen: PropTypes.bool,
- updateGas: PropTypes.func,
- updateSendTo: PropTypes.func,
- updateSendToError: PropTypes.func,
- scanQrCode: PropTypes.func,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- handleToChange (to, nickname = '', toError) {
- const { hasHexData, updateSendTo, updateSendToError, updateGas } = this.props
- const toErrorObject = getToErrorObject(to, toError, hasHexData)
- updateSendTo(to, nickname)
- updateSendToError(toErrorObject)
- if (toErrorObject.to === null) {
- updateGas({ to })
- }
- }
-
- render () {
- const {
- closeToDropdown,
- inError,
- network,
- openToDropdown,
- to,
- toAccounts,
- toDropdownOpen,
- } = this.props
-
- return (
- <SendRowWrapper
- errorType={'to'}
- label={`${this.context.t('to')}: `}
- showError={inError}
- >
- <EnsInput
- scanQrCode={_ => this.props.scanQrCode()}
- accounts={toAccounts}
- closeDropdown={() => closeToDropdown()}
- dropdownOpen={toDropdownOpen}
- inError={inError}
- name={'address'}
- network={network}
- onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)}
- 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 3ee188bad..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getCurrentNetwork,
- getSendTo,
- getSendToAccounts,
- getSendHexData,
-} from '../../send.selectors.js'
-import {
- getToDropdownOpen,
- sendToIsInError,
-} from './send-to-row.selectors.js'
-import {
- updateSendTo,
-} from '../../../../actions'
-import {
- updateSendErrors,
- 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),
- network: getCurrentNetwork(state),
- to: getSendTo(state),
- toAccounts: getSendToAccounts(state),
- toDropdownOpen: getToDropdownOpen(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- closeToDropdown: () => dispatch(closeToDropdown()),
- openToDropdown: () => dispatch(openToDropdown()),
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- updateSendToError: (toErrorObject) => {
- dispatch(updateSendErrors(toErrorObject))
- },
- }
-}
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
deleted file mode 100644
index 8919014be..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const selectors = {
- getToDropdownOpen,
- sendToIsInError,
-}
-
-module.exports = selectors
-
-function getToDropdownOpen (state) {
- return state.send.toDropdownOpen
-}
-
-function sendToIsInError (state) {
- return Boolean(state.send.errors.to)
-}
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 0eeaa3a11..000000000
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const {
- REQUIRED_ERROR,
- INVALID_RECIPIENT_ADDRESS_ERROR,
-} = require('../../send.constants')
-const { isValidAddress } = require('../../../../util')
-
-function getToErrorObject (to, toError = null, hasHexData = false) {
- if (!to) {
- if (!hasHexData) {
- toError = REQUIRED_ERROR
- }
- } else if (!isValidAddress(to) && !toError) {
- toError = INVALID_RECIPIENT_ADDRESS_ERROR
- }
-
- return { to: toError }
-}
-
-module.exports = {
- getToErrorObject,
-}
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
deleted file mode 100644
index 591229deb..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import proxyquire from 'proxyquire'
-
-const SendToRow = proxyquire('../send-to-row.component.js', {
- './send-to-row.utils.js': {
- getToErrorObject: (to, toError) => ({
- to: to === false ? null : `mockToErrorObject:${to}${toError}`,
- }),
- },
-}).default
-
-import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
-import EnsInput from '../../../../ens-input'
-
-const propsMethodSpies = {
- closeToDropdown: sinon.spy(),
- openToDropdown: sinon.spy(),
- updateGas: sinon.spy(),
- updateSendTo: sinon.spy(),
- updateSendToError: sinon.spy(),
-}
-
-sinon.spy(SendToRow.prototype, 'handleToChange')
-
-describe('SendToRow Component', function () {
- let wrapper
- let instance
-
- beforeEach(() => {
- wrapper = shallow(<SendToRow
- closeToDropdown={propsMethodSpies.closeToDropdown}
- inError={false}
- network={'mockNetwork'}
- openToDropdown={propsMethodSpies.openToDropdown}
- to={'mockTo'}
- toAccounts={['mockAccount']}
- toDropdownOpen={false}
- updateGas={propsMethodSpies.updateGas}
- updateSendTo={propsMethodSpies.updateSendTo}
- updateSendToError={propsMethodSpies.updateSendToError}
- />, { context: { t: str => str + '_t' } })
- instance = wrapper.instance()
- })
-
- afterEach(() => {
- propsMethodSpies.closeToDropdown.resetHistory()
- propsMethodSpies.openToDropdown.resetHistory()
- propsMethodSpies.updateSendTo.resetHistory()
- propsMethodSpies.updateSendToError.resetHistory()
- SendToRow.prototype.handleToChange.resetHistory()
- })
-
- describe('handleToChange', () => {
-
- it('should call updateSendTo', () => {
- assert.equal(propsMethodSpies.updateSendTo.callCount, 0)
- instance.handleToChange('mockTo2', 'mockNickname')
- assert.equal(propsMethodSpies.updateSendTo.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendTo.getCall(0).args,
- ['mockTo2', 'mockNickname']
- )
- })
-
- it('should call updateSendToError', () => {
- assert.equal(propsMethodSpies.updateSendToError.callCount, 0)
- instance.handleToChange('mockTo2', '', 'mockToError')
- assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateSendToError.getCall(0).args,
- [{ to: 'mockToErrorObject:mockTo2mockToError' }]
- )
- })
-
- it('should not call updateGas if there is a to error', () => {
- assert.equal(propsMethodSpies.updateGas.callCount, 0)
- instance.handleToChange('mockTo2')
- assert.equal(propsMethodSpies.updateGas.callCount, 0)
- })
-
- it('should call updateGas if there is no to error', () => {
- assert.equal(propsMethodSpies.updateGas.callCount, 0)
- instance.handleToChange(false)
- assert.equal(propsMethodSpies.updateGas.callCount, 1)
- })
- })
-
- describe('render', () => {
- it('should render a SendRowWrapper component', () => {
- assert.equal(wrapper.find(SendRowWrapper).length, 1)
- })
-
- it('should pass the correct props to SendRowWrapper', () => {
- const {
- errorType,
- label,
- showError,
- } = wrapper.find(SendRowWrapper).props()
-
- assert.equal(errorType, 'to')
-
- assert.equal(label, 'to_t: ')
-
- assert.equal(showError, false)
- })
-
- it('should render an EnsInput as a child of the SendRowWrapper', () => {
- assert(wrapper.find(SendRowWrapper).childAt(0).is(EnsInput))
- })
-
- it('should render the EnsInput with the correct props', () => {
- const {
- accounts,
- closeDropdown,
- dropdownOpen,
- inError,
- name,
- network,
- onChange,
- openDropdown,
- placeholder,
- to,
- } = wrapper.find(SendRowWrapper).childAt(0).props()
- assert.deepEqual(accounts, ['mockAccount'])
- assert.equal(dropdownOpen, false)
- assert.equal(inError, false)
- assert.equal(name, 'address')
- assert.equal(network, 'mockNetwork')
- assert.equal(placeholder, 'recipientAddress_t')
- assert.equal(to, 'mockTo')
- assert.equal(propsMethodSpies.closeToDropdown.callCount, 0)
- closeDropdown()
- assert.equal(propsMethodSpies.closeToDropdown.callCount, 1)
- assert.equal(propsMethodSpies.openToDropdown.callCount, 0)
- openDropdown()
- assert.equal(propsMethodSpies.openToDropdown.callCount, 1)
- assert.equal(SendToRow.prototype.handleToChange.callCount, 0)
- onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' })
- assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
- assert.deepEqual(
- SendToRow.prototype.handleToChange.getCall(0).args,
- ['mockNewTo', 'mockNewNickname', 'mockToError']
- )
- })
- })
-})
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 dfce7652f..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
+++ /dev/null
@@ -1,115 +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(),
-}
-
-proxyquire('../send-to-row.container.js', {
- 'react-redux': {
- connect: (ms, md) => {
- mapStateToProps = ms
- mapDispatchToProps = md
- return () => ({})
- },
- },
- '../../send.selectors.js': {
- getCurrentNetwork: (s) => `mockNetwork:${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}`,
- },
- '../../../../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',
- network: 'mockNetwork:mockState',
- to: 'mockTo:mockState',
- toAccounts: 'mockToAccounts:mockState',
- toDropdownOpen: 'mockToDropdownOpen: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'
- )
- })
- })
-
- })
-
-})
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
deleted file mode 100644
index 122ad3265..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import assert from 'assert'
-import {
- getToDropdownOpen,
- sendToIsInError,
-} from '../send-to-row.selectors.js'
-
-describe('send-to-row selectors', () => {
-
- describe('getToDropdownOpen()', () => {
- it('should return send.getToDropdownOpen', () => {
- const state = {
- send: {
- toDropdownOpen: false,
- },
- }
-
- assert.equal(getToDropdownOpen(state), false)
- })
- })
-
- describe('sendToIsInError()', () => {
- it('should return true if send.errors.to is truthy', () => {
- const state = {
- send: {
- errors: {
- to: 'abc',
- },
- },
- }
-
- assert.equal(sendToIsInError(state), true)
- })
-
- it('should return false if send.errors.to is falsy', () => {
- const state = {
- send: {
- errors: {
- to: null,
- },
- },
- }
-
- assert.equal(sendToIsInError(state), false)
- })
- })
-
-})
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 c779aeb76..000000000
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import sinon from 'sinon'
-
-import {
- REQUIRED_ERROR,
- INVALID_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,
-} = 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',
- })
- })
- })
-
-})
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 230bf450f..000000000
--- a/ui/app/components/send/send-footer/send-footer.component.js
+++ /dev/null
@@ -1,104 +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,
- };
-
- static contextTypes = {
- t: 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
-
- // 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(() => history.push(CONFIRM_TRANSACTION_ROUTE))
- }
-
- formShouldBeDisabled () {
- const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
- const missingTokenBalance = selectedToken && !tokenBalance
- return inError || !gasTotal || missingTokenBalance || !(data || to)
- }
-
- 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 60de4d030..000000000
--- a/ui/app/components/send/send-footer/send-footer.container.js
+++ /dev/null
@@ -1,105 +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,
-} 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),
- }
-}
-
-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 65e4bb654..000000000
--- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js
+++ /dev/null
@@ -1,232 +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}
- />, { context: { t: str => str } })
- })
-
- 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 } })
- })
-
- 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 cf4c893ee..000000000
--- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js
+++ /dev/null
@@ -1,196 +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('mockConstructedTxParams'),
- 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}`,
- },
- './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',
- })
- })
-
- })
-
- 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', 'mockConstructedTxParams' ]
- )
- })
-
- 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,
- [ 'mockConstructedTxParams' ]
- )
- })
- })
-
- 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 fb7beca16..000000000
--- a/ui/app/components/send/send.component.js
+++ /dev/null
@@ -1,212 +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,
- 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,
- updateAndSetGasTotal,
- } = this.props
-
- updateAndSetGasTotal({
- 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({
- amount,
- amountConversionRate,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- selectedToken,
- tokenBalance,
- })
- : { gasFee: null }
- updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject))
- }
-
- if (!uninitialized) {
-
- if (network !== prevNetwork && network !== 'loading') {
- updateSendTokenBalance({
- selectedToken,
- tokenContract,
- address,
- })
- this.updateGas()
- }
- }
- }
-
- componentWillMount () {
- const {
- from: { address },
- selectedToken,
- tokenContract,
- updateSendTokenBalance,
- } = this.props
- updateSendTokenBalance({
- selectedToken,
- tokenContract,
- address,
- })
- this.updateGas()
-
- // 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 8acdf0641..000000000
--- a/ui/app/components/send/send.constants.js
+++ /dev/null
@@ -1,57 +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 REQUIRED_ERROR = 'required'
-
-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,
- 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 87056499f..000000000
--- a/ui/app/components/send/send.container.js
+++ /dev/null
@@ -1,108 +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 {
- 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 {
- updateAndSetGasTotal: ({
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- recentBlocks,
- selectedAddress,
- selectedToken,
- to,
- value,
- data,
- }) => {
- !editingTransactionId
- ? dispatch(updateGasData({ 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)),
- }
-}
diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js
deleted file mode 100644
index eb22a08b7..000000000
--- a/ui/app/components/send/send.selectors.js
+++ /dev/null
@@ -1,299 +0,0 @@
-const { valuesFor } = require('../../util')
-const abi = require('human-standard-token-abi')
-const {
- multiplyCurrencies,
-} = require('../../conversion-util')
-const {
- estimateGasPriceFromRecentBlocks,
-} = require('./send.utils')
-
-const selectors = {
- accountsWithSendEtherInfoSelector,
- // autoAddToBetaUI,
- 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,
- getTokenBalance,
- getTokenExchangeRate,
- getUnapprovedTxs,
- transactionsSelector,
- getQrCodeData,
-}
-
-module.exports = selectors
-
-function accountsWithSendEtherInfoSelector (state) {
- const {
- accounts,
- identities,
- } = state.metamask
-
- const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
- return Object.assign({}, account, identities[key])
- })
-
- return accountsWithSendEtherInfo
-}
-
-// function autoAddToBetaUI (state) {
-// const autoAddTransactionThreshold = 12
-// const autoAddAccountsThreshold = 2
-// const autoAddTokensThreshold = 1
-
-// const numberOfTransactions = state.metamask.selectedAddressTxList.length
-// const numberOfAccounts = Object.keys(state.metamask.accounts).length
-// const numberOfTokensAdded = state.metamask.tokens.length
-
-// const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
-// (numberOfAccounts > autoAddAccountsThreshold) &&
-// (numberOfTokensAdded > autoAddTokensThreshold)
-// const userIsNotInBeta = !state.metamask.featureFlags.betaUI
-
-// return userIsNotInBeta && userPassesThreshold
-// }
-
-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
-}
-
-function getGasPrice (state) {
- return state.metamask.send.gasPrice
-}
-
-function getGasPriceFromRecentBlocks (state) {
- return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks)
-}
-
-function getGasTotal (state) {
- return state.metamask.send.gasTotal
-}
-
-function getPrimaryCurrency (state) {
- const selectedToken = getSelectedToken(state)
- return selectedToken && selectedToken.symbol
-}
-
-function getRecentBlocks (state) {
- return state.metamask.recentBlocks
-}
-
-function getSelectedAccount (state) {
- const accounts = state.metamask.accounts
- const selectedAddress = getSelectedAddress(state)
-
- return accounts[selectedAddress]
-}
-
-function getSelectedAddress (state) {
- const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[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 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 eb1667c63..000000000
--- a/ui/app/components/send/send.utils.js
+++ /dev/null
@@ -1,330 +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: 'dec',
- },
- {
- value: calcTokenAmount(amountInDec, decimals),
- fromNumericBase: 'dec',
- },
- )
-
- 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 ({
- amount,
- 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) + ''
-}
-
-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 not, fall back to block gasLimit
- paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 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 f4943e707..000000000
--- a/ui/app/components/send/tests/send-component.test.js
+++ /dev/null
@@ -1,338 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-
-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 propsMethodSpies = {
- updateAndSetGasTotal: sinon.spy(),
- updateSendErrors: sinon.spy(),
- updateSendTokenBalance: sinon.spy(),
- resetSendState: 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'}
- 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'}
- updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
- 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.updateAndSetGasTotal.resetHistory()
- propsMethodSpies.updateSendErrors.resetHistory()
- propsMethodSpies.updateSendTokenBalance.resetHistory()
- })
-
- it('should call componentDidMount', () => {
- assert(SendTransactionScreen.prototype.componentDidMount.calledOnce)
- })
-
- describe('componentWillMount', () => {
- it('should call this.updateGas', () => {
- SendTransactionScreen.prototype.updateGas.resetHistory()
- propsMethodSpies.updateSendErrors.resetHistory()
- assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
- wrapper.instance().componentWillMount()
- 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],
- {
- amount: 'mockAmount',
- amountConversionRate: 'mockAmountConversionRate',
- balance: 'mockBalance',
- conversionRate: 10,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- selectedToken: 'mockSelectedToken',
- tokenBalance: 'mockTokenBalance',
- }
- )
- })
-
- 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 updateAndSetGasTotal with the correct params if no to prop is passed', () => {
- propsMethodSpies.updateAndSetGasTotal.resetHistory()
- wrapper.instance().updateGas()
- assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
- assert.deepEqual(
- propsMethodSpies.updateAndSetGasTotal.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 updateAndSetGasTotal with the correct params if a to prop is passed', () => {
- propsMethodSpies.updateAndSetGasTotal.resetHistory()
- wrapper.setProps({ to: 'someAddress' })
- wrapper.instance().updateGas()
- assert.equal(
- propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to,
- 'someaddress',
- )
- })
-
- it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
- propsMethodSpies.updateAndSetGasTotal.resetHistory()
- wrapper.instance().updateGas({ to: '0xABC' })
- assert.equal(propsMethodSpies.updateAndSetGasTotal.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 6aa4bf826..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('updateAndSetGasTotal()', () => {
- 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.updateAndSetGasTotal(mockProps)
- assert(dispatchSpy.calledOnce)
- assert.equal(
- actionSpies.setGasTotal.getCall(0).args[0],
- '0x30x4'
- )
- })
-
- it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
- const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
- mapDispatchToPropsObject.updateAndSetGasTotal(
- Object.assign({}, mockProps, {editingTransactionId: false})
- )
- assert(dispatchSpy.calledOnce)
- assert.deepEqual(
- actionSpies.updateGasData.getCall(0).args[0],
- { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, 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/send/tests/send-selectors-test-data.js
deleted file mode 100644
index 30a2666cf..000000000
--- a/ui/app/components/send/tests/send-selectors-test-data.js
+++ /dev/null
@@ -1,231 +0,0 @@
-module.exports = {
- 'metamask': {
- 'isInitialized': true,
- 'isUnlocked': true,
- 'featureFlags': {'betaUI': true, 'sendHexData': true},
- 'rpcTarget': 'https://rawtestrpc.metamask.io/',
- 'identities': {
- '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
- 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- 'name': 'Send Account 1',
- },
- '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
- 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- 'name': 'Send Account 2',
- },
- '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
- 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- 'name': 'Send Account 3',
- },
- '0xd85a4b6a394794842887b8284293d69163007bbb': {
- 'address': '0xd85a4b6a394794842887b8284293d69163007bbb',
- 'name': 'Send Account 4',
- },
- },
- 'currentBlockGasLimit': '0x4c1878',
- 'currentCurrency': 'USD',
- 'conversionRate': 1200.88200327,
- 'conversionDate': 1489013762,
- 'nativeCurrency': 'ETH',
- 'noActiveNotices': true,
- 'frequentRpcList': [],
- 'network': '3',
- 'accounts': {
- '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
- 'code': '0x',
- 'balance': '0x47c9d71831c76efe',
- 'nonce': '0x1b',
- 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- },
- '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
- 'code': '0x',
- 'balance': '0x37452b1315889f80',
- 'nonce': '0xa',
- 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- },
- '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
- 'code': '0x',
- 'balance': '0x30c9d71831c76efe',
- 'nonce': '0x1c',
- 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- },
- '0xd85a4b6a394794842887b8284293d69163007bbb': {
- 'code': '0x',
- 'balance': '0x0',
- 'nonce': '0x0',
- 'address': '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- },
- 'addressBook': [
- {
- 'address': '0x06195827297c7a80a443b6894d3bdb8824b43896',
- 'name': 'Address Book Account 1',
- },
- ],
- 'tokens': [
- {
- 'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896',
- 'decimals': 18,
- 'symbol': 'ABC',
- },
- {
- 'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- 'decimals': 4,
- 'symbol': 'DEF',
- },
- {
- 'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b',
- 'decimals': 18,
- 'symbol': 'GHI',
- },
- ],
- 'tokenExchangeRates': {
- 'def_eth': {
- rate: 2.0,
- },
- 'ghi_eth': {
- rate: 31.01,
- },
- },
- 'transactions': {},
- 'selectedAddressTxList': [
- {
- 'id': 'mockTokenTx1',
- 'txParams': {
- 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- 'time': 1700000000000,
- },
- {
- 'id': 'mockTokenTx2',
- 'txParams': {
- 'to': '0xafaketokenaddress',
- },
- 'time': 1600000000000,
- },
- {
- 'id': 'mockTokenTx3',
- 'txParams': {
- 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- 'time': 1500000000000,
- },
- {
- 'id': 'mockEthTx1',
- 'txParams': {
- 'to': '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- 'time': 1400000000000,
- },
- ],
- 'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- 'unapprovedMsgs': {
- '0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 },
- '0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 },
- '0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 },
- },
- 'unapprovedMsgCount': 0,
- 'unapprovedPersonalMsgs': {},
- 'unapprovedPersonalMsgCount': 0,
- 'keyringTypes': [
- 'Simple Key Pair',
- 'HD Key Tree',
- ],
- 'keyrings': [
- {
- 'type': 'HD Key Tree',
- 'accounts': [
- 'fdea65c8e26263f6d9a1b5de9555d2931a33b825',
- 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- '2f8d4a878cfa04a6e60d46362f5644deab66572d',
- ],
- },
- {
- 'type': 'Simple Key Pair',
- 'accounts': [
- '0xd85a4b6a394794842887b8284293d69163007bbb',
- ],
- },
- ],
- 'selectedAddress': '0xd85a4b6a394794842887b8284293d69163007bbb',
- 'provider': {
- 'type': 'testnet',
- },
- 'shapeShiftTxList': [
- { id: 'shapeShiftTx1', 'time': 1675000000000 },
- { id: 'shapeShiftTx2', 'time': 1575000000000 },
- { id: 'shapeShiftTx3', 'time': 1475000000000 },
- ],
- 'lostAccounts': [],
- 'send': {
- 'gasLimit': '0xFFFF',
- 'gasPrice': '0xaa',
- 'gasTotal': '0xb451dc41b578',
- 'tokenBalance': 3434,
- 'from': {
- 'address': '0xabcdefg',
- 'balance': '0x5f4e3d2c1',
- },
- 'to': '0x987fedabc',
- 'amount': '0x080',
- 'memo': '',
- 'errors': {
- 'someError': null,
- },
- 'maxModeOn': false,
- 'editingTransactionId': 97531,
- 'forceGasMin': true,
- },
- 'unapprovedTxs': {
- '4768706228115573': {
- 'id': 4768706228115573,
- 'time': 1487363153561,
- 'status': 'unapproved',
- 'gasMultiplier': 1,
- 'metamaskNetworkId': '3',
- 'txParams': {
- 'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- 'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
- 'value': '0xde0b6b3a7640000',
- 'metamaskId': 4768706228115573,
- 'metamaskNetworkId': '3',
- 'gas': '0x5209',
- },
- 'gasLimitSpecified': false,
- 'estimatedGas': '0x5209',
- 'txFee': '17e0186e60800',
- 'txValue': 'de0b6b3a7640000',
- 'maxCost': 'de234b52e4a0800',
- 'gasPrice': '4a817c800',
- },
- },
- 'currentLocale': 'en',
- recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'],
- },
- 'appState': {
- 'menuOpen': false,
- 'currentView': {
- 'name': 'accountDetail',
- 'detailView': null,
- 'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
- },
- 'accountDetail': {
- 'subview': 'transactions',
- },
- 'modal': {
- 'modalState': {},
- 'previousModalState': {},
- },
- 'transForward': true,
- 'isLoading': false,
- 'warning': null,
- 'scrollToBottom': false,
- 'forgottenPassword': null,
- },
- 'identities': {},
- 'send': {
- 'fromDropdownOpen': false,
- 'toDropdownOpen': false,
- 'errors': { 'someError': null },
- },
-}
diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js
deleted file mode 100644
index e7e901f0d..000000000
--- a/ui/app/components/send/tests/send-selectors.test.js
+++ /dev/null
@@ -1,705 +0,0 @@
-import assert from 'assert'
-import sinon from 'sinon'
-import selectors from '../send.selectors.js'
-const {
- accountsWithSendEtherInfoSelector,
- // autoAddToBetaUI,
- getAddressBook,
- getBlockGasLimit,
- getAmountConversionRate,
- getConversionRate,
- getCurrentAccountWithSendEtherInfo,
- getCurrentCurrency,
- getCurrentNetwork,
- getCurrentViewContext,
- getNativeCurrency,
- getForceGasMin,
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getPrimaryCurrency,
- getRecentBlocks,
- getSelectedAccount,
- getSelectedAddress,
- getSelectedIdentity,
- getSelectedToken,
- getSelectedTokenContract,
- getSelectedTokenExchangeRate,
- getSelectedTokenToFiatRate,
- getSendAmount,
- getSendEditingTransactionId,
- getSendErrors,
- getSendFrom,
- getSendFromBalance,
- getSendFromObject,
- getSendHexDataFeatureFlagState,
- getSendMaxModeState,
- getSendTo,
- getSendToAccounts,
- getTokenBalance,
- getTokenExchangeRate,
- getUnapprovedTxs,
- transactionsSelector,
-} = selectors
-import mockState from './send-selectors-test-data'
-
-describe('send selectors', () => {
- const tempGlobalEth = Object.assign({}, global.eth)
- beforeEach(() => {
- global.eth = {
- contract: sinon.stub().returns({
- at: address => 'mockAt:' + address,
- }),
- }
- })
-
- afterEach(() => {
- global.eth = tempGlobalEth
- })
-
- describe('accountsWithSendEtherInfoSelector()', () => {
- it('should return an array of account objects with name info from identities', () => {
- assert.deepEqual(
- accountsWithSendEtherInfoSelector(mockState),
- [
- {
- code: '0x',
- balance: '0x47c9d71831c76efe',
- nonce: '0x1b',
- address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- name: 'Send Account 1',
- },
- {
- code: '0x',
- balance: '0x37452b1315889f80',
- nonce: '0xa',
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- name: 'Send Account 2',
- },
- {
- code: '0x',
- balance: '0x30c9d71831c76efe',
- nonce: '0x1c',
- address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- name: 'Send Account 3',
- },
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- },
- ]
- )
- })
- })
-
- // describe('autoAddToBetaUI()', () => {
- // it('should', () => {
- // assert.deepEqual(
- // autoAddToBetaUI(mockState),
-
- // )
- // })
- // })
-
- describe('getAddressBook()', () => {
- it('should return the address book', () => {
- assert.deepEqual(
- getAddressBook(mockState),
- [
- {
- address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
- name: 'Address Book Account 1',
- },
- ],
- )
- })
- })
-
- describe('getAmountConversionRate()', () => {
- it('should return the token conversion rate if a token is selected', () => {
- assert.equal(
- getAmountConversionRate(mockState),
- 2401.76400654
- )
- })
-
- it('should return the eth conversion rate if no token is selected', () => {
- const editedMockState = {
- metamask: Object.assign({}, mockState.metamask, { selectedTokenAddress: null }),
- }
- assert.equal(
- getAmountConversionRate(editedMockState),
- 1200.88200327
- )
- })
- })
-
- describe('getBlockGasLimit', () => {
- it('should return the current block gas limit', () => {
- assert.deepEqual(
- getBlockGasLimit(mockState),
- '0x4c1878'
- )
- })
- })
-
- describe('getConversionRate()', () => {
- it('should return the eth conversion rate', () => {
- assert.deepEqual(
- getConversionRate(mockState),
- 1200.88200327
- )
- })
- })
-
- describe('getCurrentAccountWithSendEtherInfo()', () => {
- it('should return the currently selected account with identity info', () => {
- assert.deepEqual(
- getCurrentAccountWithSendEtherInfo(mockState),
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- }
- )
- })
- })
-
- describe('getCurrentCurrency()', () => {
- it('should return the currently selected currency', () => {
- assert.equal(
- getCurrentCurrency(mockState),
- 'USD'
- )
- })
- })
-
- describe('getNativeCurrency()', () => {
- it('should return the ticker symbol of the selected network', () => {
- assert.equal(
- getNativeCurrency(mockState),
- 'ETH'
- )
- })
- })
-
- describe('getCurrentNetwork()', () => {
- it('should return the id of the currently selected network', () => {
- assert.equal(
- getCurrentNetwork(mockState),
- '3'
- )
- })
- })
-
- describe('getCurrentViewContext()', () => {
- it('should return the context of the current view', () => {
- assert.equal(
- getCurrentViewContext(mockState),
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
- )
- })
- })
-
- describe('getForceGasMin()', () => {
- it('should get the send.forceGasMin property', () => {
- assert.equal(
- getForceGasMin(mockState),
- true
- )
- })
- })
-
- describe('getGasLimit()', () => {
- it('should return the send.gasLimit', () => {
- assert.equal(
- getGasLimit(mockState),
- '0xFFFF'
- )
- })
- })
-
- describe('getGasPrice()', () => {
- it('should return the send.gasPrice', () => {
- assert.equal(
- getGasPrice(mockState),
- '0xaa'
- )
- })
- })
-
- describe('getGasTotal()', () => {
- it('should return the send.gasTotal', () => {
- assert.equal(
- getGasTotal(mockState),
- '0xb451dc41b578'
- )
- })
- })
-
- describe('getPrimaryCurrency()', () => {
- it('should return the symbol of the selected token', () => {
- assert.equal(
- getPrimaryCurrency(mockState),
- 'DEF'
- )
- })
- })
-
- describe('getRecentBlocks()', () => {
- it('should return the recent blocks', () => {
- assert.deepEqual(
- getRecentBlocks(mockState),
- ['mockBlock1', 'mockBlock2', 'mockBlock3']
- )
- })
- })
-
- describe('getSelectedAccount()', () => {
- it('should return the currently selected account', () => {
- assert.deepEqual(
- getSelectedAccount(mockState),
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- }
- )
- })
- })
-
- describe('getSelectedAddress()', () => {
- it('should', () => {
- assert.equal(
- getSelectedAddress(mockState),
- '0xd85a4b6a394794842887b8284293d69163007bbb'
- )
- })
- })
-
- describe('getSelectedIdentity()', () => {
- it('should return the identity object of the currently selected address', () => {
- assert.deepEqual(
- getSelectedIdentity(mockState),
- {
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- }
- )
- })
- })
-
- describe('getSelectedToken()', () => {
- it('should return the currently selected token if selected', () => {
- assert.deepEqual(
- getSelectedToken(mockState),
- {
- address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'DEF',
- }
- )
- })
-
- it('should return the send token if none is currently selected, but a send token exists', () => {
- const mockSendToken = {
- address: '0x123456708414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'JKL',
- }
- const editedMockState = {
- metamask: Object.assign({}, mockState.metamask, {
- selectedTokenAddress: null,
- send: {
- token: mockSendToken,
- },
- }),
- }
- assert.deepEqual(
- getSelectedToken(editedMockState),
- Object.assign({}, mockSendToken)
- )
- })
- })
-
- describe('getSelectedTokenContract()', () => {
- it('should return the contract at the selected token address', () => {
- assert.equal(
- getSelectedTokenContract(mockState),
- 'mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3'
- )
- })
-
- it('should return null if no token is selected', () => {
- const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
- assert.equal(
- getSelectedTokenContract(Object.assign({}, mockState, { metamask: modifiedMetamaskState })),
- null
- )
- })
- })
-
- describe('getSelectedTokenExchangeRate()', () => {
- it('should return the exchange rate for the selected token', () => {
- assert.equal(
- getSelectedTokenExchangeRate(mockState),
- 2.0
- )
- })
- })
-
- describe('getSelectedTokenToFiatRate()', () => {
- it('should return rate for converting the selected token to fiat', () => {
- assert.equal(
- getSelectedTokenToFiatRate(mockState),
- 2401.76400654
- )
- })
- })
-
- describe('getSendAmount()', () => {
- it('should return the send.amount', () => {
- assert.equal(
- getSendAmount(mockState),
- '0x080'
- )
- })
- })
-
- describe('getSendEditingTransactionId()', () => {
- it('should return the send.editingTransactionId', () => {
- assert.equal(
- getSendEditingTransactionId(mockState),
- 97531
- )
- })
- })
-
- describe('getSendErrors()', () => {
- it('should return the send.errors', () => {
- assert.deepEqual(
- getSendErrors(mockState),
- { someError: null }
- )
- })
- })
-
- describe('getSendHexDataFeatureFlagState()', () => {
- it('should return the sendHexData feature flag state', () => {
- assert.deepEqual(
- getSendHexDataFeatureFlagState(mockState),
- true
- )
- })
- })
-
- describe('getSendFrom()', () => {
- it('should return the send.from', () => {
- assert.deepEqual(
- getSendFrom(mockState),
- {
- address: '0xabcdefg',
- balance: '0x5f4e3d2c1',
- }
- )
- })
- })
-
- describe('getSendFromBalance()', () => {
- it('should get the send.from balance if it exists', () => {
- assert.equal(
- getSendFromBalance(mockState),
- '0x5f4e3d2c1'
- )
- })
-
- it('should get the selected account balance if the send.from does not exist', () => {
- const editedMockState = {
- metamask: Object.assign({}, mockState.metamask, {
- send: {
- from: null,
- },
- }),
- }
- assert.equal(
- getSendFromBalance(editedMockState),
- '0x0'
- )
- })
- })
-
- describe('getSendFromObject()', () => {
- it('should return send.from if it exists', () => {
- assert.deepEqual(
- getSendFromObject(mockState),
- {
- address: '0xabcdefg',
- balance: '0x5f4e3d2c1',
- }
- )
- })
-
- it('should return the current account with send ether info if send.from does not exist', () => {
- const editedMockState = {
- metamask: Object.assign({}, mockState.metamask, {
- send: {
- from: null,
- },
- }),
- }
- assert.deepEqual(
- getSendFromObject(editedMockState),
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- }
- )
- })
- })
-
- describe('getSendMaxModeState()', () => {
- it('should return send.maxModeOn', () => {
- assert.equal(
- getSendMaxModeState(mockState),
- false
- )
- })
- })
-
- describe('getSendTo()', () => {
- it('should return send.to', () => {
- assert.equal(
- getSendTo(mockState),
- '0x987fedabc'
- )
- })
- })
-
- describe('getSendToAccounts()', () => {
- it('should return an array including all the users accounts and the address book', () => {
- assert.deepEqual(
- getSendToAccounts(mockState),
- [
- {
- code: '0x',
- balance: '0x47c9d71831c76efe',
- nonce: '0x1b',
- address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- name: 'Send Account 1',
- },
- {
- code: '0x',
- balance: '0x37452b1315889f80',
- nonce: '0xa',
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- name: 'Send Account 2',
- },
- {
- code: '0x',
- balance: '0x30c9d71831c76efe',
- nonce: '0x1c',
- address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- name: 'Send Account 3',
- },
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- },
- {
- address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
- name: 'Address Book Account 1',
- },
- ]
- )
- })
- })
-
- describe('getTokenBalance()', () => {
- it('should', () => {
- assert.equal(
- getTokenBalance(mockState),
- 3434
- )
- })
- })
-
- describe('getTokenExchangeRate()', () => {
- it('should return the passed tokens exchange rates', () => {
- assert.equal(
- getTokenExchangeRate(mockState, 'GHI'),
- 31.01
- )
- })
- })
-
- describe('getUnapprovedTxs()', () => {
- it('should return the unapproved txs', () => {
- assert.deepEqual(
- getUnapprovedTxs(mockState),
- {
- 4768706228115573: {
- id: 4768706228115573,
- time: 1487363153561,
- status: 'unapproved',
- gasMultiplier: 1,
- metamaskNetworkId: '3',
- txParams: {
- from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
- value: '0xde0b6b3a7640000',
- metamaskId: 4768706228115573,
- metamaskNetworkId: '3',
- gas: '0x5209',
- },
- gasLimitSpecified: false,
- estimatedGas: '0x5209',
- txFee: '17e0186e60800',
- txValue: 'de0b6b3a7640000',
- maxCost: 'de234b52e4a0800',
- gasPrice: '4a817c800',
- },
- }
- )
- })
- })
-
- describe('transactionsSelector()', () => {
- it('should return the selected addresses selected token transactions', () => {
- assert.deepEqual(
- transactionsSelector(mockState),
- [
- {
- id: 'mockTokenTx1',
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- time: 1700000000000,
- },
- {
- id: 'mockTokenTx3',
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- time: 1500000000000,
- },
- ]
- )
- })
-
- it('should return all transactions if no token is selected', () => {
- const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
- const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
- assert.deepEqual(
- transactionsSelector(modifiedState),
- [
- {
- id: 'mockTokenTx1',
- time: 1700000000000,
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- },
- {
- id: 'unapprovedMessage1',
- time: 1650000000000,
- },
- {
- id: 'mockTokenTx2',
- time: 1600000000000,
- txParams: {
- to: '0xafaketokenaddress',
- },
- },
- {
- id: 'unapprovedMessage2',
- time: 1550000000000,
- },
- {
- id: 'mockTokenTx3',
- time: 1500000000000,
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- },
- {
- id: 'unapprovedMessage3',
- time: 1450000000000,
- },
- {
- id: 'mockEthTx1',
- time: 1400000000000,
- txParams: {
- to: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- },
- ]
- )
- })
-
- it('should return shapeshift transactions if current network is 1', () => {
- const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false, network: '1' })
- const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
- assert.deepEqual(
- transactionsSelector(modifiedState),
- [
- {
- id: 'mockTokenTx1',
- time: 1700000000000,
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- },
- { id: 'shapeShiftTx1', 'time': 1675000000000 },
- {
- id: 'unapprovedMessage1',
- time: 1650000000000,
- },
- {
- id: 'mockTokenTx2',
- time: 1600000000000,
- txParams: {
- to: '0xafaketokenaddress',
- },
- },
- { id: 'shapeShiftTx2', 'time': 1575000000000 },
- {
- id: 'unapprovedMessage2',
- time: 1550000000000,
- },
- {
- id: 'mockTokenTx3',
- time: 1500000000000,
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- },
- },
- { id: 'shapeShiftTx3', 'time': 1475000000000 },
- {
- id: 'unapprovedMessage3',
- time: 1450000000000,
- },
- {
- id: 'mockEthTx1',
- time: 1400000000000,
- txParams: {
- to: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- },
- ]
- )
- })
- })
-
-})
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 b72d87eee..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: 'dec',
- },
- {
- value: 'calc:1610',
- fromNumericBase: 'dec',
- },
- ]
- )
-
- 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',
- }
-
- 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: undefined, data, from: baseExpectedCall.from, gas: baseExpectedCall.gas},
- )
- assert.equal(result, '0xabc16')
- })
-
- it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
- assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
- const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' }))
- 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/index.scss b/ui/app/components/sender-to-recipient/index.scss
deleted file mode 100644
index 0ab0413be..000000000
--- a/ui/app/components/sender-to-recipient/index.scss
+++ /dev/null
@@ -1,120 +0,0 @@
-.sender-to-recipient {
- &--default {
- width: 100%;
- display: flex;
- flex-direction: row;
- justify-content: center;
- border-bottom: 1px solid $geyser;
- position: relative;
- flex: 0 0 auto;
- height: 42px;
-
- .sender-to-recipient {
- &__tooltip-wrapper {
- min-width: 0;
- }
-
- &__tooltip-container {
- max-width: 100%;
- }
-
- &__party {
- display: flex;
- flex-direction: row;
- align-items: center;
- flex: 1;
- padding: 0 16px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &--sender {
- padding-right: 30px;
- cursor: pointer;
- }
-
- &--recipient {
- padding-left: 30px;
- border-left: 1px solid $geyser;
-
- &-with-address {
- cursor: pointer;
- }
- }
- }
-
- &__arrow-container {
- position: absolute;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- &__arrow-circle {
- background: $white;
- padding: 5px;
- border: 1px solid $geyser;
- border-radius: 20px;
- height: 32px;
- width: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
-
- &__name {
- padding-left: 14px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: .875rem;
- }
- }
- }
-
- &--cards {
- width: 100%;
- display: flex;
- flex-direction: row;
- justify-content: center;
- position: relative;
- flex: 0 0 auto;
-
- .sender-to-recipient {
- &__party {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- flex: 1;
- border-radius: 4px;
- box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
- padding: 6px;
- background: $white;
- cursor: pointer;
- min-width: 0;
- color: $dusty-gray;
- }
-
- &__tooltip-wrapper {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- &__name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: .5rem;
- }
-
- &__arrow-container {
- display: flex;
- justify-content: center;
- align-items: center;
- }
- }
- }
-}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
deleted file mode 100644
index e71bd7406..000000000
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ /dev/null
@@ -1,177 +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 } from './sender-to-recipient.constants'
-import { checksumAddress } from '../../util'
-
-const variantHash = {
- [DEFAULT_VARIANT]: 'sender-to-recipient--default',
- [CARDS_VARIANT]: 'sender-to-recipient--cards',
-}
-
-export default class SenderToRecipient extends PureComponent {
- static propTypes = {
- senderName: PropTypes.string,
- senderAddress: PropTypes.string,
- recipientName: PropTypes.string,
- recipientAddress: PropTypes.string,
- t: PropTypes.func,
- variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]),
- addressOnly: PropTypes.bool,
- assetImage: PropTypes.string,
- }
-
- static defaultProps = {
- variant: DEFAULT_VARIANT,
- }
-
- static contextTypes = {
- 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 } = 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)
- }}
- >
- { 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 === CARDS_VARIANT
- ? (
- <div className="sender-to-recipient__arrow-container">
- <img
- height={20}
- src="./images/caret-right.svg"
- />
- </div>
- ) : (
- <div className="sender-to-recipient__arrow-container">
- <div className="sender-to-recipient__arrow-circle">
- <img
- height={15}
- width={15}
- src="./images/arrow-right.svg"
- />
- </div>
- </div>
- )
- }
-
- render () {
- const { senderAddress, recipientAddress, variant } = this.props
- const checksummedSenderAddress = checksumAddress(senderAddress)
-
- return (
- <div className={classnames(variantHash[variant])}>
- <div
- className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
- onClick={() => {
- this.setState({ senderAddressCopied: true })
- copyToClipboard(checksummedSenderAddress)
- }}
- >
- { this.renderSenderIdenticon() }
- { this.renderSenderAddress() }
- </div>
- { this.renderArrow() }
- {
- recipientAddress
- ? this.renderRecipientWithAddress()
- : this.renderRecipientWithoutAddress()
- }
- </div>
- )
- }
-}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
deleted file mode 100644
index 166228932..000000000
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// Component design variants
-export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
-export const CARDS_VARIANT = 'CARDS_VARIANT'
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
deleted file mode 100644
index a842bcc8b..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-npm')
-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 0461b615a..000000000
--- a/ui/app/components/shift-list-item.js
+++ /dev/null
@@ -1,209 +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 vreme = new (require('vreme'))()
-const explorerLink = require('etherscan-link').createExplorerLink
-const actions = require('../actions')
-const addressSummary = require('../util').addressSummary
-
-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(),
- ])
-}
-
-function formatDate (date) {
- return vreme.format(new Date(date), 'March 16 2014 14:30')
-}
-
-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 5ab0664df..000000000
--- a/ui/app/components/sidebars/index.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-.sidebar-right-enter {
- transition: transform 300ms ease-in-out;
- transform: translateX(-100%);
-}
-
-.sidebar-right-enter.sidebar-right-enter-active {
- transition: transform 300ms ease-in-out;
- transform: translateX(0%);
-}
-
-.sidebar-right-leave {
- transition: transform 200ms ease-out;
- transform: translateX(0%);
-}
-
-.sidebar-right-leave.sidebar-right-leave-active {
- transition: transform 200ms ease-out;
- transform: translateX(-100%);
-}
-
-.sidebar-left-enter {
- transition: transform 300ms ease-in-out;
- transform: translateX(100%);
-}
-
-.sidebar-left-enter.sidebar-left-enter-active {
- transition: transform 300ms ease-in-out;
- transform: translateX(0%);
-}
-
-.sidebar-left-leave {
- transition: transform 200ms ease-out;
- transform: translateX(0%);
-}
-
-.sidebar-left-leave.sidebar-left-leave-active {
- transition: transform 200ms ease-out;
- transform: translateX(100%);
-}
-
-.sidebar-left {
- flex: 1 0 230px;
- background: rgb(250, 250, 250);
- z-index: $sidebar-z-index;
- position: fixed;
- left: 15%;
- right: 0;
- bottom: 0;
- opacity: 1;
- visibility: visible;
- will-change: transform;
- overflow-y: auto;
- box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px;
- width: 85%;
- height: 100%;
-
- @media screen and (min-width: 769px) {
- width: 408px;
- left: calc(100% - 408px);
- }
-}
-
-.sidebar-overlay {
- z-index: $sidebar-overlay-z-index;
- position: fixed;
- height: 100%;
- width: 100%;
- left: 0;
- right: 0;
- bottom: 0;
- opacity: 1;
- visibility: visible;
- background-color: rgba(0, 0, 0, .3);
-} \ No newline at end of file
diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js
deleted file mode 100644
index 57cdd7111..000000000
--- a/ui/app/components/sidebars/sidebar.component.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
-import WalletView from '../wallet-view'
-import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
-
-export default class Sidebar extends Component {
-
- static propTypes = {
- sidebarOpen: PropTypes.bool,
- hideSidebar: PropTypes.func,
- transitionName: PropTypes.string,
- type: PropTypes.string,
- };
-
- renderOverlay () {
- return <div className="sidebar-overlay" onClick={() => this.props.hideSidebar()} />
- }
-
- renderSidebarContent () {
- const { type } = this.props
-
- switch (type) {
- case WALLET_VIEW_SIDEBAR:
- return <WalletView responsiveDisplayClassname={'sidebar-right' } />
- default:
- return null
- }
-
- }
-
- render () {
- const { transitionName, sidebarOpen } = this.props
-
- return (
- <div>
- <ReactCSSTransitionGroup
- transitionName={transitionName}
- transitionEnterTimeout={300}
- transitionLeaveTimeout={200}
- >
- { sidebarOpen ? this.renderSidebarContent() : null }
- </ReactCSSTransitionGroup>
- { sidebarOpen ? this.renderOverlay() : null }
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/sidebars/tests/sidebars-component.test.js
deleted file mode 100644
index e2d77518a..000000000
--- a/ui/app/components/sidebars/tests/sidebars-component.test.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
-import Sidebar from '../sidebar.component.js'
-
-import WalletView from '../../wallet-view'
-
-const propsMethodSpies = {
- hideSidebar: sinon.spy(),
-}
-
-describe('Sidebar Component', function () {
- let wrapper
-
- beforeEach(() => {
- wrapper = shallow(<Sidebar
- sidebarOpen={false}
- hideSidebar={propsMethodSpies.hideSidebar}
- transitionName={'someTransition'}
- type={'wallet-view'}
- />)
- })
-
- afterEach(() => {
- propsMethodSpies.hideSidebar.resetHistory()
- })
-
- describe('renderOverlay', () => {
- let renderOverlay
-
- beforeEach(() => {
- renderOverlay = shallow(wrapper.instance().renderOverlay())
- })
-
- it('should render a overlay element', () => {
- assert(renderOverlay.hasClass('sidebar-overlay'))
- })
-
- it('should pass the correct onClick function to the element', () => {
- assert.equal(propsMethodSpies.hideSidebar.callCount, 0)
- renderOverlay.props().onClick()
- assert.equal(propsMethodSpies.hideSidebar.callCount, 1)
- })
- })
-
- describe('renderSidebarContent', () => {
- let renderSidebarContent
-
- beforeEach(() => {
- wrapper.setProps({ type: 'wallet-view' })
- renderSidebarContent = wrapper.instance().renderSidebarContent()
- })
-
- it('should render sidebar content with the correct props', () => {
- wrapper.setProps({ type: 'wallet-view' })
- renderSidebarContent = wrapper.instance().renderSidebarContent()
- assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
- })
-
- it('should not render with an unrecognized type', () => {
- wrapper.setProps({ type: 'foobar' })
- renderSidebarContent = wrapper.instance().renderSidebarContent()
- assert.equal(renderSidebarContent, undefined)
- })
- })
-
- describe('render', () => {
- it('should render a div with one child', () => {
- assert(wrapper.is('div'))
- assert.equal(wrapper.children().length, 1)
- })
-
- it('should render the ReactCSSTransitionGroup without any children', () => {
- assert(wrapper.children().at(0).is(ReactCSSTransitionGroup))
- assert.equal(wrapper.children().at(0).children().length, 0)
- })
-
- it('should render sidebar content and the overlay if sidebarOpen is true', () => {
- wrapper.setProps({ sidebarOpen: true })
- assert.equal(wrapper.children().length, 2)
- assert(wrapper.children().at(1).hasClass('sidebar-overlay'))
- assert.equal(wrapper.children().at(0).children().length, 1)
- assert(wrapper.children().at(0).children().at(0).is(WalletView))
- })
- })
-})
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
deleted file mode 100644
index 85af3b00b..000000000
--- a/ui/app/components/signature-request.js
+++ /dev/null
@@ -1,307 +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')
-
-const AccountDropdownMini = require('./dropdowns/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,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(SignatureRequest)
-
-
-inherits(SignatureRequest, Component)
-function SignatureRequest (props) {
- Component.call(this)
-
- this.state = {
- selectedAccount: props.selectedAccount,
- accountDropdownOpen: false,
- }
-}
-
-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,
- accountDropdownOpen,
- } = 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,
- onSelect: selectedAccount => this.setState({ selectedAccount }),
- dropdownOpen: accountDropdownOpen,
- openDropdown: () => this.setState({ accountDropdownOpen: true }),
- closeDropdown: () => this.setState({ accountDropdownOpen: false }),
- }),
-
- ])
-}
-
-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.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.props.clearConfirmTransaction()
- this.props.history.push(DEFAULT_ROUTE)
- })
- },
- }, this.context.t('cancel')),
- h(Button, {
- type: 'primary',
- large: true,
- onClick: event => {
- sign(event).then(() => {
- 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.component.js b/ui/app/components/token-balance/token-balance.component.js
deleted file mode 100644
index 2b4f73980..000000000
--- a/ui/app/components/token-balance/token-balance.component.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-
-export default class TokenBalance extends PureComponent {
- static propTypes = {
- string: PropTypes.string,
- symbol: PropTypes.string,
- error: PropTypes.string,
- className: PropTypes.string,
- withSymbol: PropTypes.bool,
- }
-
- render () {
- const { className, string, withSymbol, symbol } = this.props
-
- return (
- <div className={classnames('hide-text-overflow', className)}>
- { string + (withSymbol ? ` ${symbol}` : '') }
- </div>
- )
- }
-}
diff --git a/ui/app/components/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 75ba347fa..000000000
--- a/ui/app/components/token-cell.js
+++ /dev/null
@@ -1,165 +0,0 @@
-const Component = require('react').Component
-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.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)
- 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 4bb09a4b6..000000000
--- a/ui/app/components/token-currency-display/token-currency-display.component.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import CurrencyDisplay from '../currency-display/currency-display.component'
-import { getTokenData } from '../../helpers/transactions.util'
-import { getTokenValue, calcTokenAmount } from '../../token-util'
-
-export default class TokenCurrencyDisplay extends PureComponent {
- static propTypes = {
- transactionData: PropTypes.string,
- token: PropTypes.object,
- }
-
- state = {
- displayValue: '',
- }
-
- componentDidMount () {
- this.setDisplayValue()
- }
-
- componentDidUpdate (prevProps) {
- const { transactionData } = this.props
- const { transactionData: prevTransactionData } = prevProps
-
- if (transactionData !== prevTransactionData) {
- this.setDisplayValue()
- }
- }
-
- setDisplayValue () {
- const { transactionData: data, token } = this.props
- const { decimals = '', symbol = '' } = token
- const tokenData = getTokenData(data)
-
- let displayValue
-
- if (tokenData.params && tokenData.params.length) {
- const tokenValue = getTokenValue(tokenData.params)
- const tokenAmount = calcTokenAmount(tokenValue, decimals)
- displayValue = `${tokenAmount} ${symbol}`
- }
-
- this.setState({ displayValue })
- }
-
- render () {
- return (
- <CurrencyDisplay
- {...this.props}
- displayValue={this.state.displayValue}
- />
- )
- }
-}
diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js
deleted file mode 100644
index 2dacb9bc4..000000000
--- a/ui/app/components/token-input/tests/token-input.component.test.js
+++ /dev/null
@@ -1,308 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import assert from 'assert'
-import { shallow, mount } from 'enzyme'
-import sinon from 'sinon'
-import { Provider } from 'react-redux'
-import configureMockStore from 'redux-mock-store'
-import TokenInput from '../token-input.component'
-import UnitInput from '../../unit-input'
-import CurrencyDisplay from '../../currency-display'
-
-describe('TokenInput Component', () => {
- const t = key => `translate ${key}`
-
- describe('rendering', () => {
- it('should render properly without a token', () => {
- const wrapper = shallow(
- <TokenInput />,
- { context: { t } }
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find(UnitInput).length, 1)
- })
-
- it('should render properly with a token', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- />
- </Provider>,
- { context: { t },
- childContextTypes: {
- t: PropTypes.func,
- },
- },
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
- assert.equal(wrapper.find('.currency-input__conversion-component').length, 1)
- assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable')
- })
-
- it('should render properly with a token and selectedTokenExchangeRate', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- />
- </Provider>,
- { context: { t },
- childContextTypes: {
- t: PropTypes.func,
- },
- },
- )
-
- assert.ok(wrapper)
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
- assert.equal(wrapper.find(CurrencyDisplay).length, 1)
- })
-
- it('should render properly with a token value for ETH', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- value="2710"
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
- assert.equal(tokenInputInstance.state.decimalValue, 1)
- assert.equal(tokenInputInstance.state.hexValue, '2710')
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
- assert.equal(wrapper.find('.unit-input__input').props().value, '1')
- assert.equal(wrapper.find('.currency-display-component').text(), '2ETH')
- })
-
- it('should render properly with a token value for fiat', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
-
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- value="2710"
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- showFiat
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
- assert.equal(tokenInputInstance.state.decimalValue, 1)
- assert.equal(tokenInputInstance.state.hexValue, '2710')
- assert.equal(wrapper.find('.unit-input__suffix').length, 1)
- assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
- assert.equal(wrapper.find('.unit-input__input').props().value, '1')
- assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD')
- })
- })
-
- describe('handling actions', () => {
- const handleChangeSpy = sinon.spy()
- const handleBlurSpy = sinon.spy()
-
- afterEach(() => {
- handleChangeSpy.resetHistory()
- handleBlurSpy.resetHistory()
- })
-
- it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- assert.equal(handleChangeSpy.callCount, 0)
- assert.equal(handleBlurSpy.callCount, 0)
-
- const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
- assert.equal(tokenInputInstance.state.decimalValue, 0)
- assert.equal(tokenInputInstance.state.hexValue, undefined)
- assert.equal(wrapper.find('.currency-display-component').text(), '0ETH')
- const input = wrapper.find('input')
- assert.equal(input.props().value, 0)
-
- input.simulate('change', { target: { value: 1 } })
- assert.equal(handleChangeSpy.callCount, 1)
- assert.ok(handleChangeSpy.calledWith('2710'))
- assert.equal(wrapper.find('.currency-display-component').text(), '2ETH')
- assert.equal(tokenInputInstance.state.decimalValue, 1)
- assert.equal(tokenInputInstance.state.hexValue, '2710')
-
- assert.equal(handleBlurSpy.callCount, 0)
- input.simulate('blur')
- assert.equal(handleBlurSpy.callCount, 1)
- assert.ok(handleBlurSpy.calledWith('2710'))
- })
-
- it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = mount(
- <Provider store={store}>
- <TokenInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- showFiat
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- assert.equal(handleChangeSpy.callCount, 0)
- assert.equal(handleBlurSpy.callCount, 0)
-
- const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
- assert.equal(tokenInputInstance.state.decimalValue, 0)
- assert.equal(tokenInputInstance.state.hexValue, undefined)
- assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
- const input = wrapper.find('input')
- assert.equal(input.props().value, 0)
-
- input.simulate('change', { target: { value: 1 } })
- assert.equal(handleChangeSpy.callCount, 1)
- assert.ok(handleChangeSpy.calledWith('2710'))
- assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD')
- assert.equal(tokenInputInstance.state.decimalValue, 1)
- assert.equal(tokenInputInstance.state.hexValue, '2710')
-
- assert.equal(handleBlurSpy.callCount, 0)
- input.simulate('blur')
- assert.equal(handleBlurSpy.callCount, 1)
- assert.ok(handleBlurSpy.calledWith('2710'))
- })
-
- it('should change the state and pass in a new decimalValue when props.value changes', () => {
- const mockStore = {
- metamask: {
- currentCurrency: 'usd',
- conversionRate: 231.06,
- },
- }
- const store = configureMockStore()(mockStore)
- const wrapper = shallow(
- <Provider store={store}>
- <TokenInput
- onChange={handleChangeSpy}
- onBlur={handleBlurSpy}
- selectedToken={{
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- }}
- suffix="ABC"
- selectedTokenExchangeRate={2}
- showFiat
- />
- </Provider>
- )
-
- assert.ok(wrapper)
- const tokenInputInstance = wrapper.find(TokenInput).dive()
- assert.equal(tokenInputInstance.state('decimalValue'), 0)
- assert.equal(tokenInputInstance.state('hexValue'), undefined)
- assert.equal(tokenInputInstance.find(UnitInput).props().value, 0)
-
- tokenInputInstance.setProps({ value: '2710' })
- tokenInputInstance.update()
- assert.equal(tokenInputInstance.state('decimalValue'), 1)
- assert.equal(tokenInputInstance.state('hexValue'), '2710')
- assert.equal(tokenInputInstance.find(UnitInput).props().value, 1)
- })
- })
-})
diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js
deleted file mode 100644
index d73bc9a94..000000000
--- a/ui/app/components/token-input/tests/token-input.container.test.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-let mapStateToProps, mergeProps
-
-proxyquire('../token-input.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mergeProps = mp
- return () => ({})
- },
- },
-})
-
-describe('TokenInput container', () => {
- describe('mapStateToProps()', () => {
- it('should return the correct props when send is empty', () => {
- const mockState = {
- metamask: {
- currentCurrency: 'usd',
- tokens: [
- {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- ],
- selectedTokenAddress: '0x1',
- contractExchangeRates: {},
- send: {},
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- currentCurrency: 'usd',
- selectedToken: {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- selectedTokenExchangeRate: 0,
- })
- })
-
- it('should return the correct props when selectedTokenAddress is not found and send is populated', () => {
- const mockState = {
- metamask: {
- currentCurrency: 'usd',
- tokens: [
- {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- ],
- selectedTokenAddress: '0x2',
- contractExchangeRates: {},
- send: {
- token: { address: 'test' },
- },
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- currentCurrency: 'usd',
- selectedToken: {
- address: 'test',
- },
- selectedTokenExchangeRate: 0,
- })
- })
-
- it('should return the correct props when contractExchangeRates is populated', () => {
- const mockState = {
- metamask: {
- currentCurrency: 'usd',
- tokens: [
- {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- ],
- selectedTokenAddress: '0x1',
- contractExchangeRates: {
- '0x1': 5,
- },
- send: {},
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- currentCurrency: 'usd',
- selectedToken: {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- selectedTokenExchangeRate: 5,
- })
- })
- })
-
- describe('mergeProps()', () => {
- it('should return the correct props', () => {
- const mockStateProps = {
- currentCurrency: 'usd',
- selectedToken: {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- selectedTokenExchangeRate: 5,
- }
-
- assert.deepEqual(mergeProps(mockStateProps, {}, {}), {
- currentCurrency: 'usd',
- selectedToken: {
- address: '0x1',
- decimals: '4',
- symbol: 'ABC',
- },
- selectedTokenExchangeRate: 5,
- suffix: 'ABC',
- })
- })
- })
-})
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 d1388945b..000000000
--- a/ui/app/components/token-input/token-input.component.js
+++ /dev/null
@@ -1,136 +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,
- selectedToken: PropTypes.object,
- selectedTokenExchangeRate: PropTypes.number,
- }
-
- constructor (props) {
- super(props)
-
- const { value: hexValue } = props
- const decimalValue = hexValue ? this.getDecimalValue(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.getDecimalValue(this.props)
- this.setState({ hexValue: propsHexValue, decimalValue })
- }
- }
-
- getDecimalValue (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) || 0
- }
-
- 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 } = this.props
- const { decimalValue } = this.state
- let currency, numberOfDecimals
-
- 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 ec233b1b8..000000000
--- a/ui/app/components/token-input/token-input.container.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux'
-import TokenInput from './token-input.component'
-import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors'
-
-const mapStateToProps = state => {
- const { metamask: { currentCurrency } } = state
-
- return {
- currentCurrency,
- selectedToken: getSelectedToken(state),
- selectedTokenExchangeRate: getSelectedTokenExchangeRate(state),
- }
-}
-
-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 6a88f30bf..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 (nextProps) {
- const {
- network: oldNet,
- userAddress: oldAddress,
- tokens,
- } = this.props
- const {
- network: newNet,
- userAddress: newAddress,
- tokens: newTokens,
- } = nextProps
-
- 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/tooltip-v2.js b/ui/app/components/tooltip-v2.js
deleted file mode 100644
index 054782203..000000000
--- a/ui/app/components/tooltip-v2.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import PropTypes from 'prop-types'
-import React, {PureComponent} from 'react'
-import {Tooltip as ReactTippy} from 'react-tippy'
-
-export default class Tooltip extends PureComponent {
- static defaultProps = {
- arrow: true,
- children: null,
- containerClassName: '',
- hideOnClick: false,
- onHidden: null,
- position: 'left',
- size: 'small',
- title: null,
- trigger: 'mouseenter',
- wrapperClassName: '',
- }
-
- static propTypes = {
- arrow: PropTypes.bool,
- children: PropTypes.node,
- containerClassName: PropTypes.string,
- onHidden: PropTypes.func,
- position: PropTypes.oneOf([
- 'top',
- 'right',
- 'bottom',
- 'left',
- ]),
- size: PropTypes.oneOf([
- 'small', 'regular', 'big',
- ]),
- title: PropTypes.string,
- trigger: PropTypes.any,
- wrapperClassName: PropTypes.string,
- }
-
- render () {
- const {arrow, children, containerClassName, position, size, title, trigger, onHidden, wrapperClassName } = this.props
-
- if (!title) {
- return (
- <div className={wrapperClassName}>
- {children}
- </div>
- )
- }
-
- return (
- <div className={wrapperClassName}>
- <ReactTippy
- className={containerClassName}
- title={title}
- position={position}
- trigger={trigger}
- hideOnClick={false}
- size={size}
- arrow={arrow}
- onHidden={onHidden}
- >
- {children}
- </ReactTippy>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-action/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/index.scss b/ui/app/components/transaction-activity-log/index.scss
deleted file mode 100644
index 27f3006b3..000000000
--- a/ui/app/components/transaction-activity-log/index.scss
+++ /dev/null
@@ -1,70 +0,0 @@
-.transaction-activity-log {
- &__card {
- background: $white;
- height: 100%;
- }
-
- &__activities-container {
- padding-top: 8px;
- }
-
- &__activity {
- padding: 4px 0;
- display: flex;
- flex-direction: row;
- align-items: center;
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- width: 6px;
- border-right: 1px solid $scorpion;
- }
-
- &:first-child::after {
- height: 50%;
- top: 50%;
- }
-
- &:last-child::after {
- height: 50%;
- }
-
- &:first-child:last-child::after {
- display: none;
- }
- }
-
- &__activity-icon {
- width: 13px;
- height: 13px;
- margin-right: 6px;
- border-radius: 50%;
- background: $scorpion;
- flex: 0 0 auto;
- }
-
- &__activity-text {
- color: $scorpion;
- font-size: .75rem;
-
- @media screen and (min-width: $break-large) {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- &__value {
- display: inline;
- font-weight: 500;
- }
-
- b {
- font-weight: 500;
- }
-}
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
deleted file mode 100644
index 8687dbbc7..000000000
--- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import TransactionActivityLog from '../transaction-activity-log.component'
-import Card from '../../card'
-
-describe('TransactionActivityLog Component', () => {
- it('should render properly', () => {
- const transaction = {
- history: [],
- id: 1,
- status: 'confirmed',
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- }
-
- const wrapper = shallow(
- <TransactionActivityLog
- transaction={transaction}
- className="test-class"
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-activity-log'))
- assert.ok(wrapper.hasClass('test-class'))
- assert.equal(wrapper.find(Card).length, 1)
- })
-})
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
deleted file mode 100644
index 586500408..000000000
--- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import assert from 'assert'
-import { getActivities } from '../transaction-activity-log.util'
-
-describe('getActivities', () => {
- it('should return no activities for an empty history', () => {
- const transaction = {
- history: [],
- id: 1,
- status: 'confirmed',
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- }
-
- assert.deepEqual(getActivities(transaction), [])
- })
-
- it('should return activities for a transaction\'s history', () => {
- const transaction = {
- history: [
- {
- id: 5559712943815343,
- loadingDefaults: true,
- metamaskNetworkId: '3',
- status: 'unapproved',
- time: 1535507561452,
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- },
- [
- {
- op: 'replace',
- path: '/loadingDefaults',
- timestamp: 1535507561515,
- value: false,
- },
- {
- op: 'add',
- path: '/gasPriceSpecified',
- value: true,
- },
- {
- op: 'add',
- path: '/gasLimitSpecified',
- value: true,
- },
- {
- op: 'add',
- path: '/estimatedGas',
- value: '0x5208',
- },
- ],
- [
- {
- note: '#newUnapprovedTransaction - adding the origin',
- op: 'add',
- path: '/origin',
- timestamp: 1535507561516,
- value: 'MetaMask',
- },
- [],
- ],
- [
- {
- note: 'confTx: user approved transaction',
- op: 'replace',
- path: '/txParams/gasPrice',
- timestamp: 1535664571504,
- value: '0x77359400',
- },
- ],
- [
- {
- note: 'txStateManager: setting status to approved',
- op: 'replace',
- path: '/status',
- timestamp: 1535507564302,
- value: 'approved',
- },
- ],
- [
- {
- note: 'transactions#approveTransaction',
- op: 'add',
- path: '/txParams/nonce',
- timestamp: 1535507564439,
- value: '0xa4',
- },
- {
- op: 'add',
- path: '/nonceDetails',
- value: {
- local: {},
- network: {},
- params: {},
- },
- },
- ],
- [
- {
- note: 'transactions#publishTransaction',
- op: 'replace',
- path: '/status',
- timestamp: 1535507564518,
- value: 'signed',
- },
- {
- op: 'add',
- path: '/rawTx',
- value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf',
- },
- ],
- [],
- [
- {
- note: 'transactions#setTxHash',
- op: 'add',
- path: '/hash',
- timestamp: 1535507564658,
- value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7',
- },
- ],
- [
- {
- note: 'txStateManager - add submitted time stamp',
- op: 'add',
- path: '/submittedTime',
- timestamp: 1535507564660,
- value: 1535507564660,
- },
- ],
- [
- {
- note: 'txStateManager: setting status to submitted',
- op: 'replace',
- path: '/status',
- timestamp: 1535507564665,
- value: 'submitted',
- },
- ],
- [
- {
- note: 'transactions/pending-tx-tracker#event: tx:block-update',
- op: 'add',
- path: '/firstRetryBlockNumber',
- timestamp: 1535507575476,
- value: '0x3bf624',
- },
- ],
- [
- {
- note: 'txStateManager: setting status to confirmed',
- op: 'replace',
- path: '/status',
- timestamp: 1535507615993,
- value: 'confirmed',
- },
- ],
- ],
- id: 1,
- status: 'confirmed',
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- }
-
- const expectedResult = [
- {
- 'eventKey': 'transactionCreated',
- 'timestamp': 1535507561452,
- 'value': '0x2386f26fc10000',
- },
- {
- 'eventKey': 'transactionUpdatedGas',
- 'timestamp': 1535664571504,
- 'value': '0x77359400',
- },
- {
- 'eventKey': 'transactionSubmitted',
- 'timestamp': 1535507564665,
- 'value': undefined,
- },
- {
- 'eventKey': 'transactionConfirmed',
- 'timestamp': 1535507615993,
- 'value': undefined,
- },
- ]
-
- assert.deepEqual(getActivities(transaction), expectedResult)
- })
-})
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
deleted file mode 100644
index 58d932a0f..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import { getActivities } from './transaction-activity-log.util'
-import Card from '../card'
-import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
-import { formatDate } from '../../util'
-
-export default class TransactionActivityLog extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- transaction: PropTypes.object,
- className: PropTypes.string,
- conversionRate: PropTypes.number,
- nativeCurrency: PropTypes.string,
- }
-
- state = {
- activities: [],
- }
-
- componentDidMount () {
- this.setActivites()
- }
-
- componentDidUpdate (prevProps) {
- const {
- transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {},
- } = prevProps
- const {
- transaction: { history = [], txReceipt: { status } = {} } = {},
- } = this.props
-
- if (prevHistory.length !== history.length || prevStatus !== status) {
- this.setActivites()
- }
- }
-
- setActivites () {
- const activities = getActivities(this.props.transaction)
- this.setState({ activities })
- }
-
- renderActivity (activity, index) {
- const { conversionRate, nativeCurrency } = this.props
- const { eventKey, value, timestamp } = activity
- const ethValue = index === 0
- ? `${getValueFromWeiHex({
- value,
- fromCurrency: nativeCurrency,
- toCurrency: nativeCurrency,
- conversionRate,
- numberOfDecimals: 6,
- })} ${nativeCurrency}`
- : getEthConversionFromWeiHex({ value, fromCurrency: nativeCurrency, conversionRate })
- const formattedTimestamp = formatDate(timestamp)
- const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
-
- return (
- <div
- key={index}
- className="transaction-activity-log__activity"
- >
- <div className="transaction-activity-log__activity-icon" />
- <div
- className="transaction-activity-log__activity-text"
- title={activityText}
- >
- { activityText }
- </div>
- </div>
- )
- }
-
- render () {
- const { t } = this.context
- const { className } = this.props
- const { activities } = this.state
-
- return (
- <div className={classnames('transaction-activity-log', className)}>
- <Card
- title={t('activityLog')}
- className="transaction-activity-log__card"
- >
- <div className="transaction-activity-log__activities-container">
- { activities.map((activity, index) => this.renderActivity(activity, index)) }
- </div>
- </Card>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
deleted file mode 100644
index 622f77df1..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux'
-import TransactionActivityLog from './transaction-activity-log.component'
-import { conversionRateSelector, getNativeCurrency } from '../../selectors'
-
-const mapStateToProps = state => {
- return {
- conversionRate: conversionRateSelector(state),
- nativeCurrency: getNativeCurrency(state),
- }
-}
-
-export default connect(mapStateToProps)(TransactionActivityLog)
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
deleted file mode 100644
index 16597ae1a..000000000
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// path constants
-const STATUS_PATH = '/status'
-const GAS_PRICE_PATH = '/txParams/gasPrice'
-
-// status constants
-const UNAPPROVED_STATUS = 'unapproved'
-const SUBMITTED_STATUS = 'submitted'
-const CONFIRMED_STATUS = 'confirmed'
-const DROPPED_STATUS = 'dropped'
-
-// op constants
-const REPLACE_OP = 'replace'
-
-// event constants
-const TRANSACTION_CREATED_EVENT = 'transactionCreated'
-const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas'
-const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
-const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
-const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
-const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
-const TRANSACTION_ERRORED_EVENT = 'transactionErrored'
-
-const eventPathsHash = {
- [STATUS_PATH]: true,
- [GAS_PRICE_PATH]: true,
-}
-
-const statusHash = {
- [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
- [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
- [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
-}
-
-function eventCreator (eventKey, timestamp, value) {
- return {
- eventKey,
- timestamp,
- value,
- }
-}
-
-export function getActivities (transaction) {
- const { history = [], txReceipt: { status } = {} } = transaction
-
- const historyActivities = history.reduce((acc, base) => {
- // First history item should be transaction creation
- if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
- const { time, txParams: { value } = {} } = base
- return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
- // 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: {
- if (value in statusHash) {
- events.push(eventCreator(statusHash[value], timestamp))
- }
-
- break
- }
-
- case GAS_PRICE_PATH: {
- events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value))
- break
- }
-
- default: {
- events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp))
- }
- }
- }
- })
-
- return acc.concat(events)
- }
-
- return acc
- }, [])
-
- // 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(eventCreator(TRANSACTION_ERRORED_EVENT))
- : historyActivities
-}
diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/transaction-breakdown/index.scss
deleted file mode 100644
index 1bb108943..000000000
--- a/ui/app/components/transaction-breakdown/index.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-@import './transaction-breakdown-row/index';
-
-.transaction-breakdown {
- &__card {
- background: $white;
- height: 100%;
- }
-
- &__row-title {
- text-transform: capitalize;
- }
-
- &__value {
- text-align: end;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &--eth-total {
- font-weight: 500;
- }
- }
-}
diff --git a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
deleted file mode 100644
index d18cd420c..000000000
--- a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import assert from 'assert'
-import { shallow } from 'enzyme'
-import TransactionBreakdown from '../transaction-breakdown.component'
-import TransactionBreakdownRow from '../transaction-breakdown-row'
-import Card from '../../card'
-
-describe('TransactionBreakdown Component', () => {
- it('should render properly', () => {
- const transaction = {
- history: [],
- id: 1,
- status: 'confirmed',
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- }
-
- const wrapper = shallow(
- <TransactionBreakdown
- transaction={transaction}
- className="test-class"
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-breakdown'))
- assert.ok(wrapper.hasClass('test-class'))
- assert.equal(wrapper.find(Card).length, 1)
- assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4)
- })
-})
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/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 3a7647873..000000000
--- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import classnames from 'classnames'
-import TransactionBreakdownRow from './transaction-breakdown-row'
-import Card from '../card'
-import CurrencyDisplay from '../currency-display'
-import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
-import HexToDecimal from '../hex-to-decimal'
-import { GWEI, PRIMARY, SECONDARY } from '../../constants/common'
-import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
-import { sumHexes } from '../../helpers/transactions.util'
-
-export default class TransactionBreakdown extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- transaction: PropTypes.object,
- className: PropTypes.string,
- nativeCurrency: PropTypes.string.isRequired,
- }
-
- static defaultProps = {
- transaction: {},
- }
-
- render () {
- const { t } = this.context
- const { transaction, className, nativeCurrency } = this.props
- const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction
-
- const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas
-
- const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
- const totalInHex = sumHexes(hexGasTotal, value)
-
- return (
- <div className={classnames('transaction-breakdown', className)}>
- <Card
- title={t('transaction')}
- className="transaction-breakdown__card"
- >
- <TransactionBreakdownRow title={t('amount')}>
- <UserPreferencedCurrencyDisplay
- className="transaction-breakdown__value"
- type={PRIMARY}
- value={value}
- />
- </TransactionBreakdownRow>
- <TransactionBreakdownRow
- title={`${t('gasLimit')} (${t('units')})`}
- className="transaction-breakdown__row-title"
- >
- <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')}>
- <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}
- />
- <UserPreferencedCurrencyDisplay
- className="transaction-breakdown__value"
- type={SECONDARY}
- value={totalInHex}
- />
- </div>
- </TransactionBreakdownRow>
- </Card>
- </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 ed2708e03..000000000
--- a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { connect } from 'react-redux'
-import TransactionBreakdown from './transaction-breakdown.component'
-import { getNativeCurrency } from '../../selectors'
-
-const mapStateToProps = (state) => {
- return {
- nativeCurrency: getNativeCurrency(state),
- }
-}
-
-export default connect(mapStateToProps)(TransactionBreakdown)
diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/transaction-list-item-details/index.scss
deleted file mode 100644
index 54cf834cc..000000000
--- a/ui/app/components/transaction-list-item-details/index.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-.transaction-list-item-details {
- &__header {
- margin-bottom: 8px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- &__header-buttons {
- display: flex;
- flex-direction: row;
- }
-
- &__header-button {
- font-size: .625rem;
-
- &:not(:last-child) {
- margin-right: 8px;
- }
- }
-
- &__sender-to-recipient-container {
- margin-bottom: 8px;
- }
-
- &__cards-container {
- display: flex;
- flex-direction: row;
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- }
- }
-
- &__transaction-breakdown {
- flex: 1;
- margin-right: 8px;
- min-width: 0;
-
- @media screen and (max-width: $break-small) {
- margin: 0 0 8px 0;
- }
- }
-
- &__transaction-activity-log {
- flex: 2;
- min-width: 0;
- }
-}
diff --git a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
deleted file mode 100644
index f2bbe8789..000000000
--- a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
+++ /dev/null
@@ -1,66 +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 wrapper = shallow(
- <TransactionListItemDetails
- transaction={transaction}
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-list-item-details'))
- assert.equal(wrapper.find(Button).length, 1)
- assert.equal(wrapper.find(SenderToRecipient).length, 1)
- assert.equal(wrapper.find(TransactionBreakdown).length, 1)
- assert.equal(wrapper.find(TransactionActivityLog).length, 1)
- })
-
- it('should render a retry button', () => {
- const transaction = {
- history: [],
- id: 1,
- status: 'confirmed',
- txParams: {
- from: '0x1',
- gas: '0x5208',
- gasPrice: '0x3b9aca00',
- nonce: '0xa4',
- to: '0x2',
- value: '0x2386f26fc10000',
- },
- }
-
- const wrapper = shallow(
- <TransactionListItemDetails
- transaction={transaction}
- showRetry={true}
- />,
- { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
- )
-
- assert.ok(wrapper.hasClass('transaction-list-item-details'))
- assert.equal(wrapper.find(Button).length, 2)
- })
-})
diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
deleted file mode 100644
index a4f28fd63..000000000
--- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import SenderToRecipient from '../sender-to-recipient'
-import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
-import TransactionActivityLog from '../transaction-activity-log'
-import TransactionBreakdown from '../transaction-breakdown'
-import Button from '../button'
-import Tooltip from '../tooltip'
-import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
-
-export default class TransactionListItemDetails extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- onCancel: PropTypes.func,
- onRetry: PropTypes.func,
- showCancel: PropTypes.bool,
- showRetry: PropTypes.bool,
- transaction: PropTypes.object,
- }
-
- handleEtherscanClick = () => {
- const { hash, metamaskNetworkId } = this.props.transaction
-
- const prefix = prefixForNetwork(metamaskNetworkId)
- const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
- global.platform.openWindow({ url: etherscanUrl })
- this.setState({ showTransactionDetails: true })
- }
-
- handleCancel = event => {
- const { onCancel } = this.props
-
- event.stopPropagation()
- onCancel()
- }
-
- handleRetry = event => {
- const { onRetry } = this.props
-
- event.stopPropagation()
- onRetry()
- }
-
- render () {
- const { t } = this.context
- const { transaction, showCancel, showRetry } = this.props
- const { txParams: { to, from } = {} } = transaction
-
- return (
- <div className="transaction-list-item-details">
- <div className="transaction-list-item-details__header">
- <div>Details</div>
- <div className="transaction-list-item-details__header-buttons">
- {
- showRetry && (
- <Button
- type="raised"
- onClick={this.handleRetry}
- className="transaction-list-item-details__header-button"
- >
- { t('speedUp') }
- </Button>
- )
- }
- {
- showCancel && (
- <Button
- type="raised"
- onClick={this.handleCancel}
- className="transaction-list-item-details__header-button"
- >
- { t('cancel') }
- </Button>
- )
- }
- <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__sender-to-recipient-container">
- <SenderToRecipient
- variant={CARDS_VARIANT}
- addressOnly
- recipientAddress={to}
- senderAddress={from}
- />
- </div>
- <div className="transaction-list-item-details__cards-container">
- <TransactionBreakdown
- transaction={transaction}
- className="transaction-list-item-details__transaction-breakdown"
- />
- <TransactionActivityLog
- transaction={transaction}
- className="transaction-list-item-details__transaction-activity-log"
- />
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss
deleted file mode 100644
index ac0e7beeb..000000000
--- a/ui/app/components/transaction-list-item/index.scss
+++ /dev/null
@@ -1,133 +0,0 @@
-.transaction-list-item {
- box-sizing: border-box;
- min-height: 74px;
- border-bottom: 1px solid $geyser;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- background: $white;
-
- &__grid {
- cursor: pointer;
- width: 100%;
- padding: 16px 20px;
- display: grid;
- grid-template-columns: 45px 1fr 1fr 1fr;
- grid-template-areas:
- "identicon action status primary-amount"
- "identicon nonce status secondary-amount";
-
- @media screen and (max-width: $break-small) {
- padding: 8px 20px 12px;
- grid-template-columns: 45px 5fr 3fr;
- grid-template-areas:
- "nonce nonce nonce"
- "identicon action primary-amount"
- "identicon status secondary-amount";
- }
-
- &:hover {
- background: rgba($alto, .2);
- }
- }
-
- &__identicon {
- grid-area: identicon;
- grid-row: 1 / span 2;
- align-self: center;
-
- @media screen and (max-width: $break-small) {
- grid-row: 2 / span 2;
- }
- }
-
- &__action {
- text-transform: capitalize;
- padding: 0 8px 2px 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- grid-area: action;
- align-self: end;
- }
-
- &__status {
- grid-area: status;
- grid-row: 1 / span 2;
- align-self: center;
-
- @media screen and (max-width: $break-small) {
- grid-row: 3;
- }
- }
-
- &__nonce {
- font-size: .75rem;
- color: #5e6064;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- grid-area: nonce;
- align-self: start;
-
- @media screen and (max-width: $break-small) {
- padding-bottom: 4px;
- }
- }
-
- &__amount {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &--primary {
- text-align: end;
- grid-area: primary-amount;
- align-self: end;
- justify-self: end;
-
- @media screen and (max-width: $break-small) {
- padding-bottom: 2px;
- }
- }
-
- &--secondary {
- text-align: end;
- font-size: .75rem;
- color: #5e6064;
- grid-area: secondary-amount;
- align-self: start;
- justify-self: end;
- }
- }
-
- &__retry {
- background: #d1edff;
- border-radius: 12px;
- font-size: .75rem;
- padding: 4px 12px;
- cursor: pointer;
- margin-top: 8px;
-
- @media screen and (max-width: $break-small) {
- font-size: .5rem;
- }
- }
-
- &__details-container {
- padding: 8px 16px 16px;
- background: #f3f4f7;
- width: 100%;
- }
-
- &__expander {
- max-height: 0px;
- width: 100%;
-
- &--show {
- max-height: 1000px;
- transition: max-height 700ms ease-out;
- }
- }
-}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js
deleted file mode 100644
index 696634fe0..000000000
--- a/ui/app/components/transaction-list-item/transaction-list-item.component.js
+++ /dev/null
@@ -1,184 +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,
- retryTransaction: PropTypes.func,
- setSelectedToken: PropTypes.func,
- showCancelModal: PropTypes.func,
- showCancel: PropTypes.bool,
- showRetry: PropTypes.bool,
- token: PropTypes.object,
- tokenData: PropTypes.object,
- transaction: PropTypes.object,
- value: PropTypes.string,
- }
-
- 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
- }
-
- this.setState({ showTransactionDetails: !showTransactionDetails })
- }
-
- handleCancel = () => {
- const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props
- showCancelModal(id, gasPrice)
- }
-
- handleRetry = () => {
- const {
- transaction: { txParams: { to } = {} },
- methodData: { name } = {},
- setSelectedToken,
- } = this.props
-
- if (name === TOKEN_METHOD_TRANSFER) {
- setSelectedToken(to)
- }
-
- return this.resubmit()
- }
-
- resubmit () {
- const { transaction: { id }, retryTransaction, history } = this.props
- return retryTransaction(id)
- .then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
- }
-
- renderPrimaryCurrency () {
- const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
-
- return token
- ? (
- <TokenCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--primary"
- token={token}
- transactionData={data}
- prefix="-"
- />
- ) : (
- <UserPreferencedCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--primary"
- value={value}
- type={PRIMARY}
- prefix="-"
- />
- )
- }
-
- renderSecondaryCurrency () {
- const { token, value } = this.props
-
- return token
- ? null
- : (
- <UserPreferencedCurrencyDisplay
- className="transaction-list-item__amount transaction-list-item__amount--secondary"
- value={value}
- prefix="-"
- type={SECONDARY}
- />
- )
- }
-
- render () {
- const {
- assetImages,
- methodData,
- nonceAndDate,
- showCancel,
- showRetry,
- tokenData,
- transaction,
- } = this.props
- const { txParams = {} } = transaction
- const { showTransactionDetails } = this.state
- const toAddress = tokenData
- ? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to
- : txParams.to
-
- return (
- <div className="transaction-list-item">
- <div
- className="transaction-list-item__grid"
- onClick={this.handleClick}
- >
- <Identicon
- className="transaction-list-item__identicon"
- address={toAddress}
- diameter={34}
- image={assetImages[toAddress]}
- />
- <TransactionAction
- transaction={transaction}
- methodData={methodData}
- className="transaction-list-item__action"
- />
- <div
- className="transaction-list-item__nonce"
- title={nonceAndDate}
- >
- { nonceAndDate }
- </div>
- <TransactionStatus
- className="transaction-list-item__status"
- statusKey={getStatusKey(transaction)}
- title={(
- (transaction.err && transaction.err.rpc)
- ? transaction.err.rpc.message
- : transaction.err && transaction.err.message
- )}
- />
- { this.renderPrimaryCurrency() }
- { this.renderSecondaryCurrency() }
- </div>
- <div className={classnames('transaction-list-item__expander', {
- 'transaction-list-item__expander--show': showTransactionDetails,
- })}>
- {
- showTransactionDetails && (
- <div className="transaction-list-item__details-container">
- <TransactionListItemDetails
- transaction={transaction}
- onRetry={this.handleRetry}
- showRetry={showRetry && methodData.done}
- onCancel={this.handleCancel}
- showCancel={showCancel}
- />
- </div>
- )
- }
- </div>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js
deleted file mode 100644
index 62ed7a73f..000000000
--- a/ui/app/components/transaction-list-item/transaction-list-item.container.js
+++ /dev/null
@@ -1,38 +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, retryTransaction, showModal } from '../../actions'
-import { hexToDecimal } from '../../helpers/conversions.util'
-import { getTokenData } from '../../helpers/transactions.util'
-import { formatDate } from '../../util'
-
-const mapStateToProps = (state, ownProps) => {
- const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
-
- const tokenData = data && getTokenData(data)
- const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
-
- return {
- value,
- nonceAndDate,
- tokenData,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
- retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
- showCancelModal: (transactionId, originalGasPrice) => {
- return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
- },
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps),
- withMethodData,
-)(TransactionListItem)
diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss
deleted file mode 100644
index ba7ffd87b..000000000
--- a/ui/app/components/transaction-list/index.scss
+++ /dev/null
@@ -1,46 +0,0 @@
-.transaction-list {
- display: flex;
- flex-direction: column;
- flex: 1;
- margin-top: 8px;
-
- &__completed-transactions {
- display: flex;
- flex-direction: column;
- flex: 1;
- }
-
- &__header {
- flex: 0 0 auto;
- font-size: .875rem;
- color: $dusty-gray;
- border-bottom: 1px solid $geyser;
- padding: 8px 0 8px 20px;
-
- @media screen and (max-width: $break-small) {
- padding: 8px 0 8px 16px;
- }
- }
-
- &__transactions {
- flex: 1;
- }
-
- &__pending-transactions {
- margin-bottom: 16px;
- }
-
- &__empty {
- flex: 1;
- display: grid;
- grid-template-rows: 35% 1fr;
- padding-top: 8px;
- }
-
- &__empty-text {
- grid-row-start: 2;
- display: flex;
- justify-content: center;
- color: $silver;
- }
-}
diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js
deleted file mode 100644
index eef60186d..000000000
--- a/ui/app/components/transaction-list/transaction-list.component.js
+++ /dev/null
@@ -1,119 +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: [],
- transactionToRetry: {},
- }
-
- static propTypes = {
- pendingTransactions: PropTypes.array,
- completedTransactions: PropTypes.array,
- transactionToRetry: PropTypes.object,
- selectedToken: PropTypes.object,
- updateNetworkNonce: PropTypes.func,
- assetImages: PropTypes.object,
- }
-
- componentDidMount () {
- this.props.updateNetworkNonce()
- }
-
- componentDidUpdate (prevProps) {
- const { pendingTransactions: prevPendingTransactions = [] } = prevProps
- const { pendingTransactions = [], updateNetworkNonce } = this.props
-
- if (pendingTransactions.length > prevPendingTransactions.length) {
- updateNetworkNonce()
- }
- }
-
- shouldShowRetry = transaction => {
- const { transactionToRetry } = this.props
- const { id, submittedTime } = transaction
- return id === transactionToRetry.id && Date.now() - submittedTime > 30000
- }
-
- renderTransactions () {
- const { t } = this.context
- const { pendingTransactions = [], completedTransactions = [] } = this.props
- return (
- <div className="transaction-list__transactions">
- {
- pendingTransactions.length > 0 && (
- <div className="transaction-list__pending-transactions">
- <div className="transaction-list__header">
- { `${t('queue')} (${pendingTransactions.length})` }
- </div>
- {
- pendingTransactions.map((transaction, index) => (
- this.renderTransaction(transaction, index, true)
- ))
- }
- </div>
- )
- }
- <div className="transaction-list__completed-transactions">
- <div className="transaction-list__header">
- { t('history') }
- </div>
- {
- completedTransactions.length > 0
- ? completedTransactions.map((transaction, index) => (
- this.renderTransaction(transaction, index)
- ))
- : this.renderEmpty()
- }
- </div>
- </div>
- )
- }
-
- renderTransaction (transaction, index, showCancel) {
- const { selectedToken, assetImages } = this.props
-
- return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
- ? (
- <ShapeShiftTransactionListItem
- { ...transaction }
- key={`shapeshift${index}`}
- />
- ) : (
- <TransactionListItem
- transaction={transaction}
- key={transaction.id}
- showRetry={this.shouldShowRetry(transaction)}
- showCancel={showCancel}
- token={selectedToken}
- assetImages={assetImages}
- />
- )
- }
-
- renderEmpty () {
- return (
- <div className="transaction-list__empty">
- <div className="transaction-list__empty-text">
- { this.context.t('noTransactions') }
- </div>
- </div>
- )
- }
-
- render () {
- return (
- <div className="transaction-list">
- { this.renderTransactions() }
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/transaction-list/transaction-list.container.js
deleted file mode 100644
index 2e946c67d..000000000
--- a/ui/app/components/transaction-list/transaction-list.container.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { connect } from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import TransactionList from './transaction-list.component'
-import {
- pendingTransactionsSelector,
- submittedPendingTransactionsSelector,
- completedTransactionsSelector,
-} from '../../selectors/transactions'
-import { getSelectedAddress, getAssetImages } from '../../selectors'
-import { selectedTokenSelector } from '../../selectors/tokens'
-import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
-import { updateNetworkNonce } from '../../actions'
-
-const mapStateToProps = state => {
- const pendingTransactions = pendingTransactionsSelector(state)
- const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
- const networkNonce = state.appState.networkNonce
-
- return {
- completedTransactions: completedTransactionsSelector(state),
- pendingTransactions,
- transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
- selectedToken: selectedTokenSelector(state),
- selectedAddress: getSelectedAddress(state),
- assetImages: getAssetImages(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { selectedAddress, ...restStateProps } = stateProps
- const { updateNetworkNonce, ...restDispatchProps } = dispatchProps
-
- return {
- ...restStateProps,
- ...restDispatchProps,
- ...ownProps,
- updateNetworkNonce: () => updateNetworkNonce(selectedAddress),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps, mergeProps)
-)(TransactionList)
diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss
deleted file mode 100644
index 26a1f5d38..000000000
--- a/ui/app/components/transaction-status/index.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-.transaction-status {
- height: 26px;
- width: 81px;
- border-radius: 4px;
- background-color: #f0f0f0;
- color: #5e6064;
- font-size: .625rem;
- text-transform: uppercase;
- display: flex;
- justify-content: center;
- align-items: center;
-
- @media screen and (max-width: $break-small) {
- height: 16px;
- width: 70px;
- font-size: .5rem;
- }
-
- &--confirmed {
- background-color: #eafad7;
- color: #609a1c;
- }
-
- &--approved, &--submitted {
- background-color: #FFF2DB;
- color: #CA810A;
- }
-
- &--failed {
- background: lighten($monzo, 56%);
- color: $monzo;
- }
-}
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 c22baf18a..000000000
--- a/ui/app/components/transaction-status/transaction-status.component.js
+++ /dev/null
@@ -1,59 +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,
-} from '../../constants/transactions'
-
-const statusToClassNameHash = {
- [UNAPPROVED_STATUS]: 'transaction-status--unapproved',
- [REJECTED_STATUS]: 'transaction-status--rejected',
- [APPROVED_STATUS]: 'transaction-status--approved',
- [SIGNED_STATUS]: 'transaction-status--signed',
- [SUBMITTED_STATUS]: 'transaction-status--submitted',
- [CONFIRMED_STATUS]: 'transaction-status--confirmed',
- [FAILED_STATUS]: 'transaction-status--failed',
- [DROPPED_STATUS]: 'transaction-status--dropped',
-}
-
-const statusToTextHash = {
- [APPROVED_STATUS]: 'pending',
- [SUBMITTED_STATUS]: 'pending',
-}
-
-export default class TransactionStatus extends PureComponent {
- static defaultProps = {
- title: null,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- statusKey: PropTypes.string,
- className: PropTypes.string,
- title: PropTypes.string,
- }
-
- render () {
- const { className, statusKey, title } = this.props
- const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
-
- return (
- <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
- <Tooltip position="top" title={title}>
- { statusText }
- </Tooltip>
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss
deleted file mode 100644
index 659f896ff..000000000
--- a/ui/app/components/transaction-view-balance/index.scss
+++ /dev/null
@@ -1,79 +0,0 @@
-.transaction-view-balance {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex: 1;
- height: 54px;
- min-width: 0;
-
- &__balance {
- margin: 0 12px;
- display: flex;
- flex-direction: column;
- min-width: 0;
-
- @media screen and (max-width: $break-small) {
- align-items: center;
- margin: 16px 0;
- }
- }
-
- &__token-balance {
- margin-left: 12px;
- font-size: 1.5rem;
-
- @media screen and (max-width: $break-small) {
- margin: 12px 0;
- margin-left: 0;
- font-size: 1.75rem;
- }
- }
-
- &__primary-balance {
- font-size: 1.5rem;
-
- @media screen and (max-width: $break-small) {
- font-size: 1.75rem;
- }
- }
-
- &__secondary-balance {
- font-size: 1.15rem;
- color: #a0a0a0;
- }
-
- &__balance-container {
- flex: 1;
- display: flex;
- flex-direction: row;
- align-items: center;
- min-width: 0;
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- }
- }
-
- &__buttons {
- display: flex;
- flex-direction: row;
-
- @media screen and (max-width: $break-small) {
- margin-bottom: 16px;
- }
- }
-
- &__button {
- min-width: initial;
- width: 100px;
-
- &:not(:last-child) {
- margin-right: 12px;
- }
- }
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- height: initial
- }
-}
diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
deleted file mode 100644
index 513a8aac9..000000000
--- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
+++ /dev/null
@@ -1,71 +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
-
-describe('TransactionViewBalance Component', () => {
- afterEach(() => {
- propsMethodSpies.showDepositModal.resetHistory()
- historySpies.push.resetHistory()
- })
-
- it('should render ETH balance properly', () => {
- const wrapper = shallow(<TransactionViewBalance
- showDepositModal={propsMethodSpies.showDepositModal}
- history={historySpies}
- network="3"
- ethBalance={123}
- fiatBalance={456}
- currentCurrency="usd"
- />, { context: { t } })
-
- assert.equal(wrapper.find('.transaction-view-balance').length, 1)
- assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
- assert.equal(wrapper.find(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 402b0f486..000000000
--- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-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'
-
-export default class TransactionViewBalance extends PureComponent {
- static contextTypes = {
- t: PropTypes.func,
- }
-
- static propTypes = {
- showDepositModal: PropTypes.func,
- selectedToken: PropTypes.object,
- history: PropTypes.object,
- network: PropTypes.string,
- balance: PropTypes.string,
- assetImage: PropTypes.string,
- }
-
- renderBalance () {
- const { selectedToken, balance } = this.props
-
- return selectedToken
- ? (
- <TokenBalance
- token={selectedToken}
- withSymbol
- className="transaction-view-balance__token-balance"
- />
- ) : (
- <div className="transaction-view-balance__balance">
- <UserPreferencedCurrencyDisplay
- className="transaction-view-balance__primary-balance"
- value={balance}
- type={PRIMARY}
- ethNumberOfDecimals={4}
- />
- <UserPreferencedCurrencyDisplay
- className="transaction-view-balance__secondary-balance"
- value={balance}
- type={SECONDARY}
- ethNumberOfDecimals={4}
- />
- </div>
- )
- }
-
- renderButtons () {
- const { t } = this.context
- const { selectedToken, showDepositModal, history } = this.props
-
- return (
- <div className="transaction-view-balance__buttons">
- {
- !selectedToken && (
- <Button
- type="primary"
- className="transaction-view-balance__button"
- onClick={() => showDepositModal()}
- >
- { t('deposit') }
- </Button>
- )
- }
- <Button
- type="primary"
- className="transaction-view-balance__button"
- onClick={() => history.push(SEND_ROUTE)}
- >
- { t('send') }
- </Button>
- </div>
- )
- }
-
- render () {
- const { network, selectedToken, assetImage } = this.props
-
- return (
- <div className="transaction-view-balance">
- <div className="transaction-view-balance__balance-container">
- <Identicon
- diameter={50}
- address={selectedToken && selectedToken.address}
- network={network}
- image={assetImage}
- />
- { this.renderBalance() }
- </div>
- { this.renderButtons() }
- </div>
- )
- }
-}
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
deleted file mode 100644
index cb8078ec1..000000000
--- a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
+++ /dev/null
@@ -1,32 +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 } from '../../selectors'
-import { showModal } from '../../actions'
-
-const mapStateToProps = state => {
- const selectedAddress = getSelectedAddress(state)
- const { metamask: { network, accounts } } = state
- const account = accounts[selectedAddress]
- const { balance } = account
-
- return {
- selectedToken: getSelectedToken(state),
- network,
- balance,
- nativeCurrency: getNativeCurrency(state),
- assetImage: getSelectedTokenAssetImage(state),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
- }
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TransactionViewBalance)
diff --git a/ui/app/components/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/ui/account-dropdown-mini/index.js b/ui/app/components/ui/account-dropdown-mini/index.js
new file mode 100644
index 000000000..cb0839e72
--- /dev/null
+++ b/ui/app/components/ui/account-dropdown-mini/index.js
@@ -0,0 +1 @@
+export { default } from './account-dropdown-mini.component'
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/ui/balance/index.js b/ui/app/components/ui/balance/index.js
new file mode 100644
index 000000000..f8fb9ea19
--- /dev/null
+++ b/ui/app/components/ui/balance/index.js
@@ -0,0 +1 @@
+export { default } from './balance.container'
diff --git a/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js
new file mode 100644
index 000000000..6644836db
--- /dev/null
+++ b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js
@@ -0,0 +1,29 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class Breadcrumbs extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ currentIndex: PropTypes.number,
+ total: PropTypes.number,
+ }
+
+ render () {
+ const { className, currentIndex, total } = this.props
+
+ return (
+ <div className={classnames('breadcrumbs', className)}>
+ {
+ Array(total).fill().map((_, i) => (
+ <div
+ key={i}
+ className="breadcrumb"
+ style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}}
+ />
+ ))
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/ui/breadcrumbs/index.js b/ui/app/components/ui/breadcrumbs/index.js
new file mode 100644
index 000000000..07a11574f
--- /dev/null
+++ b/ui/app/components/ui/breadcrumbs/index.js
@@ -0,0 +1 @@
+export { default } from './breadcrumbs.component'
diff --git a/ui/app/components/ui/breadcrumbs/index.scss b/ui/app/components/ui/breadcrumbs/index.scss
new file mode 100644
index 000000000..e23aa7970
--- /dev/null
+++ b/ui/app/components/ui/breadcrumbs/index.scss
@@ -0,0 +1,15 @@
+.breadcrumbs {
+ display: flex;
+ flex-flow: row nowrap;
+}
+
+.breadcrumb {
+ height: 10px;
+ width: 10px;
+ border: 1px solid #979797;
+ border-radius: 50%;
+}
+
+.breadcrumb + .breadcrumb {
+ margin-left: 10px;
+}
diff --git a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js
new file mode 100644
index 000000000..5013c5b60
--- /dev/null
+++ b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import Breadcrumbs from '../breadcrumbs.component'
+
+describe('Breadcrumbs Component', () => {
+ it('should render with the correct colors', () => {
+ const wrapper = shallow(
+ <Breadcrumbs
+ currentIndex={1}
+ total={3}
+ />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.breadcrumbs').length, 1)
+ assert.equal(wrapper.find('.breadcrumb').length, 3)
+ assert.equal(wrapper.find('.breadcrumb').at(0).props().style['backgroundColor'], '#FFFFFF')
+ assert.equal(wrapper.find('.breadcrumb').at(1).props().style['backgroundColor'], '#D8D8D8')
+ assert.equal(wrapper.find('.breadcrumb').at(2).props().style['backgroundColor'], '#FFFFFF')
+ })
+})
diff --git a/ui/app/components/ui/button-group/button-group.component.js b/ui/app/components/ui/button-group/button-group.component.js
new file mode 100644
index 000000000..17a281030
--- /dev/null
+++ b/ui/app/components/ui/button-group/button-group.component.js
@@ -0,0 +1,73 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class ButtonGroup extends PureComponent {
+ static propTypes = {
+ defaultActiveButtonIndex: PropTypes.number,
+ noButtonActiveByDefault: PropTypes.bool,
+ disabled: PropTypes.bool,
+ children: PropTypes.array,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ newActiveButtonIndex: PropTypes.number,
+ }
+
+ static defaultProps = {
+ className: 'button-group',
+ defaultActiveButtonIndex: 0,
+ }
+
+ state = {
+ activeButtonIndex: this.props.noButtonActiveByDefault
+ ? null
+ : this.props.defaultActiveButtonIndex,
+ }
+
+ componentDidUpdate (_, prevState) {
+ // Provides an API for dynamically updating the activeButtonIndex
+ if (typeof this.props.newActiveButtonIndex === 'number' && prevState.activeButtonIndex !== this.props.newActiveButtonIndex) {
+ this.setState({ activeButtonIndex: this.props.newActiveButtonIndex })
+ }
+ }
+
+ handleButtonClick (activeButtonIndex) {
+ this.setState({ activeButtonIndex })
+ }
+
+ renderButtons () {
+ const { children, disabled } = this.props
+
+ return React.Children.map(children, (child, index) => {
+ return child && (
+ <button
+ className={classnames(
+ 'button-group__button',
+ { 'button-group__button--active': index === this.state.activeButtonIndex },
+ )}
+ onClick={() => {
+ this.handleButtonClick(index)
+ child.props.onClick && child.props.onClick()
+ }}
+ disabled={disabled || child.props.disabled}
+ key={index}
+ >
+ { child.props.children }
+ </button>
+ )
+ })
+ }
+
+ render () {
+ const { className, style } = this.props
+
+ return (
+ <div
+ className={className}
+ style={style}
+ >
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
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/ui/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/tests/button-group-component.test.js
new file mode 100644
index 000000000..0bece90d6
--- /dev/null
+++ b/ui/app/components/ui/button-group/tests/button-group-component.test.js
@@ -0,0 +1,111 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ButtonGroup from '../button-group.component.js'
+
+const childButtonSpies = {
+ onClick: sinon.spy(),
+}
+
+sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
+sinon.spy(ButtonGroup.prototype, 'renderButtons')
+
+const mockButtons = [
+ <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
+ <button onClick={childButtonSpies.onClick} key={'b'}></button>,
+ <button onClick={childButtonSpies.onClick} key={'c'}></button>,
+]
+
+describe('ButtonGroup Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<ButtonGroup
+ defaultActiveButtonIndex={1}
+ disabled={false}
+ className="someClassName"
+ style={ { color: 'red' } }
+ >{mockButtons}</ButtonGroup>)
+ })
+
+ afterEach(() => {
+ childButtonSpies.onClick.resetHistory()
+ ButtonGroup.prototype.handleButtonClick.resetHistory()
+ ButtonGroup.prototype.renderButtons.resetHistory()
+ })
+
+ describe('componentDidUpdate', () => {
+ it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.setProps({ newActiveButtonIndex: 2 })
+ assert.equal(wrapper.state('activeButtonIndex'), 2)
+ })
+
+ it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.setProps({ newActiveButtonIndex: null })
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ })
+ })
+
+ describe('handleButtonClick', () => {
+ it('should set the activeButtonIndex', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.instance().handleButtonClick(2)
+ assert.equal(wrapper.state('activeButtonIndex'), 2)
+ })
+ })
+
+ describe('renderButtons', () => {
+ it('should render a button for each child', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ assert.equal(childButtons.length, 3)
+ })
+
+ it('should render the correct button with an active state', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ const activeChildButton = wrapper.find('.button-group__button--active')
+ assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
+ })
+
+ it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
+ assert.equal(childButtonSpies.onClick.callCount, 0)
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.at(0).props().onClick()
+ childButtons.at(1).props().onClick()
+ childButtons.at(2).props().onClick()
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
+ assert.equal(childButtonSpies.onClick.callCount, 3)
+ })
+
+ it('should render all child buttons as disabled if props.disabled is true', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.forEach(button => {
+ assert.equal(button.props().disabled, undefined)
+ })
+ wrapper.setProps({ disabled: true })
+ const disabledChildButtons = wrapper.find('[disabled=true]')
+ assert.equal(disabledChildButtons.length, 3)
+ })
+
+ it('should render the children of the button', () => {
+ const mockClass = wrapper.find('.mockClass')
+ assert.equal(mockClass.length, 1)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with the expected class and style', () => {
+ assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
+ assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
+ })
+
+ it('should call renderButtons when rendering', () => {
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
+ wrapper.instance().render()
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
+ })
+ })
+})
diff --git a/ui/app/components/ui/button/button.component.js b/ui/app/components/ui/button/button.component.js
new file mode 100644
index 000000000..5d19219b4
--- /dev/null
+++ b/ui/app/components/ui/button/button.component.js
@@ -0,0 +1,51 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const CLASSNAME_DEFAULT = 'btn-default'
+const CLASSNAME_PRIMARY = 'btn-primary'
+const CLASSNAME_SECONDARY = 'btn-secondary'
+const CLASSNAME_CONFIRM = 'btn-confirm'
+const CLASSNAME_RAISED = 'btn-raised'
+const CLASSNAME_LARGE = 'btn--large'
+const CLASSNAME_FIRST_TIME = 'btn--first-time'
+
+const typeHash = {
+ default: CLASSNAME_DEFAULT,
+ primary: CLASSNAME_PRIMARY,
+ secondary: CLASSNAME_SECONDARY,
+ confirm: CLASSNAME_CONFIRM,
+ raised: CLASSNAME_RAISED,
+ 'first-time': CLASSNAME_FIRST_TIME,
+}
+
+export default class Button extends Component {
+ static propTypes = {
+ type: PropTypes.string,
+ large: PropTypes.bool,
+ className: PropTypes.string,
+ children: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.array,
+ PropTypes.element,
+ ]),
+ }
+
+ render () {
+ const { type, large, className, ...buttonProps } = this.props
+
+ return (
+ <button
+ className={classnames(
+ 'button',
+ typeHash[type],
+ large && CLASSNAME_LARGE,
+ className
+ )}
+ { ...buttonProps }
+ >
+ { this.props.children }
+ </button>
+ )
+ }
+}
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/ui/currency-display/tests/currency-display.container.test.js b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js
new file mode 100644
index 000000000..9888c366e
--- /dev/null
+++ b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js
@@ -0,0 +1,145 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps, mergeProps
+
+proxyquire('../currency-display.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+})
+
+describe('CurrencyDisplay container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ })
+ })
+ })
+
+ describe('mergeProps()', () => {
+ it('should return the correct props', () => {
+ const mockStateProps = {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ }
+
+ const tests = [
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ numberOfDecimals: 2,
+ currency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ result: {
+ displayValue: '$2.80',
+ suffix: 'USD',
+ nativeCurrency: 'ETH',
+ },
+ },
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ currency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ result: {
+ displayValue: '$2.80',
+ suffix: 'USD',
+ nativeCurrency: 'ETH',
+ },
+ },
+ {
+ props: {
+ value: '0x1193461d01595930',
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 3,
+ },
+ result: {
+ displayValue: '1.266',
+ suffix: 'ETH',
+ nativeCurrency: 'ETH',
+ },
+ },
+ {
+ props: {
+ value: '0x1193461d01595930',
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 3,
+ hideLabel: true,
+ },
+ result: {
+ nativeCurrency: 'ETH',
+ displayValue: '1.266',
+ suffix: undefined,
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ denomination: 'GWEI',
+ hideLabel: true,
+ },
+ result: {
+ nativeCurrency: 'ETH',
+ displayValue: '1',
+ suffix: undefined,
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ denomination: 'WEI',
+ hideLabel: true,
+ },
+ result: {
+ nativeCurrency: 'ETH',
+ displayValue: '1000000000',
+ suffix: undefined,
+ },
+ },
+ {
+ props: {
+ value: '0x3b9aca00',
+ currency: 'ETH',
+ nativeCurrency: 'ETH',
+ numberOfDecimals: 100,
+ hideLabel: true,
+ },
+ result: {
+ nativeCurrency: 'ETH',
+ displayValue: '0.000000001',
+ suffix: undefined,
+ },
+ },
+ ]
+
+ tests.forEach(({ props, result }) => {
+ assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result)
+ })
+ })
+ })
+})
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/ui/currency-input/index.scss b/ui/app/components/ui/currency-input/index.scss
new file mode 100644
index 000000000..f659f5b35
--- /dev/null
+++ b/ui/app/components/ui/currency-input/index.scss
@@ -0,0 +1,26 @@
+.currency-input {
+ &__conversion-component {
+ font-size: 12px;
+ line-height: 12px;
+ padding-left: 1px;
+ }
+
+ &__swap-component {
+ flex: 0 0 auto;
+ height: 24px;
+ width: 24px;
+ background-image: url("images/icons/swap.svg");
+ background-size: contain;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ opacity: .4;
+
+ &:hover {
+ opacity: .3;
+ }
+
+ &:active {
+ opacity: .5;
+ }
+ }
+}
diff --git a/ui/app/components/ui/currency-input/tests/currency-input.component.test.js b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js
new file mode 100644
index 000000000..6d4612e3c
--- /dev/null
+++ b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js
@@ -0,0 +1,345 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import assert from 'assert'
+import { shallow, mount } from 'enzyme'
+import sinon from 'sinon'
+import { Provider } from 'react-redux'
+import configureMockStore from 'redux-mock-store'
+import CurrencyInput from '../currency-input.component'
+import UnitInput from '../../unit-input'
+import CurrencyDisplay from '../../currency-display'
+
+describe('CurrencyInput Component', () => {
+ describe('rendering', () => {
+ it('should render properly without a suffix', () => {
+ const wrapper = shallow(
+ <CurrencyInput />
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(UnitInput).length, 1)
+ })
+
+ it('should render properly with a suffix', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ nativeSuffix="ETH"
+ fiatSuffix="USD"
+ nativeCurrency="ETH"
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ })
+
+ it('should render properly with an ETH value', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ value="de0b6b3a7640000"
+ fiatSuffix="USD"
+ nativeSuffix="ETH"
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 1)
+ assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
+ })
+
+ it('should render properly with a fiat value', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ value="f602f2234d0ea"
+ fiatSuffix="USD"
+ nativeSuffix="ETH"
+ useFiat
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 1)
+ assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
+ })
+
+ it('should render properly with a native value when hideFiat is true', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ value="f602f2234d0ea"
+ fiatSuffix="USD"
+ nativeSuffix="ETH"
+ useFiat
+ hideFiat={true}
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ />
+ </Provider>,
+ {
+ context: { t: str => str + '_t' },
+ childContextTypes: { t: PropTypes.func },
+ }
+ )
+
+ assert.ok(wrapper)
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 0.004328)
+ assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '0.004328')
+ assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'noConversionRateAvailable_t')
+ })
+ })
+
+ describe('handling actions', () => {
+ const handleChangeSpy = sinon.spy()
+ const handleBlurSpy = sinon.spy()
+
+ afterEach(() => {
+ handleChangeSpy.resetHistory()
+ handleBlurSpy.resetHistory()
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ suffix="ETH"
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 0)
+ assert.equal(currencyInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
+ assert.equal(currencyInputInstance.state.decimalValue, 1)
+ assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000'))
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ suffix="USD"
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ useFiat
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 0)
+ assert.equal(currencyInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '0ETH')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('f602f2234d0ea'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
+ assert.equal(currencyInputInstance.state.decimalValue, 1)
+ assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('f602f2234d0ea'))
+ })
+
+ it('should change the state and pass in a new decimalValue when props.value changes', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = shallow(
+ <Provider store={store}>
+ <CurrencyInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ suffix="USD"
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ useFiat
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const currencyInputInstance = wrapper.find(CurrencyInput).dive()
+ assert.equal(currencyInputInstance.state('decimalValue'), 0)
+ assert.equal(currencyInputInstance.state('hexValue'), undefined)
+ assert.equal(currencyInputInstance.find(UnitInput).props().value, 0)
+
+ currencyInputInstance.setProps({ value: '1ec05e43e72400' })
+ currencyInputInstance.update()
+ assert.equal(currencyInputInstance.state('decimalValue'), 2)
+ assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400')
+ assert.equal(currencyInputInstance.find(UnitInput).props().value, 2)
+ })
+
+ it('should swap selected currency when swap icon is clicked', () => {
+ const mockStore = {
+ metamask: {
+ nativeCurrency: 'ETH',
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+ <Provider store={store}>
+ <CurrencyInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ nativeSuffix="ETH"
+ fiatSuffix="USD"
+ nativeCurrency="ETH"
+ currentCurrency="usd"
+ conversionRate={231.06}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
+ assert.equal(currencyInputInstance.state.decimalValue, 0)
+ assert.equal(currencyInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
+ assert.equal(currencyInputInstance.state.decimalValue, 1)
+ assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000'))
+
+ const swap = wrapper.find('.currency-input__swap-component')
+ swap.simulate('click')
+ assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
+ })
+ })
+})
diff --git a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js
new file mode 100644
index 000000000..6109d29b6
--- /dev/null
+++ b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js
@@ -0,0 +1,170 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps, mergeProps
+
+proxyquire('../currency-input.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+})
+
+describe('CurrencyInput container', () => {
+ describe('mapStateToProps()', () => {
+ const tests = [
+ // Test # 1
+ {
+ comment: 'should return correct props in mainnet',
+ mockState: {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ hideFiat: false,
+ },
+ },
+ // Test # 2
+ {
+ comment: 'should return correct props when not in mainnet and showFiatInTestnets is false',
+ mockState: {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ hideFiat: true,
+ },
+ },
+ // Test # 3
+ {
+ comment: 'should return correct props when not in mainnet and showFiatInTestnets is true',
+ mockState: {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ hideFiat: false,
+ },
+ },
+ // Test # 4
+ {
+ comment: 'should return correct props when in mainnet and showFiatInTestnets is true',
+ mockState: {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ hideFiat: false,
+ },
+ },
+ ]
+
+ tests.forEach(({ mockState, expected, comment }) => {
+ it(comment, () => assert.deepEqual(mapStateToProps(mockState), expected))
+ })
+ })
+
+ describe('mergeProps()', () => {
+ const tests = [
+ // Test # 1
+ {
+ comment: 'should return the correct props',
+ mock: {
+ stateProps: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ dispatchProps: {},
+ ownProps: {},
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ // useFiat: true,
+ nativeSuffix: 'ETH',
+ fiatSuffix: 'USD',
+ },
+ },
+ // Test # 1
+ {
+ comment: 'should return the correct props when useFiat is true',
+ mock: {
+ stateProps: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ dispatchProps: {},
+ ownProps: { useFiat: true },
+ },
+ expected: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ useFiat: true,
+ nativeSuffix: 'ETH',
+ fiatSuffix: 'USD',
+ },
+ },
+ ]
+
+ tests.forEach(({ mock: { stateProps, dispatchProps, ownProps }, expected, comment }) => {
+ it(comment, () => {
+ assert.deepEqual(mergeProps(stateProps, dispatchProps, ownProps), expected)
+ })
+ })
+ })
+})
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/ui/lock-icon/index.js b/ui/app/components/ui/lock-icon/index.js
new file mode 100644
index 000000000..6b4df0e58
--- /dev/null
+++ b/ui/app/components/ui/lock-icon/index.js
@@ -0,0 +1 @@
+export { default } from './lock-icon.component'
diff --git a/ui/app/components/ui/lock-icon/lock-icon.component.js b/ui/app/components/ui/lock-icon/lock-icon.component.js
new file mode 100644
index 000000000..d010cb6b2
--- /dev/null
+++ b/ui/app/components/ui/lock-icon/lock-icon.component.js
@@ -0,0 +1,32 @@
+import React from 'react'
+
+export default function LockIcon (props) {
+ return (
+ <svg
+ version="1.1"
+ id="Capa_1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ x="0px"
+ y="0px"
+ width="401.998px"
+ height="401.998px"
+ viewBox="0 0 401.998 401.998"
+ style={{enableBackground: 'new 0 0 401.998 401.998'}}
+ xmlSpace="preserve"
+ {...props}
+ >
+ <g>
+ <path
+ d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218
+ C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821
+ h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417
+ c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135
+ C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675
+ c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728
+ z"
+ />
+ </g>
+ </svg>
+ )
+}
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/ui/page-container/index.scss b/ui/app/components/ui/page-container/index.scss
new file mode 100644
index 000000000..b71a3cb9d
--- /dev/null
+++ b/ui/app/components/ui/page-container/index.scss
@@ -0,0 +1,219 @@
+.page-container {
+ width: 408px;
+ background-color: $white;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ z-index: 25;
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+ overflow-y: auto;
+
+ &__header {
+ display: flex;
+ flex-flow: column;
+ border-bottom: 1px solid $geyser;
+ padding: 16px;
+ flex: 0 0 auto;
+ position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
+ }
+
+ &__header-close {
+ color: $tundora;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ cursor: pointer;
+ overflow: hidden;
+
+ &::after {
+ content: '\00D7';
+ font-size: 40px;
+ line-height: 20px;
+ }
+ }
+
+ &__header-row {
+ padding-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__bottom {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__footer {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ border-top: 1px solid $geyser;
+ flex: 0 0 auto;
+
+ .btn-default,
+ .btn-confirm {
+ font-size: 1rem;
+ }
+
+ header {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ padding: 16px;
+ flex: 0 0 auto;
+ }
+
+ footer {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-around;
+ padding: 0 16px 16px;
+ flex: 0 0 auto;
+
+ a, a:hover {
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ color: #2f9ae0;
+ }
+ }
+ }
+
+ &__footer-button {
+ height: 55px;
+ font-size: 1rem;
+ text-transform: uppercase;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ }
+
+ &__title {
+ color: $black;
+ font-size: 2rem;
+ font-weight: 500;
+ line-height: 2rem;
+ margin-right: 1.5rem;
+ }
+
+ &__subtitle {
+ padding-top: .5rem;
+ line-height: initial;
+ font-size: .9rem;
+ color: $gray;
+ }
+
+ &__tabs {
+ display: flex;
+ margin-top: 16px;
+ }
+
+ &__tab {
+ min-width: 5rem;
+ padding: 8px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 1rem;
+ text-align: center;
+ cursor: pointer;
+ border-bottom: none;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ &--selected {
+ color: $curious-blue;
+ border-bottom: 2px solid $curious-blue;
+ }
+ }
+
+ &--full-width {
+ width: 100% !important;
+ }
+
+ &--full-height {
+ height: 100% !important;
+ max-height: initial !important;
+ min-height: initial !important;
+ }
+
+ &__content {
+ overflow-y: auto;
+ flex: 1;
+ }
+
+ &__warning-container {
+ background: $linen;
+ padding: 20px;
+ display: flex;
+ align-items: start;
+ }
+
+ &__warning-message {
+ padding-left: 15px;
+ }
+
+ &__warning-title {
+ font-weight: 500;
+ }
+
+ &__warning-icon {
+ padding-top: 5px;
+ }
+}
+
+@media screen and (max-width: 250px) {
+ .page-container {
+ &__footer {
+ flex-flow: column-reverse;
+ }
+
+ &__footer-button {
+ width: 100%;
+ margin-bottom: 1rem;
+ margin-right: 0;
+
+ &:first-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 575px) {
+ .page-container {
+ height: 100%;
+ width: 100%;
+ background-color: $white;
+ border-radius: 0;
+ flex: 1;
+ overflow-y: auto;
+ }
+}
+
+@media screen and (min-width: 576px) {
+ .page-container {
+ max-height: 82vh;
+ min-height: 570px;
+ flex: 0 0 auto;
+ margin-right: auto;
+ margin-left: auto;
+ }
+}
diff --git a/ui/app/components/page-container/page-container-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/ui/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
new file mode 100644
index 000000000..85b16cefe
--- /dev/null
+++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../button'
+
+export default class PageContainerFooter extends Component {
+
+ static propTypes = {
+ children: PropTypes.node,
+ onCancel: PropTypes.func,
+ cancelText: PropTypes.string,
+ onSubmit: PropTypes.func,
+ submitText: PropTypes.string,
+ disabled: PropTypes.bool,
+ submitButtonType: PropTypes.string,
+ hideCancel: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const {
+ children,
+ onCancel,
+ cancelText,
+ onSubmit,
+ submitText,
+ disabled,
+ submitButtonType,
+ hideCancel,
+ } = this.props
+
+ return (
+ <div className="page-container__footer">
+
+ <header>
+ {!hideCancel && <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={e => onCancel(e)}
+ >
+ { cancelText || this.context.t('cancel') }
+ </Button>}
+
+ <Button
+ type={submitButtonType || 'primary'}
+ large
+ className="page-container__footer-button"
+ disabled={disabled}
+ onClick={e => onSubmit(e)}
+ >
+ { submitText || this.context.t('next') }
+ </Button>
+ </header>
+
+ {children && (
+ <footer>
+ {children}
+ </footer>
+ )}
+
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/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/ui/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
new file mode 100644
index 000000000..08f9c7544
--- /dev/null
+++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js
@@ -0,0 +1,83 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class PageContainerHeader extends Component {
+ static propTypes = {
+ title: PropTypes.string,
+ subtitle: PropTypes.string,
+ onClose: PropTypes.func,
+ showBackButton: PropTypes.bool,
+ onBackButtonClick: PropTypes.func,
+ backButtonStyles: PropTypes.object,
+ backButtonString: PropTypes.string,
+ tabs: PropTypes.node,
+ headerCloseText: PropTypes.string,
+ }
+
+ renderTabs () {
+ const { tabs } = this.props
+
+ return tabs && (
+ <ul className="page-container__tabs">
+ { tabs }
+ </ul>
+ )
+ }
+
+ renderHeaderRow () {
+ const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
+
+ return showBackButton && (
+ <div className="page-container__header-row">
+ <span
+ className="page-container__back-button"
+ onClick={onBackButtonClick}
+ style={backButtonStyles}
+ >
+ { backButtonString || 'Back' }
+ </span>
+ </div>
+ )
+ }
+
+ render () {
+ const { title, subtitle, onClose, tabs, headerCloseText } = this.props
+
+ return (
+ <div className={
+ classnames(
+ 'page-container__header',
+ { 'page-container__header--no-padding-bottom': Boolean(tabs) }
+ )
+ }>
+
+ { this.renderHeaderRow() }
+
+ {
+ title && <div className="page-container__title">
+ { title }
+ </div>
+ }
+
+ {
+ subtitle && <div className="page-container__subtitle">
+ { subtitle }
+ </div>
+ }
+
+ {
+ onClose && headerCloseText
+ ? <div className="page-container__header-close-text" onClick={() => onClose()}>{ headerCloseText }</div>
+ : onClose && <div
+ className="page-container__header-close"
+ onClick={() => onClose()}
+ />
+ }
+
+ { this.renderTabs() }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/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/ui/page-container/page-container.component.js b/ui/app/components/ui/page-container/page-container.component.js
new file mode 100644
index 000000000..45dfff517
--- /dev/null
+++ b/ui/app/components/ui/page-container/page-container.component.js
@@ -0,0 +1,131 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+
+import PageContainerHeader from './page-container-header'
+import PageContainerFooter from './page-container-footer'
+
+export default class PageContainer extends PureComponent {
+ static propTypes = {
+ // PageContainerHeader props
+ backButtonString: PropTypes.string,
+ backButtonStyles: PropTypes.object,
+ headerCloseText: PropTypes.string,
+ onBackButtonClick: PropTypes.func,
+ onClose: PropTypes.func,
+ showBackButton: PropTypes.bool,
+ subtitle: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ // Tabs-related props
+ defaultActiveTabIndex: PropTypes.number,
+ tabsComponent: PropTypes.node,
+ // Content props
+ contentComponent: PropTypes.node,
+ // PageContainerFooter props
+ cancelText: PropTypes.string,
+ disabled: PropTypes.bool,
+ hideCancel: PropTypes.bool,
+ onCancel: PropTypes.func,
+ onSubmit: PropTypes.func,
+ submitText: PropTypes.string,
+ }
+
+ state = {
+ activeTabIndex: this.props.defaultActiveTabIndex || 0,
+ }
+
+ handleTabClick (activeTabIndex) {
+ this.setState({ activeTabIndex })
+ }
+
+ renderTabs () {
+ const { tabsComponent } = this.props
+
+ if (!tabsComponent) {
+ return
+ }
+
+ const numberOfTabs = React.Children.count(tabsComponent.props.children)
+
+ return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
+ return child && React.cloneElement(child, {
+ onClick: index => this.handleTabClick(index),
+ tabIndex,
+ isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
+ key: tabIndex,
+ className: 'page-container__tab',
+ activeClassName: 'page-container__tab--selected',
+ })
+ })
+ }
+
+ renderActiveTabContent () {
+ const { tabsComponent } = this.props
+ let { children } = tabsComponent.props
+ children = children.filter(child => child)
+ const { activeTabIndex } = this.state
+
+ return children[activeTabIndex]
+ ? children[activeTabIndex].props.children
+ : children.props.children
+ }
+
+ renderContent () {
+ const { contentComponent, tabsComponent } = this.props
+
+ if (contentComponent) {
+ return contentComponent
+ } else if (tabsComponent) {
+ return this.renderActiveTabContent()
+ } else {
+ return null
+ }
+ }
+
+ render () {
+ const {
+ title,
+ subtitle,
+ onClose,
+ showBackButton,
+ onBackButtonClick,
+ backButtonStyles,
+ backButtonString,
+ onCancel,
+ cancelText,
+ onSubmit,
+ submitText,
+ disabled,
+ headerCloseText,
+ hideCancel,
+ } = this.props
+
+ return (
+ <div className="page-container">
+ <PageContainerHeader
+ title={title}
+ subtitle={subtitle}
+ onClose={onClose}
+ showBackButton={showBackButton}
+ onBackButtonClick={onBackButtonClick}
+ backButtonStyles={backButtonStyles}
+ backButtonString={backButtonString}
+ tabs={this.renderTabs()}
+ headerCloseText={headerCloseText}
+ />
+ <div className="page-container__bottom">
+ <div className="page-container__content">
+ { this.renderContent() }
+ </div>
+ <PageContainerFooter
+ onCancel={onCancel}
+ cancelText={cancelText}
+ hideCancel={hideCancel}
+ onSubmit={onSubmit}
+ submitText={submitText}
+ disabled={disabled}
+ />
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/send/account-list-item/account-list-item.scss b/ui/app/components/ui/page-container/tests/page-container.component.test.js
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send/account-list-item/account-list-item.scss
+++ 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/ui/sender-to-recipient/index.scss b/ui/app/components/ui/sender-to-recipient/index.scss
new file mode 100644
index 000000000..b21e4e1bb
--- /dev/null
+++ b/ui/app/components/ui/sender-to-recipient/index.scss
@@ -0,0 +1,149 @@
+.sender-to-recipient {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ position: relative;
+ flex: 0 0 auto;
+
+ &--default {
+ border-bottom: 1px solid $geyser;
+ height: 42px;
+
+ .sender-to-recipient {
+ &__tooltip-wrapper {
+ min-width: 0;
+ }
+
+ &__tooltip-container {
+ max-width: 100%;
+ }
+
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex: 1;
+ padding: 0 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &--sender {
+ padding-right: 30px;
+ cursor: pointer;
+ }
+
+ &--recipient {
+ padding-left: 30px;
+ border-left: 1px solid $geyser;
+
+ &-with-address {
+ cursor: pointer;
+ }
+ }
+ }
+
+ &__arrow-container {
+ position: absolute;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__arrow-circle {
+ background: $white;
+ padding: 5px;
+ border: 1px solid $geyser;
+ border-radius: 20px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__name {
+ padding-left: 14px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .875rem;
+ }
+ }
+ }
+
+ &--cards {
+ .sender-to-recipient {
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ border-radius: 4px;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
+ padding: 6px;
+ background: $white;
+ cursor: pointer;
+ min-width: 0;
+ color: $dusty-gray;
+ }
+
+ &__tooltip-wrapper {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .5rem;
+ }
+
+ &__arrow-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+ }
+
+ &--flat {
+ .sender-to-recipient {
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ padding: 6px;
+ cursor: pointer;
+ min-width: 0;
+ color: $dusty-gray;
+ }
+
+ &__tooltip-wrapper {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .6875rem;
+ }
+
+ &__arrow-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+ }
+}
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/ui/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js
new file mode 100644
index 000000000..f53a5115d
--- /dev/null
+++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js
@@ -0,0 +1,4 @@
+// Component design variants
+export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
+export const CARDS_VARIANT = 'CARDS_VARIANT'
+export const FLAT_VARIANT = 'FLAT_VARIANT'
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/ui/token-balance/index.scss b/ui/app/components/ui/token-balance/index.scss
new file mode 100644
index 000000000..2ff6dfbc8
--- /dev/null
+++ b/ui/app/components/ui/token-balance/index.scss
@@ -0,0 +1,14 @@
+.token-balance-component {
+ display: flex;
+ align-items: center;
+
+ &__text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__suffix {
+ padding-left: 4px;
+ }
+}
diff --git a/ui/app/components/ui/token-balance/token-balance.component.js b/ui/app/components/ui/token-balance/token-balance.component.js
new file mode 100644
index 000000000..af1a32578
--- /dev/null
+++ b/ui/app/components/ui/token-balance/token-balance.component.js
@@ -0,0 +1,25 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../currency-display'
+
+export default class TokenBalance extends PureComponent {
+ static propTypes = {
+ string: PropTypes.string,
+ symbol: PropTypes.string,
+ error: PropTypes.string,
+ className: PropTypes.string,
+ withSymbol: PropTypes.bool,
+ }
+
+ render () {
+ const { className, string, symbol } = this.props
+
+ return (
+ <CurrencyDisplay
+ className={className}
+ displayValue={string}
+ suffix={symbol}
+ />
+ )
+ }
+}
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/ui/token-input/tests/token-input.component.test.js b/ui/app/components/ui/token-input/tests/token-input.component.test.js
new file mode 100644
index 000000000..881101880
--- /dev/null
+++ b/ui/app/components/ui/token-input/tests/token-input.component.test.js
@@ -0,0 +1,350 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import assert from 'assert'
+import { shallow, mount } from 'enzyme'
+import sinon from 'sinon'
+import { Provider } from 'react-redux'
+import configureMockStore from 'redux-mock-store'
+import TokenInput from '../token-input.component'
+import UnitInput from '../../unit-input'
+import CurrencyDisplay from '../../currency-display'
+
+describe('TokenInput Component', () => {
+ const t = key => `translate ${key}`
+
+ describe('rendering', () => {
+ it('should render properly without a token', () => {
+ const wrapper = shallow(
+ <TokenInput />,
+ { context: { t } }
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(UnitInput).length, 1)
+ })
+
+ it('should render properly with a token', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ />
+ </Provider>,
+ { context: { t },
+ childContextTypes: {
+ t: PropTypes.func,
+ },
+ },
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.currency-input__conversion-component').length, 1)
+ assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable')
+ })
+
+ it('should render properly with a token and selectedTokenExchangeRate', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ />
+ </Provider>,
+ { context: { t },
+ childContextTypes: {
+ t: PropTypes.func,
+ },
+ },
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ })
+
+ it('should render properly with a token value for ETH', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ value="2710"
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '2ETH')
+ })
+
+ it('should render properly with a token value for fiat', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ value="2710"
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ showFiat
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD')
+ })
+
+ it('should render properly with a token value for fiat, but hideConversion is true', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ value="2710"
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ showFiat
+ hideConversion
+ />
+ </Provider>,
+ {
+ context: { t },
+ childContextTypes: {
+ t: PropTypes.func,
+ },
+ },
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable')
+ })
+ })
+
+ describe('handling actions', () => {
+ const handleChangeSpy = sinon.spy()
+ const handleBlurSpy = sinon.spy()
+
+ afterEach(() => {
+ handleChangeSpy.resetHistory()
+ handleBlurSpy.resetHistory()
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 0)
+ assert.equal(tokenInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '0ETH')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('2710'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '2ETH')
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('2710'))
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+ <Provider store={store}>
+ <TokenInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ showFiat
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 0)
+ assert.equal(tokenInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('2710'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD')
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('2710'))
+ })
+
+ it('should change the state and pass in a new decimalValue when props.value changes', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = shallow(
+ <Provider store={store}>
+ <TokenInput
+ onChange={handleChangeSpy}
+ onBlur={handleBlurSpy}
+ selectedToken={{
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ }}
+ suffix="ABC"
+ selectedTokenExchangeRate={2}
+ showFiat
+ />
+ </Provider>
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).dive()
+ assert.equal(tokenInputInstance.state('decimalValue'), 0)
+ assert.equal(tokenInputInstance.state('hexValue'), undefined)
+ assert.equal(tokenInputInstance.find(UnitInput).props().value, 0)
+
+ tokenInputInstance.setProps({ value: '2710' })
+ tokenInputInstance.update()
+ assert.equal(tokenInputInstance.state('decimalValue'), 1)
+ assert.equal(tokenInputInstance.state('hexValue'), '2710')
+ assert.equal(tokenInputInstance.find(UnitInput).props().value, 1)
+ })
+ })
+})
diff --git a/ui/app/components/ui/token-input/tests/token-input.container.test.js b/ui/app/components/ui/token-input/tests/token-input.container.test.js
new file mode 100644
index 000000000..2b1c102c8
--- /dev/null
+++ b/ui/app/components/ui/token-input/tests/token-input.container.test.js
@@ -0,0 +1,255 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps, mergeProps
+
+proxyquire('../token-input.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+})
+
+describe('TokenInput container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props when send is empty', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {},
+ send: {},
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 0,
+ hideConversion: false,
+ })
+ })
+
+ it('should return the correct props when selectedTokenAddress is not found and send is populated', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x2',
+ contractExchangeRates: {},
+ send: {
+ token: { address: 'test' },
+ },
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: 'test',
+ },
+ selectedTokenExchangeRate: 0,
+ hideConversion: false,
+ })
+ })
+
+ it('should return the correct props when contractExchangeRates is populated', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {
+ '0x1': 5,
+ },
+ send: {},
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ hideConversion: false,
+ })
+ })
+
+ it('should return the correct props when not in mainnet and showFiatInTestnets is false', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {},
+ send: {},
+ preferences: {
+ showFiatInTestnets: false,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 0,
+ hideConversion: true,
+ })
+ })
+
+ it('should return the correct props when not in mainnet and showFiatInTestnets is true', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {},
+ send: {},
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'rinkeby',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 0,
+ hideConversion: false,
+ })
+ })
+
+ it('should return the correct props when in mainnet and showFiatInTestnets is true', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {},
+ send: {},
+ preferences: {
+ showFiatInTestnets: true,
+ },
+ provider: {
+ type: 'mainnet',
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 0,
+ hideConversion: false,
+ })
+ })
+ })
+
+ describe('mergeProps()', () => {
+ it('should return the correct props', () => {
+ const mockStateProps = {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ }
+
+ assert.deepEqual(mergeProps(mockStateProps, {}, {}), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ suffix: 'ABC',
+ })
+ })
+ })
+})
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/ui/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js
new file mode 100644
index 000000000..b54026794
--- /dev/null
+++ b/ui/app/components/ui/tooltip-v2.js
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types'
+import React, {PureComponent} from 'react'
+import {Tooltip as ReactTippy} from 'react-tippy'
+
+export default class Tooltip extends PureComponent {
+ static defaultProps = {
+ arrow: true,
+ children: null,
+ containerClassName: '',
+ hideOnClick: false,
+ onHidden: null,
+ position: 'left',
+ size: 'small',
+ title: null,
+ trigger: 'mouseenter',
+ wrapperClassName: '',
+ }
+
+ static propTypes = {
+ arrow: PropTypes.bool,
+ children: PropTypes.node,
+ containerClassName: PropTypes.string,
+ disabled: PropTypes.bool,
+ onHidden: PropTypes.func,
+ position: PropTypes.oneOf([
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ ]),
+ size: PropTypes.oneOf([
+ 'small', 'regular', 'big',
+ ]),
+ title: PropTypes.string,
+ trigger: PropTypes.any,
+ wrapperClassName: PropTypes.string,
+ style: PropTypes.object,
+ }
+
+ render () {
+ const {arrow, children, containerClassName, disabled, position, size, title, trigger, onHidden, wrapperClassName, style } = this.props
+
+ if (!title) {
+ return (
+ <div className={wrapperClassName}>
+ {children}
+ </div>
+ )
+ }
+
+ return (
+ <div className={wrapperClassName}>
+ <ReactTippy
+ className={containerClassName}
+ disabled={disabled}
+ title={title}
+ position={position}
+ trigger={trigger}
+ hideOnClick={false}
+ size={size}
+ arrow={arrow}
+ onHidden={onHidden}
+ style={style}
+ >
+ {children}
+ </ReactTippy>
+ </div>
+ )
+ }
+}
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/ui/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss
new file mode 100644
index 000000000..e4075d225
--- /dev/null
+++ b/ui/app/components/ui/unit-input/index.scss
@@ -0,0 +1,55 @@
+.unit-input {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ min-height: 54px;
+ border: 1px solid #dedede;
+ border-radius: 4px;
+ background-color: #fff;
+ color: #4d4d4d;
+ font-size: 1rem;
+ padding: 8px 10px;
+ position: relative;
+
+ input[type="number"] {
+ -moz-appearance: textfield;
+ }
+
+ 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;
+ }
+
+ &__inputs {
+ flex: 1 0 auto;
+ }
+
+ &__input {
+ color: #4d4d4d;
+ font-size: 1rem;
+ font-family: Roboto;
+ border: none;
+ outline: 0 !important;
+ max-width: 22ch;
+ }
+
+ &__input-container {
+ display: flex;
+ align-items: center;
+ }
+
+ &__suffix {
+ margin-left: 3px;
+ }
+
+ &--error {
+ border-color: $red;
+ }
+}
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/index.scss b/ui/app/components/unit-input/index.scss
deleted file mode 100644
index 28c5bf6f0..000000000
--- a/ui/app/components/unit-input/index.scss
+++ /dev/null
@@ -1,44 +0,0 @@
-.unit-input {
- min-height: 54px;
- border: 1px solid #dedede;
- border-radius: 4px;
- background-color: #fff;
- color: #4d4d4d;
- font-size: 1rem;
- padding: 8px 10px;
- position: relative;
-
- input[type="number"] {
- -moz-appearance: textfield;
- }
-
- 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;
- }
-
- &__input {
- color: #4d4d4d;
- font-size: 1rem;
- font-family: Roboto;
- border: none;
- outline: 0 !important;
- max-width: 22ch;
- }
-
- &__input-container {
- display: flex;
- align-items: center;
- }
-
- &--error {
- border-color: $red;
- }
-}
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 f1ebf4d77..000000000
--- a/ui/app/components/unit-input/unit-input.component.js
+++ /dev/null
@@ -1,104 +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,
- 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.75) + 'ch'
- }
-
- render () {
- const { error, placeholder, suffix, children } = this.props
- const { value } = this.state
-
- return (
- <div
- className={classnames('unit-input', { 'unit-input--error': error })}
- onClick={this.handleFocus}
- >
- <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>
- )
- }
-}
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/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
deleted file mode 100644
index ba1c23d83..000000000
--- a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import assert from 'assert'
-import proxyquire from 'proxyquire'
-
-let mapStateToProps, mergeProps
-
-proxyquire('../user-preferenced-currency-display.container.js', {
- 'react-redux': {
- connect: (ms, md, mp) => {
- mapStateToProps = ms
- mergeProps = mp
- return () => ({})
- },
- },
-})
-
-describe('UserPreferencedCurrencyDisplay container', () => {
- describe('mapStateToProps()', () => {
- it('should return the correct props', () => {
- const mockState = {
- metamask: {
- nativeCurrency: 'ETH',
- preferences: {
- useNativeCurrencyAsPrimaryCurrency: true,
- },
- },
- }
-
- assert.deepEqual(mapStateToProps(mockState), {
- nativeCurrency: 'ETH',
- useNativeCurrencyAsPrimaryCurrency: true,
- })
- })
- })
-
- describe('mergeProps()', () => {
- it('should return the correct props', () => {
- const mockDispatchProps = {}
-
- const tests = [
- {
- stateProps: {
- useNativeCurrencyAsPrimaryCurrency: true,
- nativeCurrency: 'ETH',
- },
- ownProps: {
- type: 'PRIMARY',
- },
- result: {
- currency: 'ETH',
- nativeCurrency: 'ETH',
- numberOfDecimals: 6,
- prefix: undefined,
- },
- },
- {
- stateProps: {
- useNativeCurrencyAsPrimaryCurrency: false,
- nativeCurrency: 'ETH',
- },
- ownProps: {
- type: 'PRIMARY',
- },
- result: {
- currency: undefined,
- nativeCurrency: 'ETH',
- numberOfDecimals: 2,
- prefix: undefined,
- },
- },
- {
- stateProps: {
- useNativeCurrencyAsPrimaryCurrency: true,
- nativeCurrency: 'ETH',
- },
- ownProps: {
- type: 'SECONDARY',
- fiatNumberOfDecimals: 4,
- fiatPrefix: '-',
- },
- result: {
- nativeCurrency: 'ETH',
- currency: undefined,
- numberOfDecimals: 4,
- prefix: '-',
- },
- },
- {
- stateProps: {
- useNativeCurrencyAsPrimaryCurrency: false,
- nativeCurrency: 'ETH',
- },
- ownProps: {
- type: 'SECONDARY',
- fiatNumberOfDecimals: 4,
- numberOfDecimals: 3,
- fiatPrefix: 'a',
- prefix: 'b',
- },
- result: {
- currency: 'ETH',
- nativeCurrency: 'ETH',
- numberOfDecimals: 3,
- prefix: 'b',
- },
- },
- ]
-
- tests.forEach(({ stateProps, ownProps, result }) => {
- assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), {
- ...result,
- })
- })
- })
- })
-})
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 f2a834ea7..000000000
--- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js
+++ /dev/null
@@ -1,46 +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,
- 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 7999301ad..000000000
--- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { connect } from 'react-redux'
-import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'
-import { preferencesSelector } from '../../selectors'
-import { ETH, PRIMARY, SECONDARY } from '../../constants/common'
-
-const mapStateToProps = (state, ownProps) => {
- const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state)
-
- return {
- useNativeCurrencyAsPrimaryCurrency,
- nativeCurrency: state.metamask.nativeCurrency,
- }
-}
-
-const mergeProps = (stateProps, dispatchProps, ownProps) => {
- const { useNativeCurrencyAsPrimaryCurrency, 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
- }
-
- 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 e050e0ee6..000000000
--- a/ui/app/components/wallet-view.js
+++ /dev/null
@@ -1,230 +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')
-const BalanceComponent = require('./balance-component')
-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,
-}
-
-WalletView.defaultProps = {
- responsiveDisplayClassname: '',
-}
-
-function mapStateToProps (state) {
-
- return {
- network: state.metamask.network,
- sidebarOpen: state.appState.sidebar.isOpen,
- identities: state.metamask.identities,
- accounts: state.metamask.accounts,
- tokens: state.metamask.tokens,
- 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
-
- return h(AddTokenButton, {
- onClick () {
- history.push(ADD_TOKEN_ROUTE)
- if (sidebarOpen) {
- hideSidebar()
- }
- },
- })
-}
-
-WalletView.prototype.render = function () {
- const {
- responsiveDisplayClassname,
- selectedAddress,
- keyrings,
- showAccountDetailModal,
- hideSidebar,
- identities,
- } = this.props
- // temporary logs + fake extra wallets
-
- const checksummedAddress = checksumAddress(selectedAddress)
-
- 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.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 0784a872e..000000000
--- a/ui/app/conf-tx.js
+++ /dev/null
@@ -1,224 +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')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(ConfirmTxScreen)
-
-function mapStateToProps (state) {
- const { metamask } = state
- const {
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = metamask
-
- return {
- identities: state.metamask.identities,
- accounts: state.metamask.accounts,
- 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 4ff4dc837..000000000
--- a/ui/app/constants/common.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const ETH = 'ETH'
-export const GWEI = 'GWEI'
-export const WEI = 'WEI'
-
-export const PRIMARY = 'PRIMARY'
-export const SECONDARY = 'SECONDARY'
diff --git a/ui/app/constants/transactions.js b/ui/app/constants/transactions.js
deleted file mode 100644
index 2dc061091..000000000
--- a/ui/app/constants/transactions.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export const UNAPPROVED_STATUS = 'unapproved'
-export const REJECTED_STATUS = 'rejected'
-export const APPROVED_STATUS = 'approved'
-export const SIGNED_STATUS = 'signed'
-export const SUBMITTED_STATUS = 'submitted'
-export const CONFIRMED_STATUS = 'confirmed'
-export const FAILED_STATUS = 'failed'
-export const DROPPED_STATUS = 'dropped'
-
-export const TOKEN_METHOD_TRANSFER = 'transfer'
-export const TOKEN_METHOD_APPROVE = 'approve'
-export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
-
-export const SEND_ETHER_ACTION_KEY = 'sentEther'
-export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
-export const APPROVE_ACTION_KEY = 'approve'
-export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
-export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
-export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
-export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
-export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
-
-export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
deleted file mode 100644
index f271b5683..000000000
--- a/ui/app/conversion-util.js
+++ /dev/null
@@ -1,251 +0,0 @@
-/* Currency Conversion Utility
-* This utility function can be used for converting currency related values within metamask.
-* The caller should be able to pass it a value, along with information about the value's
-* numeric base, denomination and currency, and the desired numeric base, denomination and
-* currency. It should return a single value.
-*
-* @param {(number | string | BN)} value The value to convert.
-* @param {Object} [options] Options to specify details of the conversion
-* @param {string} [options.fromCurrency = 'ETH' | 'USD'] The currency of the passed value
-* @param {string} [options.toCurrency = 'ETH' | 'USD'] The desired currency of the result
-* @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value.
-* @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result.
-* @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value
-* @param {string} [options.numberOfDecimals] The desired number of decimals in the result
-* @param {string} [options.roundDown] The desired number of decimals to round down to
-* @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion
-* @returns {(number | string | BN)}
-*
-* The utility passes value along with the options as a single object to the `converter` function.
-* `converter` uses Ramda.js to apply a composition of conditional setters to the `value` property, depending
-* on the accompanying options. Some of these conditional setters are selected via key-value maps, where
-* the keys are specified in the options parameters and the values are setter functions.
-*/
-
-const BigNumber = require('bignumber.js')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const R = require('ramda')
-const { stripHexPrefix } = require('ethereumjs-util')
-
-BigNumber.config({
- ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN,
-})
-
-// Big Number Constants
-const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
-const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
-const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1')
-
-// Individual Setters
-const convert = R.invoker(1, 'times')
-const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN)
-const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN)
-const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate)
-const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
-
-// Setter Maps
-const toBigNumber = {
- hex: n => new BigNumber(stripHexPrefix(n), 16),
- dec: n => new BigNumber(String(n), 10),
- BN: n => new BigNumber(n.toString(16), 16),
-}
-const toNormalizedDenomination = {
- WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
- GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
- ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER),
-}
-const toSpecifiedDenomination = {
- WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
- GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
- ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9),
-}
-const baseChange = {
- hex: n => n.toString(16),
- dec: n => Number(n).toString(10),
- BN: n => new BN(n.toString(16)),
-}
-
-// Predicates
-const fromAndToCurrencyPropsNotEqual = R.compose(
- R.not,
- R.eqBy(R.__, 'fromCurrency', 'toCurrency'),
- R.flip(R.prop)
-)
-
-// Lens
-const valuePropertyLens = R.over(R.lensProp('value'))
-const conversionRateLens = R.over(R.lensProp('conversionRate'))
-
-// conditional conversionRate setting wrapper
-const whenPredSetCRWithPropAndSetter = (pred, prop, setter) => R.when(
- pred,
- R.converge(
- conversionRateLens,
- [R.pipe(R.prop(prop), setter), R.identity]
- )
-)
-
-// conditional 'value' setting wrappers
-const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when(
- pred,
- R.converge(
- valuePropertyLens,
- [R.pipe(R.prop(prop), setter), R.identity]
- )
-)
-const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter(
- R.prop(prop),
- prop,
- R.prop(R.__, setterMap)
-)
-
-// Conversion utility function
-const converter = R.pipe(
- whenPredSetCRWithPropAndSetter(R.prop('conversionRate'), 'conversionRate', decToBigNumberViaString),
- whenPredSetCRWithPropAndSetter(R.prop('invertConversionRate'), 'conversionRate', invertConversionRate),
- whenPropApplySetterMap('fromNumericBase', toBigNumber),
- whenPropApplySetterMap('fromDenomination', toNormalizedDenomination),
- whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert),
- whenPropApplySetterMap('toDenomination', toSpecifiedDenomination),
- whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round),
- whenPredSetWithPropAndSetter(R.prop('roundDown'), 'roundDown', roundDown),
- whenPropApplySetterMap('toNumericBase', baseChange),
- R.view(R.lensProp('value'))
-)
-
-const conversionUtil = (value, {
- fromCurrency = null,
- toCurrency = fromCurrency,
- fromNumericBase,
- toNumericBase,
- fromDenomination,
- toDenomination,
- numberOfDecimals,
- conversionRate,
- invertConversionRate,
-}) => converter({
- fromCurrency,
- toCurrency,
- fromNumericBase,
- toNumericBase,
- fromDenomination,
- toDenomination,
- numberOfDecimals,
- conversionRate,
- invertConversionRate,
- value: value || '0',
-})
-
-const addCurrencies = (a, b, options = {}) => {
- const {
- aBase,
- bBase,
- ...conversionOptions
- } = options
- const value = (new BigNumber(a.toString(), aBase)).add(b.toString(), bBase)
-
- return converter({
- value,
- ...conversionOptions,
- })
-}
-
-const subtractCurrencies = (a, b, options = {}) => {
- const {
- aBase,
- bBase,
- ...conversionOptions
- } = options
- const value = (new BigNumber(String(a), aBase)).minus(b, bBase)
-
- return converter({
- value,
- ...conversionOptions,
- })
-}
-
-const multiplyCurrencies = (a, b, options = {}) => {
- const {
- multiplicandBase,
- multiplierBase,
- ...conversionOptions
- } = options
-
- const bigNumberA = new BigNumber(String(a), multiplicandBase)
- const bigNumberB = new BigNumber(String(b), multiplierBase)
-
- const value = bigNumberA.times(bigNumberB)
-
- return converter({
- value,
- ...conversionOptions,
- })
-}
-
-const conversionGreaterThan = (
- { ...firstProps },
- { ...secondProps },
-) => {
- const firstValue = converter({ ...firstProps })
- const secondValue = converter({ ...secondProps })
-
- return firstValue.gt(secondValue)
-}
-
-const conversionLessThan = (
- { ...firstProps },
- { ...secondProps },
-) => {
- const firstValue = converter({ ...firstProps })
- const secondValue = converter({ ...secondProps })
-
- return firstValue.lt(secondValue)
-}
-
-const conversionMax = (
- { ...firstProps },
- { ...secondProps },
-) => {
- const firstIsGreater = conversionGreaterThan(
- { ...firstProps },
- { ...secondProps }
- )
-
- return firstIsGreater ? firstProps.value : secondProps.value
-}
-
-const conversionGTE = (
- { ...firstProps },
- { ...secondProps },
-) => {
- const firstValue = converter({ ...firstProps })
- const secondValue = converter({ ...secondProps })
- return firstValue.greaterThanOrEqualTo(secondValue)
-}
-
-const conversionLTE = (
- { ...firstProps },
- { ...secondProps },
-) => {
- const firstValue = converter({ ...firstProps })
- const secondValue = converter({ ...secondProps })
- return firstValue.lessThanOrEqualTo(secondValue)
-}
-
-const toNegative = (n, options = {}) => {
- return multiplyCurrencies(n, -1, options)
-}
-
-module.exports = {
- conversionUtil,
- addCurrencies,
- multiplyCurrencies,
- conversionGreaterThan,
- conversionLessThan,
- conversionGTE,
- conversionLTE,
- conversionMax,
- toNegative,
- subtractCurrencies,
-}
diff --git a/ui/app/css/index.scss b/ui/app/css/index.scss
index c068028f8..ffccbd64f 100644
--- a/ui/app/css/index.scss
+++ b/ui/app/css/index.scss
@@ -11,8 +11,6 @@
@import './itcss/generic/index.scss';
-@import './itcss/base/index.scss';
-
@import './itcss/objects/index.scss';
@import './itcss/components/index.scss';
diff --git a/ui/app/css/itcss/base/index.scss b/ui/app/css/itcss/base/index.scss
deleted file mode 100644
index 1475e8bb5..000000000
--- a/ui/app/css/itcss/base/index.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-// Base
-
-.mouse-user-styles {
- button:focus {
- outline: 0;
- }
-}
diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss
index b29afdc8c..716404cc3 100644
--- a/ui/app/css/itcss/components/account-dropdown.scss
+++ b/ui/app/css/itcss/components/account-dropdown.scss
@@ -24,6 +24,10 @@
position: relative;
}
+ &__tooltip-wrapper {
+ left: -10px;
+ }
+
&__account-balances {
height: auto;
border: none;
@@ -34,6 +38,24 @@
position: relative;
}
+ &__primary-cached-container {
+ display: flex;
+ }
+
+ &__cached-star {
+ margin-left: 4px;
+ }
+
+ &__cached-balances {
+ div:first-of-type {
+ color: $web-orange;
+ }
+
+ div:last-of-type {
+ color: rgba(220, 153, 18, 0.6901960784313725)
+ }
+ }
+
&__account-name {
font-size: 16px;
margin-left: 8px;
@@ -52,6 +74,13 @@
font-size: 12px;
}
+ &__balance-flag {
+ position: absolute;
+ top: 3px;
+ left: -8px;
+ color: $curious-blue;
+ }
+
&__account-primary-balance {
color: $scorpion;
border: none;
diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss
deleted file mode 100644
index b14753e23..000000000
--- a/ui/app/css/itcss/components/account-menu.scss
+++ /dev/null
@@ -1,153 +0,0 @@
-.account-menu {
- position: fixed;
- z-index: 100;
- top: 58px;
- width: 310px;
-
- @media screen and (max-width: 575px) {
- right: calc(((100vw - 100%) / 2) + 8px);
- }
-
- @media screen and (min-width: 576px) {
- right: calc((100vw - 85vw) / 2);
- }
-
- @media screen and (min-width: 769px) {
- right: calc((100vw - 80vw) / 2);
- }
-
- @media screen and (min-width: 1281px) {
- right: calc((100vw - 65vw) / 2);
- }
-
- &__icon {
- margin-left: 20px;
- cursor: pointer;
-
- &--disabled {
- cursor: initial;
- }
- }
-
- &__header {
- display: flex;
- flex-flow: row nowrap;
- justify-content: space-between;
- align-items: center;
- }
-
- &__logout-button {
- border: 1px solid $dusty-gray;
- background-color: transparent;
- color: $white;
- border-radius: 4px;
- font-size: 12px;
- line-height: 23px;
- padding: 0 24px;
- }
-
- &__item-icon {
- width: 16px;
- height: 16px;
- }
-
- &__accounts {
- display: flex;
- flex-flow: column nowrap;
- overflow-y: auto;
- max-height: 240px;
- position: relative;
- z-index: 200;
-
- &::-webkit-scrollbar {
- display: none;
- }
-
- @media screen and (max-width: 575px) {
- max-height: 215px;
- }
-
- .keyring-label {
- margin-top: 5px;
- background-color: $dusty-gray;
- color: $black;
- font-weight: normal;
- letter-spacing: .5px;
- }
- }
-
- &__account {
- display: flex;
- flex-flow: row nowrap;
- padding: 16px 14px;
- flex: 0 0 auto;
-
- @media screen and (max-width: 575px) {
- padding: 12px 14px;
- }
-
- .remove-account-icon {
- width: 15px;
- margin-left: 10px;
- height: 15px;
- }
-
- &:hover {
- .remove-account-icon::after {
- content: '\00D7';
- font-size: 25px;
- color: $white;
- cursor: pointer;
- position: absolute;
- margin-top: -5px;
- }
- }
- }
-
- &__account-info {
- flex: 1 0 auto;
- display: flex;
- flex-flow: column nowrap;
- }
-
- &__check-mark {
- width: 14px;
- margin-right: 12px;
- flex: 0 0 auto;
- }
-
- &__check-mark-icon {
- background-image: url("images/check-white.svg");
- height: 18px;
- width: 18px;
- background-repeat: no-repeat;
- background-position: center;
- background-size: contain;
- margin: 3px 0;
- }
-
- .identicon {
- margin: 0 12px 0 0;
- flex: 0 0 auto;
- }
-
- &__name {
- color: $white;
- font-size: 18px;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- max-width: 200px;
- }
-
- &__balance {
- color: $dusty-gray;
- font-size: 14px;
- }
-
- &__action {
- font-size: 16px;
- line-height: 18px;
- cursor: pointer;
- }
-}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 5826a8b49..3e99d0ac6 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -87,6 +87,18 @@
min-width: initial;
}
+.btn--first-time {
+ height: 54px;
+ width: 198px;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
+ color: $white;
+ font-size: 1.25rem;
+ font-weight: 500;
+ transition: 200ms ease-in-out;
+ background-color: rgba(247, 134, 28, .9);
+ border-radius: 0;
+}
+
.btn--large {
min-height: 54px;
}
diff --git a/ui/app/css/itcss/components/gas-slider.scss b/ui/app/css/itcss/components/gas-slider.scss
index c27a560bd..e69de29bb 100644
--- a/ui/app/css/itcss/components/gas-slider.scss
+++ b/ui/app/css/itcss/components/gas-slider.scss
@@ -1,51 +0,0 @@
-.gas-slider {
- position: relative;
- width: 313px;
-
- &__input {
- width: 317px;
- margin-left: -2px;
- z-index: 2;
- }
-
- input[type=range] {
- -webkit-appearance: none !important;
- }
-
- input[type=range]::-webkit-slider-thumb {
- -webkit-appearance: none !important;
- height: 26px;
- width: 26px;
- border: 2px solid #B8B8B8;
- background-color: #FFFFFF;
- box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
- border-radius: 50%;
- position: relative;
- z-index: 10;
- }
-
- &__bar {
- height: 6px;
- width: 313px;
- background: $alto;
- display: flex;
- justify-content: space-between;
- position: absolute;
- top: 11px;
- z-index: 0;
- }
-
- &__low, &__high {
- height: 6px;
- width: 49px;
- z-index: 1;
- }
-
- &__low {
- background-color: $crimson;
- }
-
- &__high {
- background-color: $caribbean-green;
- }
-} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 63aa62eb3..f2f37bfa3 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -30,8 +30,6 @@
@import './currency-display.scss';
-@import './account-menu.scss';
-
@import './menu.scss';
@import './gas-slider.scss';
@@ -54,6 +52,4 @@
@import './tooltip.scss';
-@import './welcome-screen.scss';
-
-@import '../../../components/index';
+@import '../../../components/app/index';
diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss
index b023c8423..a99c58a23 100644
--- a/ui/app/css/itcss/components/loading-overlay.scss
+++ b/ui/app/css/itcss/components/loading-overlay.scss
@@ -1,7 +1,7 @@
.loading-overlay {
left: 0;
z-index: 51;
- position: absolute;
+ position: fixed;
flex-direction: column;
display: flex;
justify-content: center;
@@ -11,6 +11,12 @@
height: 100%;
background: rgba(255, 255, 255, .8);
+ &__screen-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
&__container {
position: absolute;
top: 33%;
@@ -26,6 +32,27 @@
font-size: 20px;
color: $manatee;
}
+
+ &__error-screen {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 160px;
+ justify-content: space-evenly;
+ }
+
+ &__error-buttons {
+ display: flex;
+ flex-direction: row;
+
+ button {
+ margin: 5px;
+ }
+ }
+
+ &__emoji {
+ font-size: 32px;
+ }
}
.spinner {
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
index 833a91f12..c828a2b26 100644
--- a/ui/app/css/itcss/components/network.scss
+++ b/ui/app/css/itcss/components/network.scss
@@ -1,6 +1,6 @@
.network-component--disabled {
// border-color: transparent !important;
- cursor: default;
+ cursor: not-allowed;
.fa-caret-down {
opacity: 0;
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index e4c7a4e0d..7bfa2d443 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -3,6 +3,7 @@
background-color: #FFFFFF;
box-shadow: 0 0 7px 0 rgba(0,0,0,0.08);
z-index: 25;
+ height: 100%;
&__header {
display: flex;
@@ -186,22 +187,8 @@
}
&__connect-btn {
- background-color: #259De5;
- color: #fff;
- border: none;
width: 315px;
- min-height: 54px;
- font-weight: 300;
- font-size: 14px;
- margin-bottom: 20px;
- margin-top: 20px;
- border-radius: 5px;
- display: flex;
- flex: 1;
- margin-left: 20px;
- margin-right: 20px;
- justify-content: center;
- text-transform: uppercase;
+ margin: 20px;
}
&__connect-btn.disabled {
@@ -449,6 +436,7 @@
margin-top: 10px;
&__button {
+ background: #fff;
height: 19px;
display: flex;
color: #33a4e7;
@@ -489,29 +477,8 @@
justify-content: space-between;
}
- &__button {
- width: 150px;
- min-width: initial;
- }
-
- .btn-primary {
- background-color: #259DE5;
- color: #FFFFFF;
- border: none;
- width: 100%;
- min-height: 54px;
- font-weight: 300;
- font-size: 14px;
- margin-bottom: 20px
- }
-
- &__button.unlock {
- width: 50%;
- }
-
- &__button.btn-primary--disabled {
- cursor: not-allowed;
- opacity: .5;
+ &__button:not(:last-child) {
+ margin-right: 16px;
}
}
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 233e781ef..9a0b81aed 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -8,6 +8,15 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma
// Component Colors
$wallet-view-bg: $alabaster;
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow-x: hidden;
+ position: relative;
+ align-items: center;
+}
+
// Main container
.main-container {
// position: absolute;
@@ -24,8 +33,10 @@ $wallet-view-bg: $alabaster;
.main-container-wrapper {
display: flex;
- width: 100vw;
justify-content: center;
+ flex: 1 0 auto;
+ min-height: 0;
+ width: 100%;
}
//Account and transaction details
@@ -207,15 +218,13 @@ $wallet-view-bg: $alabaster;
}
.main-container {
- // margin-top: 41px;
- height: 100%;
width: 100%;
overflow-y: auto;
background-color: $white;
}
.main-container-wrapper {
- height: 100%;
+ flex: 1;
}
}
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index a57653b45..07ab04613 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -520,6 +520,13 @@
color: $red;
}
+ &__warning {
+ font-size: 12px;
+ line-height: 12px;
+ left: 8px;
+ color: $orange;
+ }
+
&__error-border {
color: $red;
}
@@ -552,6 +559,8 @@
&__form-field {
flex: 1 1 auto;
+ min-width: 0;
+ max-width: 277px;
.currency-display {
color: $tundora;
@@ -578,8 +587,9 @@
font-family: Roboto;
font-size: 16px;
line-height: 22px;
- width: 88px;
+ width: 95px;
font-weight: 400;
+ flex: 0 0 auto;
}
&__from-dropdown {
@@ -682,6 +692,7 @@
display: flex;
align-items: center;
}
+
}
&__sliders-icon-container {
@@ -915,6 +926,16 @@
display: none;
}
}
+
+}
+
+.advanced-gas-options-btn {
+ display: flex;
+ justify-content: flex-end;
+ font-size: 14px;
+ color: #2f9ae0;
+ cursor: pointer;
+ margin-top: 16px;
}
.sliders-icon-container {
@@ -933,6 +954,23 @@
font-size: 1em;
}
+.gas-fee-reset {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 24px;
+ border-radius: 4px;
+ background-color: #fff;
+ position: absolute;
+ right: 12px;
+ top: 14px;
+ cursor: pointer;
+ font-size: 1em;
+ font-size: 14px;
+ color: #2f9ae0;
+ font-family: Roboto;
+}
+
.sliders-icon {
color: $curious-blue;
}
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/css/itcss/components/welcome-screen.scss b/ui/app/css/itcss/components/welcome-screen.scss
deleted file mode 100644
index af1d67398..000000000
--- a/ui/app/css/itcss/components/welcome-screen.scss
+++ /dev/null
@@ -1,60 +0,0 @@
-.welcome-screen {
- display: flex;
- flex-flow: column;
- justify-content: center;
- align-items: center;
- font-family: Roboto;
- font-weight: 400;
- width: 100%;
- flex: 1 0 auto;
- padding: 70px 0;
- background: $white;
-
- @media screen and (max-width: 575px) {
- padding: 0;
- }
-
- &__info {
- display: flex;
- flex-flow: column;
- width: 100%;
- height: 100%;
- align-items: center;
- justify-content: center;
-
- &__header {
- font-size: 1.65em;
- margin-bottom: 14px;
-
- @media screen and (max-width: 575px) {
- font-size: 1.5em;
- }
- }
-
- &__copy {
- font-size: 1em;
- width: 400px;
- max-width: 90vw;
- text-align: center;
-
- @media screen and (max-width: 575px) {
- font-size: .9em;
- }
- }
- }
-
- &__button {
- height: 54px;
- width: 198px;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
- color: #fff;
- font-size: 20px;
- font-weight: 500;
- line-height: 26px;
- text-align: center;
- text-transform: uppercase;
- margin: 35px 0 14px;
- transition: 200ms ease-in-out;
- background-color: rgba(247, 134, 28, .9);
- }
-}
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index d1c65afed..d8e62c97a 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -18,6 +18,10 @@ body {
height: 100%;
margin: 0;
padding: 0;
+
+ @media screen and (max-width: $break-small) {
+ overflow-y: overlay;
+ }
}
html {
diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss
index f90c8edc3..89bd8b96a 100644
--- a/ui/app/css/itcss/settings/variables.scss
+++ b/ui/app/css/itcss/settings/variables.scss
@@ -56,6 +56,10 @@ $zumthor: #edf7ff;
$ecstasy: #f7861c;
$linen: #fdf4f4;
$oslo-gray: #8C8E94;
+$polar: #fafcfe;
+$blizzard-blue: #bfdef3;
+$mischka: #dddee9;
+$web-orange: #f2a202;
/*
Z-Indicies
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 275eb1551..000000000
--- a/ui/app/ducks/confirm-transaction.duck.js
+++ /dev/null
@@ -1,411 +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'
-
-// 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 }) {
- 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))
- const toSmartContract = await isSmartContractAddress(to)
- dispatch(updateToSmartContract(toSmartContract))
- dispatch(setFetchingData(false))
- } catch (error) {
- dispatch(updateMethodData({}))
- 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/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/send.duck.js b/ui/app/ducks/send.duck.js
deleted file mode 100644
index db01bbaa9..000000000
--- a/ui/app/ducks/send.duck.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import extend from 'xtend'
-
-// Actions
-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 RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
-
-// TODO: determine if this approach to initState is consistent with conventional ducks pattern
-const initState = {
- fromDropdownOpen: false,
- toDropdownOpen: false,
- errors: {},
-}
-
-// Reducer
-export default function reducer ({ send: sendState = initState }, action = {}) {
- const newState = extend({}, sendState)
-
- switch (action.type) {
- case OPEN_FROM_DROPDOWN:
- return extend(newState, {
- fromDropdownOpen: true,
- })
- case CLOSE_FROM_DROPDOWN:
- return extend(newState, {
- fromDropdownOpen: false,
- })
- case OPEN_TO_DROPDOWN:
- return extend(newState, {
- toDropdownOpen: true,
- })
- case CLOSE_TO_DROPDOWN:
- return extend(newState, {
- toDropdownOpen: false,
- })
- case UPDATE_SEND_ERRORS:
- return extend(newState, {
- errors: {
- ...newState.errors,
- ...action.value,
- },
- })
- case RESET_SEND_STATE:
- return extend({}, initState)
- default:
- return newState
- }
-}
-
-// Action Creators
-export function openFromDropdown () {
- return { type: OPEN_FROM_DROPDOWN }
-}
-
-export function closeFromDropdown () {
- return { type: CLOSE_FROM_DROPDOWN }
-}
-
-export function openToDropdown () {
- return { type: OPEN_TO_DROPDOWN }
-}
-
-export function closeToDropdown () {
- return { type: CLOSE_TO_DROPDOWN }
-}
-
-export function updateSendErrors (errorObject) {
- return {
- type: UPDATE_SEND_ERRORS,
- value: errorObject,
- }
-}
-
-export function resetSendState () {
- return { type: RESET_SEND_STATE }
-}
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/send.duck.js b/ui/app/ducks/send/send.duck.js
new file mode 100644
index 000000000..90e92140b
--- /dev/null
+++ b/ui/app/ducks/send/send.duck.js
@@ -0,0 +1,106 @@
+import extend from 'xtend'
+
+// Actions
+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'
+
+// TODO: determine if this approach to initState is consistent with conventional ducks pattern
+const initState = {
+ fromDropdownOpen: false,
+ toDropdownOpen: false,
+ gasButtonGroupShown: true,
+ errors: {},
+ warnings: {},
+}
+
+// Reducer
+export default function reducer ({ send: sendState = initState }, action = {}) {
+ const newState = extend({}, sendState)
+
+ switch (action.type) {
+ case OPEN_FROM_DROPDOWN:
+ return extend(newState, {
+ fromDropdownOpen: true,
+ })
+ case CLOSE_FROM_DROPDOWN:
+ return extend(newState, {
+ fromDropdownOpen: false,
+ })
+ case OPEN_TO_DROPDOWN:
+ return extend(newState, {
+ toDropdownOpen: true,
+ })
+ case CLOSE_TO_DROPDOWN:
+ return extend(newState, {
+ toDropdownOpen: false,
+ })
+ case UPDATE_SEND_ERRORS:
+ return extend(newState, {
+ errors: {
+ ...newState.errors,
+ ...action.value,
+ },
+ })
+ case UPDATE_SEND_WARNINGS:
+ return extend(newState, {
+ warnings: {
+ ...newState.warnings,
+ ...action.value,
+ },
+ })
+ case SHOW_GAS_BUTTON_GROUP:
+ return extend(newState, {
+ gasButtonGroupShown: true,
+ })
+ case HIDE_GAS_BUTTON_GROUP:
+ return extend(newState, {
+ gasButtonGroupShown: false,
+ })
+ case RESET_SEND_STATE:
+ return extend({}, initState)
+ default:
+ return newState
+ }
+}
+
+// Action Creators
+export function openToDropdown () {
+ return { type: OPEN_TO_DROPDOWN }
+}
+
+export function closeToDropdown () {
+ return { type: CLOSE_TO_DROPDOWN }
+}
+
+export function showGasButtonGroup () {
+ return { type: SHOW_GAS_BUTTON_GROUP }
+}
+
+export function hideGasButtonGroup () {
+ return { type: HIDE_GAS_BUTTON_GROUP }
+}
+
+export function updateSendErrors (errorObject) {
+ return {
+ type: UPDATE_SEND_ERRORS,
+ value: errorObject,
+ }
+}
+
+export function updateSendWarnings (warningObject) {
+ return {
+ type: UPDATE_SEND_WARNINGS,
+ value: warningObject,
+ }
+}
+
+export function resetSendState () {
+ return { type: RESET_SEND_STATE }
+}
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/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js
deleted file mode 100644
index c101132d9..000000000
--- a/ui/app/ducks/tests/send-duck.test.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import assert from 'assert'
-
-import SendReducer, {
- openFromDropdown,
- closeFromDropdown,
- openToDropdown,
- closeToDropdown,
- updateSendErrors,
-} from '../send.duck.js'
-
-describe('Send Duck', () => {
- const mockState = {
- send: {
- mockProp: 123,
- },
- }
- const initState = {
- fromDropdownOpen: false,
- toDropdownOpen: false,
- errors: {},
- }
- 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 RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
-
- 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 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('openFromDropdown', () => {
- assert.deepEqual(
- openFromDropdown(),
- { type: OPEN_FROM_DROPDOWN }
- )
- })
-
- describe('closeFromDropdown', () => {
- assert.deepEqual(
- closeFromDropdown(),
- { type: CLOSE_FROM_DROPDOWN }
- )
- })
-
- describe('openToDropdown', () => {
- assert.deepEqual(
- openToDropdown(),
- { type: OPEN_TO_DROPDOWN }
- )
- })
-
- describe('closeToDropdown', () => {
- assert.deepEqual(
- closeToDropdown(),
- { type: CLOSE_TO_DROPDOWN }
- )
- })
-
- describe('updateSendErrors', () => {
- assert.deepEqual(
- updateSendErrors('mockErrorObject'),
- { type: UPDATE_SEND_ERRORS, value: 'mockErrorObject' }
- )
- })
-
-})
diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js
deleted file mode 100644
index e7bbfb225..000000000
--- a/ui/app/first-time/init-menu.js
+++ /dev/null
@@ -1,224 +0,0 @@
-const { EventEmitter } = require('events')
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const Mascot = require('../components/mascot')
-const actions = require('../actions')
-const Tooltip = require('../components/tooltip')
-const getCaretCoordinates = require('textarea-caret')
-const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes')
-const { getEnvironmentType } = require('../../../app/scripts/lib/util')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums')
-
-class InitializeMenuScreen extends Component {
- constructor (props) {
- super(props)
-
- this.animationEventEmitter = new EventEmitter()
- this.state = {
- warning: null,
- }
- }
-
- componentWillMount () {
- const { isInitialized, isUnlocked, history } = this.props
- if (isInitialized || isUnlocked) {
- history.push(DEFAULT_ROUTE)
- }
- }
-
- componentDidMount () {
- document.getElementById('password-box').focus()
- }
-
- render () {
- const { warning } = this.state
-
- return (
- h('.initialize-screen.flex-column.flex-center', [
-
- h(Mascot, {
- animationEventEmitter: this.animationEventEmitter,
- }),
-
- h('h1', {
- style: {
- fontSize: '1.3em',
- textTransform: 'uppercase',
- color: '#7F8082',
- marginBottom: 10,
- },
- }, this.context.t('appName')),
-
- h('div', [
- h('h3', {
- style: {
- fontSize: '0.8em',
- color: '#7F8082',
- display: 'inline',
- },
- }, this.context.t('encryptNewDen')),
-
- h(Tooltip, {
- title: this.context.t('denExplainer'),
- }, [
- h('i.fa.fa-question-circle.pointer', {
- style: {
- fontSize: '18px',
- position: 'relative',
- color: 'rgb(247, 134, 28)',
- top: '2px',
- marginLeft: '4px',
- },
- }),
- ]),
- ]),
-
- h('span.error.in-progress-notification', warning),
-
- // password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box',
- placeholder: this.context.t('newPassword'),
- onInput: this.inputChanged.bind(this),
- 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.createVaultOnEnter.bind(this),
- onInput: this.inputChanged.bind(this),
- style: {
- width: 260,
- marginTop: 16,
- },
- }),
-
-
- h('button.primary', {
- onClick: this.createNewVaultAndKeychain.bind(this),
- style: {
- margin: 12,
- },
- }, this.context.t('createDen')),
-
- h('.flex-row.flex-center.flex-grow', [
- h('p.pointer', {
- onClick: () => this.showRestoreVault(),
- style: {
- fontSize: '0.8em',
- color: 'rgb(247, 134, 28)',
- textDecoration: 'underline',
- },
- }, this.context.t('importDen')),
- ]),
-
- h('.flex-row.flex-center.flex-grow', [
- h('p.pointer', {
- onClick: this.showOldUI.bind(this),
- style: {
- fontSize: '0.8em',
- color: '#aeaeae',
- textDecoration: 'underline',
- marginTop: '32px',
- },
- }, this.context.t('classicInterface')),
- ]),
-
- ])
- )
- }
-
- createVaultOnEnter (event) {
- if (event.key === 'Enter') {
- event.preventDefault()
- this.createNewVaultAndKeychain()
- }
- }
-
- createNewVaultAndKeychain () {
- const { history } = this.props
- var passwordBox = document.getElementById('password-box')
- var password = passwordBox.value
- var passwordConfirmBox = document.getElementById('password-box-confirm')
- var passwordConfirm = passwordConfirmBox.value
-
- this.setState({ warning: null })
-
- if (password.length < 8) {
- this.setState({ warning: this.context.t('passwordShort') })
- return
- }
-
- if (password !== passwordConfirm) {
- this.setState({ warning: this.context.t('passwordMismatch') })
- return
- }
-
- this.props.createNewVaultAndKeychain(password)
- .then(() => history.push(DEFAULT_ROUTE))
- }
-
- inputChanged (event) {
- // tell mascot to look at page action
- var element = event.target
- var boundingRect = element.getBoundingClientRect()
- var coordinates = getCaretCoordinates(element, element.selectionEnd)
- this.animationEventEmitter.emit('point', {
- x: boundingRect.left + coordinates.left - element.scrollLeft,
- y: boundingRect.top + coordinates.top - element.scrollTop,
- })
- }
-
- showRestoreVault () {
- this.props.markPasswordForgotten()
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
-
- this.props.history.push(RESTORE_VAULT_ROUTE)
- }
-
- showOldUI () {
- this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- }
-}
-
-InitializeMenuScreen.propTypes = {
- history: PropTypes.object,
- isInitialized: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- createNewVaultAndKeychain: PropTypes.func,
- markPasswordForgotten: PropTypes.func,
- dispatch: PropTypes.func,
-}
-
-InitializeMenuScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const { metamask: { isInitialized, isUnlocked } } = state
-
- return {
- isInitialized,
- isUnlocked,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- createNewVaultAndKeychain: password => dispatch(actions.createNewVaultAndKeychain(password)),
- markPasswordForgotten: () => dispatch(actions.markPasswordForgotten()),
- }
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(InitializeMenuScreen)
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
deleted file mode 100644
index eb334a4b8..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 })
- : 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/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js
new file mode 100644
index 000000000..d0a819b9b
--- /dev/null
+++ b/ui/app/helpers/constants/transactions.js
@@ -0,0 +1,24 @@
+export const UNAPPROVED_STATUS = 'unapproved'
+export const REJECTED_STATUS = 'rejected'
+export const APPROVED_STATUS = 'approved'
+export const SIGNED_STATUS = 'signed'
+export const SUBMITTED_STATUS = 'submitted'
+export const CONFIRMED_STATUS = 'confirmed'
+export const FAILED_STATUS = 'failed'
+export const DROPPED_STATUS = 'dropped'
+export const CANCELLED_STATUS = 'cancelled'
+
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
+export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
+
+export const SEND_ETHER_ACTION_KEY = 'sentEther'
+export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
+export const APPROVE_ACTION_KEY = 'approve'
+export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
+export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
+export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
+export const CONTRACT_INTERACTION_KEY = 'contractInteraction'
+export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
+
+export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'
diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js
deleted file mode 100644
index cb5e1b90b..000000000
--- a/ui/app/helpers/conversions.util.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import ethUtil from 'ethereumjs-util'
-import { conversionUtil } from '../conversion-util'
-import { ETH, GWEI, WEI } from '../constants/common'
-
-export function bnToHex (inputBn) {
- return ethUtil.addHexPrefix(inputBn.toString(16))
-}
-
-export function hexToDecimal (hexValue) {
- return conversionUtil(hexValue, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
-}
-
-export function decimalToHex (decimal) {
- return conversionUtil(decimal, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
-}
-
-export function getEthConversionFromWeiHex ({ value, 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,
- })
-}
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/helpers/higher-order-components/authenticated/authenticated.container.js b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js
new file mode 100644
index 000000000..6124b0fcd
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import Authenticated from './authenticated.component'
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked, completedOnboarding } } = state
+ return {
+ isUnlocked,
+ completedOnboarding,
+ }
+}
+
+export default connect(mapStateToProps)(Authenticated)
diff --git a/ui/app/helpers/higher-order-components/authenticated/index.js b/ui/app/helpers/higher-order-components/authenticated/index.js
new file mode 100644
index 000000000..05632ed21
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/authenticated/index.js
@@ -0,0 +1 @@
+export { default } from './authenticated.container'
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/helpers/higher-order-components/initialized/index.js b/ui/app/helpers/higher-order-components/initialized/index.js
new file mode 100644
index 000000000..863fcb389
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/initialized/index.js
@@ -0,0 +1 @@
+export { default } from './initialized.container.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/helpers/higher-order-components/initialized/initialized.container.js b/ui/app/helpers/higher-order-components/initialized/initialized.container.js
new file mode 100644
index 000000000..0e7f72bcb
--- /dev/null
+++ b/ui/app/helpers/higher-order-components/initialized/initialized.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import Initialized from './initialized.component'
+
+const mapStateToProps = state => {
+ const { metamask: { completedOnboarding } } = state
+
+ return {
+ completedOnboarding,
+ }
+}
+
+export default connect(mapStateToProps)(Initialized)
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 2f4b1d095..000000000
--- a/ui/app/helpers/transactions.util.js
+++ /dev/null
@@ -1,159 +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 {
- TOKEN_METHOD_TRANSFER,
- TOKEN_METHOD_APPROVE,
- TOKEN_METHOD_TRANSFER_FROM,
- SEND_ETHER_ACTION_KEY,
- DEPLOY_CONTRACT_ACTION_KEY,
- APPROVE_ACTION_KEY,
- SEND_TOKEN_ACTION_KEY,
- TRANSFER_FROM_ACTION_KEY,
- SIGNATURE_REQUEST_KEY,
- UNKNOWN_FUNCTION_KEY,
- CANCEL_ATTEMPT_ACTION_KEY,
-} from '../constants/transactions'
-
-import { addCurrencies } from '../conversion-util'
-
-abiDecoder.addABI(abi)
-
-export function getTokenData (data = '') {
- return abiDecoder.decodeMethod(data)
-}
-
-const registry = new MethodRegistry({ provider: global.ethereumProvider })
-
-/**
- * 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 the action of a transaction as a key to be passed into the translator.
- * @param {Object} transaction - txData object
- * @param {Object} methodData - Data returned from eth-method-registry
- * @returns {string|undefined}
- */
-export async function getTransactionActionKey (transaction, methodData) {
- const { txParams: { data, to } = {}, msgParams, type } = transaction
-
- if (type === 'cancel') {
- return CANCEL_ATTEMPT_ACTION_KEY
- }
-
- if (msgParams) {
- return SIGNATURE_REQUEST_KEY
- }
-
- if (isConfirmDeployContract(transaction)) {
- return DEPLOY_CONTRACT_ACTION_KEY
- }
-
- if (data) {
- const toSmartContract = await isSmartContractAddress(to)
-
- if (!toSmartContract) {
- return SEND_ETHER_ACTION_KEY
- }
-
- const { name } = methodData
- const methodName = name && name.toLowerCase()
-
- if (!methodName) {
- return UNKNOWN_FUNCTION_KEY
- }
-
- switch (methodName) {
- case TOKEN_METHOD_TRANSFER:
- return SEND_TOKEN_ACTION_KEY
- case TOKEN_METHOD_APPROVE:
- return APPROVE_ACTION_KEY
- case TOKEN_METHOD_TRANSFER_FROM:
- return TRANSFER_FROM_ACTION_KEY
- default:
- return undefined
- }
- } else {
- return SEND_ETHER_ACTION_KEY
- }
-}
-
-export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') {
- if (!transactions.length) {
- return {}
- }
-
- return transactions.reduce((acc, current) => {
- const { submittedTime, txParams: { nonce: currentNonce } = {} } = current
-
- if (currentNonce === nonce) {
- return acc.submittedTime
- ? submittedTime > acc.submittedTime ? current : acc
- : current
- } else {
- return acc
- }
- }, {})
-}
-
-export async function isSmartContractAddress (address) {
- const code = await global.eth.getCode(address)
- // 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 } = {} } = transaction
-
- // There was an on-chain failure
- if (status === '0x0') {
- return 'failed'
- }
-
- 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/helpers/utils/conversion-util.js b/ui/app/helpers/utils/conversion-util.js
new file mode 100644
index 000000000..8cc531773
--- /dev/null
+++ b/ui/app/helpers/utils/conversion-util.js
@@ -0,0 +1,251 @@
+/* Currency Conversion Utility
+* This utility function can be used for converting currency related values within metamask.
+* The caller should be able to pass it a value, along with information about the value's
+* numeric base, denomination and currency, and the desired numeric base, denomination and
+* currency. It should return a single value.
+*
+* @param {(number | string | BN)} value The value to convert.
+* @param {Object} [options] Options to specify details of the conversion
+* @param {string} [options.fromCurrency = 'ETH' | 'USD'] The currency of the passed value
+* @param {string} [options.toCurrency = 'ETH' | 'USD'] The desired currency of the result
+* @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value.
+* @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result.
+* @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value
+* @param {string} [options.numberOfDecimals] The desired number of decimals in the result
+* @param {string} [options.roundDown] The desired number of decimals to round down to
+* @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion
+* @returns {(number | string | BN)}
+*
+* The utility passes value along with the options as a single object to the `converter` function.
+* `converter` uses Ramda.js to apply a composition of conditional setters to the `value` property, depending
+* on the accompanying options. Some of these conditional setters are selected via key-value maps, where
+* the keys are specified in the options parameters and the values are setter functions.
+*/
+
+const BigNumber = require('bignumber.js')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const R = require('ramda')
+const { stripHexPrefix } = require('ethereumjs-util')
+
+BigNumber.config({
+ ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN,
+})
+
+// Big Number Constants
+const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
+const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
+const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1')
+
+// Individual Setters
+const convert = R.invoker(1, 'times')
+const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN)
+const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN)
+const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate)
+const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
+
+// Setter Maps
+const toBigNumber = {
+ hex: n => new BigNumber(stripHexPrefix(n), 16),
+ dec: n => new BigNumber(String(n), 10),
+ BN: n => new BigNumber(n.toString(16), 16),
+}
+const toNormalizedDenomination = {
+ WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
+ GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
+ ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER),
+}
+const toSpecifiedDenomination = {
+ WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
+ GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
+ ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9),
+}
+const baseChange = {
+ hex: n => n.toString(16),
+ dec: n => (new BigNumber(n)).toString(10),
+ BN: n => new BN(n.toString(16)),
+}
+
+// Predicates
+const fromAndToCurrencyPropsNotEqual = R.compose(
+ R.not,
+ R.eqBy(R.__, 'fromCurrency', 'toCurrency'),
+ R.flip(R.prop)
+)
+
+// Lens
+const valuePropertyLens = R.over(R.lensProp('value'))
+const conversionRateLens = R.over(R.lensProp('conversionRate'))
+
+// conditional conversionRate setting wrapper
+const whenPredSetCRWithPropAndSetter = (pred, prop, setter) => R.when(
+ pred,
+ R.converge(
+ conversionRateLens,
+ [R.pipe(R.prop(prop), setter), R.identity]
+ )
+)
+
+// conditional 'value' setting wrappers
+const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when(
+ pred,
+ R.converge(
+ valuePropertyLens,
+ [R.pipe(R.prop(prop), setter), R.identity]
+ )
+)
+const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter(
+ R.prop(prop),
+ prop,
+ R.prop(R.__, setterMap)
+)
+
+// Conversion utility function
+const converter = R.pipe(
+ whenPredSetCRWithPropAndSetter(R.prop('conversionRate'), 'conversionRate', decToBigNumberViaString),
+ whenPredSetCRWithPropAndSetter(R.prop('invertConversionRate'), 'conversionRate', invertConversionRate),
+ whenPropApplySetterMap('fromNumericBase', toBigNumber),
+ whenPropApplySetterMap('fromDenomination', toNormalizedDenomination),
+ whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert),
+ whenPropApplySetterMap('toDenomination', toSpecifiedDenomination),
+ whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round),
+ whenPredSetWithPropAndSetter(R.prop('roundDown'), 'roundDown', roundDown),
+ whenPropApplySetterMap('toNumericBase', baseChange),
+ R.view(R.lensProp('value'))
+)
+
+const conversionUtil = (value, {
+ fromCurrency = null,
+ toCurrency = fromCurrency,
+ fromNumericBase,
+ toNumericBase,
+ fromDenomination,
+ toDenomination,
+ numberOfDecimals,
+ conversionRate,
+ invertConversionRate,
+}) => converter({
+ fromCurrency,
+ toCurrency,
+ fromNumericBase,
+ toNumericBase,
+ fromDenomination,
+ toDenomination,
+ numberOfDecimals,
+ conversionRate,
+ invertConversionRate,
+ value: value || '0',
+})
+
+const addCurrencies = (a, b, options = {}) => {
+ const {
+ aBase,
+ bBase,
+ ...conversionOptions
+ } = options
+ const value = (new BigNumber(a.toString(), aBase)).add(b.toString(), bBase)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const subtractCurrencies = (a, b, options = {}) => {
+ const {
+ aBase,
+ bBase,
+ ...conversionOptions
+ } = options
+ const value = (new BigNumber(String(a), aBase)).minus(b, bBase)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const multiplyCurrencies = (a, b, options = {}) => {
+ const {
+ multiplicandBase,
+ multiplierBase,
+ ...conversionOptions
+ } = options
+
+ const bigNumberA = new BigNumber(String(a), multiplicandBase)
+ const bigNumberB = new BigNumber(String(b), multiplierBase)
+
+ const value = bigNumberA.times(bigNumberB)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const conversionGreaterThan = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+
+ return firstValue.gt(secondValue)
+}
+
+const conversionLessThan = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+
+ return firstValue.lt(secondValue)
+}
+
+const conversionMax = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstIsGreater = conversionGreaterThan(
+ { ...firstProps },
+ { ...secondProps }
+ )
+
+ return firstIsGreater ? firstProps.value : secondProps.value
+}
+
+const conversionGTE = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+ return firstValue.greaterThanOrEqualTo(secondValue)
+}
+
+const conversionLTE = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+ return firstValue.lessThanOrEqualTo(secondValue)
+}
+
+const toNegative = (n, options = {}) => {
+ return multiplyCurrencies(n, -1, options)
+}
+
+module.exports = {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+ conversionLessThan,
+ conversionGTE,
+ conversionLTE,
+ conversionMax,
+ toNegative,
+ subtractCurrencies,
+}
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/utils/formatters.js b/ui/app/helpers/utils/formatters.js
new file mode 100644
index 000000000..106a2520d
--- /dev/null
+++ b/ui/app/helpers/utils/formatters.js
@@ -0,0 +1,3 @@
+export function formatETHFee (ethFee) {
+ return ethFee + ' ETH'
+}
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/helpers/utils/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js
new file mode 100644
index 000000000..01984bd5e
--- /dev/null
+++ b/ui/app/helpers/utils/metametrics.util.js
@@ -0,0 +1,184 @@
+/* eslint camelcase: 0 */
+
+const ethUtil = require('ethereumjs-util')
+
+const inDevelopment = process.env.NODE_ENV === 'development'
+
+const METAMETRICS_BASE_URL = 'https://chromeextensionmm.innocraft.cloud/piwik.php'
+const METAMETRICS_REQUIRED_PARAMS = `?idsite=${inDevelopment ? 1 : 2}&rec=1&apiv=1`
+const METAMETRICS_BASE_FULL = METAMETRICS_BASE_URL + METAMETRICS_REQUIRED_PARAMS
+
+const METAMETRICS_TRACKING_URL = inDevelopment
+ ? 'http://www.metamask.io/metametrics'
+ : 'http://www.metamask.io/metametrics-prod'
+
+const METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE = 'gasLimitChange'
+const METAMETRICS_CUSTOM_GAS_PRICE_CHANGE = 'gasPriceChange'
+const METAMETRICS_CUSTOM_FUNCTION_TYPE = 'functionType'
+const METAMETRICS_CUSTOM_RECIPIENT_KNOWN = 'recipientKnown'
+const METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN = 'origin'
+const METAMETRICS_CUSTOM_FROM_NETWORK = 'fromNetwork'
+const METAMETRICS_CUSTOM_TO_NETWORK = 'toNetwork'
+const METAMETRICS_CUSTOM_ERROR_FIELD = 'errorField'
+const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage'
+const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId'
+const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId'
+
+const METAMETRICS_CUSTOM_NETWORK = 'network'
+const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType'
+const METAMETRICS_CUSTOM_ACTIVE_CURRENCY = 'activeCurrency'
+const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType'
+const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens'
+const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts'
+
+const customVariableNameIdMap = {
+ [METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1,
+ [METAMETRICS_CUSTOM_RECIPIENT_KNOWN]: 2,
+ [METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 3,
+ [METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4,
+ [METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5,
+ [METAMETRICS_CUSTOM_FROM_NETWORK]: 1,
+ [METAMETRICS_CUSTOM_TO_NETWORK]: 2,
+ [METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 1,
+ [METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 2,
+ [METAMETRICS_CUSTOM_ERROR_FIELD]: 1,
+ [METAMETRICS_CUSTOM_ERROR_MESSAGE]: 2,
+}
+
+const customDimensionsNameIdMap = {
+ [METAMETRICS_CUSTOM_NETWORK]: 5,
+ [METAMETRICS_CUSTOM_ENVIRONMENT_TYPE]: 6,
+ [METAMETRICS_CUSTOM_ACTIVE_CURRENCY]: 7,
+ [METAMETRICS_CUSTOM_ACCOUNT_TYPE]: 8,
+ [METAMETRICS_CUSTOM_NUMBER_OF_TOKENS]: 9,
+ [METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS]: 10,
+}
+
+function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
+ const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'MetaMask'
+ return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
+}
+
+function composeCustomDimensionParamAddition (customDimensions) {
+ const customDimensionParamStrings = Object.keys(customDimensions).reduce((acc, name) => {
+ return [...acc, `dimension${customDimensionsNameIdMap[name]}=${customDimensions[name]}`]
+ }, [])
+ return `&${customDimensionParamStrings.join('&')}`
+}
+
+function composeCustomVarParamAddition (customVariables) {
+ const customVariableIdValuePairs = Object.keys(customVariables).reduce((acc, name) => {
+ return {
+ [customVariableNameIdMap[name]]: [name, customVariables[name]],
+ ...acc,
+ }
+ }, {})
+ return `&cvar=${encodeURIComponent(JSON.stringify(customVariableIdValuePairs))}`
+}
+
+function composeParamAddition (paramValue, paramName) {
+ return paramValue !== 0 && !paramValue
+ ? ''
+ : `&${paramName}=${paramValue}`
+}
+
+function composeUrl (config, permissionPreferences = {}) {
+ const {
+ eventOpts = {},
+ customVariables = '',
+ pageOpts = '',
+ network,
+ environmentType,
+ activeCurrency,
+ accountType,
+ numberOfTokens,
+ numberOfAccounts,
+ previousPath = '',
+ currentPath,
+ metaMetricsId,
+ confirmTransactionOrigin,
+ url: configUrl,
+ excludeMetaMetricsId,
+ isNewVisit,
+ } = config
+ const base = METAMETRICS_BASE_FULL
+
+ const e_c = composeParamAddition(eventOpts.category, 'e_c')
+ const e_a = composeParamAddition(eventOpts.action, 'e_a')
+ const e_n = composeParamAddition(eventOpts.name, 'e_n')
+ const new_visit = isNewVisit ? `&new_visit=1` : ''
+
+ const cvar = customVariables && composeCustomVarParamAddition(customVariables) || ''
+
+ const action_name = ''
+
+ const urlref = previousPath && composeUrlRefParamAddition(previousPath, confirmTransactionOrigin)
+
+ const dimensions = !pageOpts.hideDimensions ? composeCustomDimensionParamAddition({
+ network,
+ environmentType,
+ activeCurrency,
+ accountType,
+ numberOfTokens: customVariables && customVariables.numberOfTokens || numberOfTokens,
+ numberOfAccounts: customVariables && customVariables.numberOfAccounts || numberOfAccounts,
+ }) : ''
+ const url = configUrl || `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
+ const _id = metaMetricsId && !excludeMetaMetricsId ? `&_id=${metaMetricsId.slice(2, 18)}` : ''
+ const rand = `&rand=${String(Math.random()).slice(2)}`
+ const pv_id = `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}`
+ const uid = metaMetricsId && !excludeMetaMetricsId
+ ? `&uid=${metaMetricsId.slice(2, 18)}`
+ : excludeMetaMetricsId
+ ? '&uid=0000000000000000'
+ : ''
+
+ return [ base, e_c, e_a, e_n, cvar, action_name, urlref, dimensions, url, _id, rand, pv_id, uid, new_visit ].join('')
+}
+
+export function sendMetaMetricsEvent (config, permissionPreferences) {
+ return fetch(composeUrl(config, permissionPreferences), {
+ 'headers': {},
+ 'method': 'GET',
+ })
+}
+
+export function verifyUserPermission (config, props) {
+ const {
+ eventOpts = {},
+ } = config
+ const { userPermissionPreferences } = props
+ const {
+ allowAll,
+ allowNone,
+ allowSendMetrics,
+ } = userPermissionPreferences
+
+ if (allowNone) {
+ return false
+ } else if (allowAll) {
+ return true
+ } else if (allowSendMetrics && eventOpts.name === 'send') {
+ return true
+ } else {
+ return false
+ }
+}
+
+const trackableSendCounts = {
+ 1: true,
+ 10: true,
+ 30: true,
+ 50: true,
+ 100: true,
+ 250: true,
+ 500: true,
+ 1000: true,
+ 2500: true,
+ 5000: true,
+ 10000: true,
+ 25000: true,
+}
+
+export function sendCountIsTrackable (sendCount) {
+ return Boolean(trackableSendCounts[sendCount])
+}
diff --git a/ui/app/helpers/utils/token-util.js b/ui/app/helpers/utils/token-util.js
new file mode 100644
index 000000000..35a19a69f
--- /dev/null
+++ b/ui/app/helpers/utils/token-util.js
@@ -0,0 +1,118 @@
+const log = require('loglevel')
+const util = require('./util')
+const BigNumber = require('bignumber.js')
+import contractMap from 'eth-contract-metadata'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const DEFAULT_SYMBOL = ''
+const DEFAULT_DECIMALS = '0'
+
+async function getSymbolFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.symbol()
+ return result[0]
+ } catch (error) {
+ log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+async function getDecimalsFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.decimals()
+ const decimalsBN = result[0]
+ return decimalsBN && decimalsBN.toString()
+ } catch (error) {
+ log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+function getContractMetadata (tokenAddress) {
+ return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]
+}
+
+async function getSymbol (tokenAddress) {
+ let symbol = await getSymbolFromContract(tokenAddress)
+
+ if (!symbol) {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ symbol = contractMetadataInfo.symbol
+ }
+ }
+
+ return symbol
+}
+
+async function getDecimals (tokenAddress) {
+ let decimals = await getDecimalsFromContract(tokenAddress)
+
+ if (!decimals || decimals === '0') {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ decimals = contractMetadataInfo.decimals
+ }
+ }
+
+ return decimals
+}
+
+export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
+ const existingToken = existingTokens.find(({ address }) => tokenAddress === address)
+
+ if (existingToken) {
+ return {
+ symbol: existingToken.symbol,
+ decimals: existingToken.decimals,
+ }
+ }
+
+ let symbol, decimals
+
+ try {
+ symbol = await getSymbol(tokenAddress)
+ decimals = await getDecimals(tokenAddress)
+ } catch (error) {
+ log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error)
+ }
+
+ return {
+ symbol: symbol || DEFAULT_SYMBOL,
+ decimals: decimals || DEFAULT_DECIMALS,
+ }
+}
+
+export function tokenInfoGetter () {
+ const tokens = {}
+
+ return async (address) => {
+ if (tokens[address]) {
+ return tokens[address]
+ }
+
+ tokens[address] = await getSymbolAndDecimals(address)
+
+ return tokens[address]
+ }
+}
+
+export function calcTokenAmount (value, decimals) {
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ return new BigNumber(String(value)).div(multiplier)
+}
+
+export function getTokenValue (tokenParams = []) {
+ const valueData = tokenParams.find(param => param.name === '_value')
+ return valueData && valueData.value
+}
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/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 fed7d9865..000000000
--- a/ui/app/higher-order-components/with-method-data/with-method-data.component.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import { getMethodData } from '../../helpers/transactions.util'
-
-export default function withMethodData (WrappedComponent) {
- return class MethodDataWrappedComponent extends PureComponent {
- static propTypes = {
- transaction: PropTypes.object,
- }
-
- static defaultProps = {
- transaction: {},
- }
-
- state = {
- methodData: {},
- done: false,
- error: null,
- }
-
- componentDidMount () {
- this.fetchMethodData()
- }
-
- async fetchMethodData () {
- const { transaction } = this.props
- const { txParams: { data = '' } = {} } = transaction
-
- if (data) {
- try {
- const methodData = await getMethodData(data)
- this.setState({ methodData, done: true })
- } catch (error) {
- this.setState({ done: true, error })
- }
- } else {
- this.setState({ done: true })
- }
- }
-
- render () {
- const { methodData, done, error } = this.state
-
- return (
- <WrappedComponent
- { ...this.props }
- methodData={{ data: methodData, done, error }}
- />
- )
- }
- }
-}
diff --git a/ui/app/higher-order-components/with-modal-props/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/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/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
new file mode 100644
index 000000000..8ca7ca4e7
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
@@ -0,0 +1,14 @@
+import assert from 'assert'
+import { getMethodName } from '../confirm-transaction-base.component'
+
+describe('ConfirmTransactionBase Component', () => {
+ describe('getMethodName', () => {
+ it('should get correct method names', () => {
+ assert.equal(getMethodName(undefined), '')
+ assert.equal(getMethodName({}), '')
+ assert.equal(getMethodName('confirm'), 'confirm')
+ assert.equal(getMethodName('balanceOf'), 'balance Of')
+ assert.equal(getMethodName('ethToTokenSwapInput'), 'eth To Token Swap Input')
+ })
+ })
+})
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/pages/first-time-flow/create-password/create-password.container.js b/ui/app/pages/first-time-flow/create-password/create-password.container.js
new file mode 100644
index 000000000..89106f016
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/create-password.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import CreatePassword from './create-password.component'
+
+const mapStateToProps = state => {
+ const { metamask: { isInitialized } } = state
+
+ return {
+ isInitialized,
+ }
+}
+
+export default connect(mapStateToProps)(CreatePassword)
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/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
new file mode 100644
index 000000000..e5ff1fde5
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './import-with-seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/create-password/index.js b/ui/app/pages/first-time-flow/create-password/index.js
new file mode 100644
index 000000000..42e7436f9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/index.js
@@ -0,0 +1 @@
+export { default } from './create-password.container'
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/index.js b/ui/app/pages/first-time-flow/create-password/new-account/index.js
new file mode 100644
index 000000000..97db39cc3
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/index.js
@@ -0,0 +1 @@
+export { default } from './new-account.component'
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/pages/first-time-flow/create-password/unique-image/index.js b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
new file mode 100644
index 000000000..0e97bf755
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
@@ -0,0 +1 @@
+export { default } from './unique-image.container'
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/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
new file mode 100644
index 000000000..34874aaec
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import UniqueImage from './unique-image.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { selectedAddress } = metamask
+
+ return {
+ address: selectedAddress,
+ }
+}
+
+export default connect(mapStateToProps)(UniqueImage)
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/pages/first-time-flow/end-of-flow/index.js b/ui/app/pages/first-time-flow/end-of-flow/index.js
new file mode 100644
index 000000000..b0643d155
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.js
@@ -0,0 +1 @@
+export { default } from './end-of-flow.container'
diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.scss b/ui/app/pages/first-time-flow/end-of-flow/index.scss
new file mode 100644
index 000000000..d7eb4513b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.scss
@@ -0,0 +1,53 @@
+.end-of-flow {
+ color: black;
+ font-family: Roboto;
+ font-style: normal;
+
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ }
+ }
+
+ &__text-1, &__text-3 {
+ font-weight: normal;
+ font-size: 16px;
+ margin-top: 18px;
+ }
+
+ &__text-2 {
+ font-weight: bold;
+ font-size: 16px;
+ margin-top: 26px;
+ }
+
+ &__text-3 {
+ margin-top: 2px;
+ margin-bottom: 2px;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__text-4 {
+ margin-top: 26px;
+ }
+
+ button {
+ width: 207px;
+ }
+
+ &__start-over-button {
+ width: 744px;
+ }
+
+ &__emoji {
+ font-size: 80px;
+ margin-top: 70px;
+ }
+} \ No newline at end of file
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/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
new file mode 100644
index 000000000..d68f7a153
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import FirstTimeFlowSwitch from './first-time-flow-switch.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ participateInMetaMetrics: optInMetaMetrics,
+ } = metamask
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ optInMetaMetrics,
+ }
+}
+
+export default connect(mapStateToProps)(FirstTimeFlowSwitch)
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/index.js b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
new file mode 100644
index 000000000..3647756ef
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
@@ -0,0 +1 @@
+export { default } from './first-time-flow-switch.container'
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/pages/first-time-flow/index.js b/ui/app/pages/first-time-flow/index.js
new file mode 100644
index 000000000..5db42437c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.js
@@ -0,0 +1 @@
+export { default } from './first-time-flow.container'
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/pages/first-time-flow/metametrics-opt-in/index.js b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
new file mode 100644
index 000000000..4bc2fc3a7
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in.container'
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
new file mode 100644
index 000000000..6c2e37785
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
@@ -0,0 +1,136 @@
+.metametrics-opt-in {
+ position: relative;
+ width: 100%;
+
+ a {
+ color: #2f9ae0bf;
+ }
+
+ &__main {
+ display: flex;
+ flex-direction: column;
+ margin-left: 26.26%;
+ margin-right: 28%;
+ color: black;
+
+ @media screen and (max-width: 575px) {
+ justify-content: center;
+ margin-left: 2%;
+ margin-right: 0%;
+ }
+
+ .app-header__logo-container {
+ margin-top: 3%;
+ }
+ }
+
+ &__title {
+ position: relative;
+ margin-top: 20px;
+
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 42px;
+ }
+
+ &__body-graphic {
+ margin-top: 25px;
+
+ .fa-bar-chart {
+ color: #C4C4C4;
+ }
+ }
+
+ &__description {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 21px;
+ font-size: 16px;
+ margin-top: 12px;
+ }
+
+ &__committments {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__content {
+ overflow-y: scroll;
+ flex: 1;
+ }
+
+ &__row {
+ display: flex;
+ margin-top: 8px;
+
+ .fa-check {
+ margin-right: 12px;
+ color: #1ACC56;
+ }
+
+ .fa-times {
+ margin-right: 12px;
+ color: #D0021B;
+ }
+ }
+
+ &__bold {
+ font-weight: bold;
+ }
+
+ &__break-row {
+ margin-top: 30px;
+ }
+
+ &__body {
+ position: relative;
+ display: flex;
+ max-width: 730px;
+ flex-direction: column;
+ }
+
+ &__body-text {
+ max-width: 548px;
+ margin-left: 16px;
+ margin-right: 16px;
+ }
+
+ &__bottom-text {
+ margin-top: 10px;
+ color: #9a9a9a;
+ }
+
+ &__content {
+ overflow-y: auto;
+ }
+
+ &__footer {
+ margin-top: 26px;
+
+ @media screen and (max-width: 575px) {
+ margin-top: 10px;
+ justify-content: center;
+ margin-left: 2%;
+ max-height: 520px;
+ }
+
+ .page-container__footer {
+ border-top: none;
+ max-width: 535px;
+ margin-bottom: 15px;
+
+ button {
+ height: 44px;
+ min-height: 44px;
+ margin-right: 16px;
+ }
+
+ header {
+ padding: 0px;
+ }
+ }
+ }
+} \ No newline at end of file
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/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
new file mode 100644
index 000000000..f2476fc5c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
@@ -0,0 +1,41 @@
+export function selectSeedWord (word, shuffledIndex) {
+ return function update (state) {
+ const { selectedSeedWords, selectedSeedWordsHash } = state
+ const nextSelectedIndex = selectedSeedWords.length
+
+ return {
+ selectedSeedWords: [ ...selectedSeedWords, word ],
+ selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex },
+ }
+ }
+}
+
+export function deselectSeedWord (shuffledIndex) {
+ return function update (state) {
+ const {
+ selectedSeedWords: prevSelectedSeedWords,
+ selectedSeedWordsHash: prevSelectedSeedWordsHash,
+ } = state
+
+ const selectedSeedWords = [...prevSelectedSeedWords]
+ const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex]
+ selectedSeedWords.splice(indexToRemove, 1)
+ const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => {
+ const output = { ...acc }
+ const selectedSeedWordIndex = prevSelectedSeedWordsHash[index]
+
+ if (selectedSeedWordIndex < indexToRemove) {
+ output[index] = selectedSeedWordIndex
+ } else if (selectedSeedWordIndex > indexToRemove) {
+ output[index] = selectedSeedWordIndex - 1
+ }
+
+ return output
+ }, {})
+
+ return {
+ selectedSeedWords,
+ selectedSeedWordsHash,
+ }
+ }
+}
diff --git a/ui/app/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
new file mode 100644
index 000000000..c7b511503
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-seed-phrase.component'
diff --git a/ui/app/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
new file mode 100644
index 000000000..93137618c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
@@ -0,0 +1,48 @@
+.confirm-seed-phrase {
+ &__back-button {
+ margin-bottom: 12px;
+ }
+
+ &__selected-seed-words {
+ min-height: 190px;
+ max-width: 496px;
+ border: 1px solid #CDCDCD;
+ border-radius: 6px;
+ background-color: $white;
+ margin: 24px 0 36px;
+ padding: 12px;
+ }
+
+ &__shuffled-seed-words {
+ max-width: 496px;
+ }
+
+ &__seed-word {
+ display: inline-block;
+ color: #5B5D67;
+ background-color: #E7E7E7;
+ padding: 8px 18px;
+ min-width: 64px;
+ margin: 4px;
+ text-align: center;
+
+ &--selected {
+ background-color: #85D1CC;
+ color: $white;
+ }
+
+ &--shuffled {
+ cursor: pointer;
+ margin: 6px;
+ }
+
+ @media screen and (max-width: 575px) {
+ font-size: .875rem;
+ padding: 6px 18px;
+ }
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/index.js
new file mode 100644
index 000000000..185b3f089
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './seed-phrase.component'
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/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
new file mode 100644
index 000000000..4a1b191b5
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './reveal-seed-phrase.component'
diff --git a/ui/app/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
new file mode 100644
index 000000000..8a47447ed
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
@@ -0,0 +1,57 @@
+.reveal-seed-phrase {
+ &__secret {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ border: 1px solid #CDCDCD;
+ border-radius: 6px;
+ background-color: $white;
+ padding: 18px;
+ margin-top: 36px;
+ max-width: 350px;
+ }
+
+ &__secret-words {
+ width: 310px;
+ font-size: 1.25rem;
+ text-align: center;
+
+ &--hidden {
+ filter: blur(5px);
+ }
+ }
+
+ &__secret-blocker {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ width: 100%;
+ background-color: rgba(0,0,0,0.6);
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 0 18px;
+ cursor: pointer;
+ }
+
+ &__reveal-button {
+ color: $white;
+ font-size: .75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ margin-top: 8px;
+ text-align: center;
+ }
+
+ &__export-text {
+ color: $curious-blue;
+ cursor: pointer;
+ font-weight: 500;
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
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/pages/first-time-flow/select-action/index.js b/ui/app/pages/first-time-flow/select-action/index.js
new file mode 100644
index 000000000..4fbe1823b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.js
@@ -0,0 +1 @@
+export { default } from './select-action.container'
diff --git a/ui/app/pages/first-time-flow/select-action/index.scss b/ui/app/pages/first-time-flow/select-action/index.scss
new file mode 100644
index 000000000..e1b22d05b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.scss
@@ -0,0 +1,88 @@
+.select-action {
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__body-header {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 39px;
+ font-size: 28px;
+ text-align: center;
+ margin-top: 65px;
+ color: black;
+ }
+
+ &__select-buttons {
+ display: flex;
+ flex-direction: row;
+ margin-top: 40px;
+ }
+
+ &__select-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-evenly;
+ width: 388px;
+ height: 278px;
+
+ border: 1px solid #D8D8D8;
+ box-sizing: border-box;
+ border-radius: 10px;
+ margin-left: 22px;
+
+ .first-time-flow__button {
+ max-width: 221px;
+ height: 44px;
+ }
+ }
+
+ &__button-symbol {
+ color: #C4C4C4;
+ margin-top: 41px;
+ }
+
+ &__button-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 144px;
+ }
+
+ &__button-text-big {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 28px;
+ font-size: 20px;
+ color: #000000;
+ margin-top: 12px;
+ text-align: center;
+ }
+
+ &__button-text-small {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 20px;
+ font-size: 14px;
+ color: #7A7A7B;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ button {
+ font-weight: 500;
+ width: 221px;
+ }
+} \ No newline at end of file
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/pages/first-time-flow/welcome/index.js b/ui/app/pages/first-time-flow/welcome/index.js
new file mode 100644
index 000000000..8abeddaa1
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.js
@@ -0,0 +1 @@
+export { default } from './welcome.container'
diff --git a/ui/app/pages/first-time-flow/welcome/index.scss b/ui/app/pages/first-time-flow/welcome/index.scss
new file mode 100644
index 000000000..3b5071480
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.scss
@@ -0,0 +1,42 @@
+.welcome-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ max-width: 442px;
+ padding: 0 18px;
+ color: black;
+
+ &__wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ height: 100%;
+ margin-top: 110px;
+ }
+
+ &__header {
+ font-size: 28px;
+ margin-bottom: 22px;
+ margin-top: 50px;
+ }
+
+ &__description {
+ text-align: center;
+
+ div {
+ font-size: 16px;
+ }
+
+ @media screen and (max-width: 575px) {
+ font-size: .9rem;
+ }
+ }
+
+ .first-time-flow__button {
+ width: 184px;
+ font-weight: 500;
+ margin-top: 44px;
+ }
+}
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/pages/keychains/index.scss b/ui/app/pages/keychains/index.scss
new file mode 100644
index 000000000..868185419
--- /dev/null
+++ b/ui/app/pages/keychains/index.scss
@@ -0,0 +1,197 @@
+.first-view-main-wrapper {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ padding: 0 10px;
+}
+
+.first-view-main {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+}
+
+@media screen and (min-width: 1281px) {
+ .first-view-main {
+ width: 62vw;
+ }
+}
+
+.import-account {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 60px 0 30px 0;
+ position: relative;
+ max-width: initial;
+}
+
+@media only screen and (max-width: 575px) {
+ .import-account{
+ margin: 24px;
+ display: flex;
+ flex-flow: column nowrap;
+ width: calc(100vw - 80px);
+ }
+
+ .import-account__title {
+ width: initial !important;
+ }
+
+ .first-view-main {
+ height: 100%;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ margin-top: 12px;
+ }
+
+ .first-view-phone-invisible {
+ display: none;
+ }
+
+ .first-time-flow__input {
+ width: 100%;
+ }
+
+ .import-account__secret-phrase {
+ width: initial !important;
+ height: initial !important;
+ min-height: 190px;
+ }
+}
+
+.import-account__title {
+ color: #1B344D;
+ font-size: 40px;
+ line-height: 51px;
+ margin-bottom: 10px;
+}
+
+.import-account__back-button {
+ margin-bottom: 18px;
+ color: #22232c;
+ font-size: 16px;
+ line-height: 21px;
+ position: absolute;
+ top: -25px;
+}
+
+.import-account__secret-phrase {
+ height: 190px;
+ width: 495px;
+ border: 1px solid #CDCDCD;
+ border-radius: 6px;
+ background-color: #FFFFFF;
+ padding: 17px;
+ font-size: 16px;
+}
+
+.import-account__secret-phrase::placeholder {
+ color: #9B9B9B;
+ font-weight: 200;
+}
+
+.import-account__faq-link {
+ font-size: 18px;
+ line-height: 23px;
+ font-family: Roboto;
+}
+
+.import-account__selector-label {
+ color: #1B344D;
+ font-size: 16px;
+}
+
+.import-account__dropdown {
+ width: 325px;
+ border: 1px solid #CDCDCD;
+ border-radius: 4px;
+ background-color: #FFFFFF;
+ margin-top: 14px;
+ color: #5B5D67;
+ font-family: Roboto;
+ font-size: 18px;
+ line-height: 23px;
+ padding: 14px 21px;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ cursor: pointer;
+}
+
+.import-account__description-text {
+ color: #757575;
+ font-size: 18px;
+ line-height: 23px;
+ margin-top: 21px;
+ font-family: Roboto;
+}
+
+.import-account__input-wrapper {
+ display: flex;
+ flex-flow: column nowrap;
+ margin-top: 30px;
+}
+
+.import-account__input-error-message {
+ margin-top: 10px;
+ width: 422px;
+ color: #FF001F;
+ font-size: 16px;
+ line-height: 21px;
+}
+
+.import-account__input-label {
+ margin-bottom: 9px;
+ color: #1B344D;
+ font-size: 18px;
+ line-height: 23px;
+}
+
+.import-account__input-label__disabled {
+ opacity: 0.5;
+}
+
+.import-account__input {
+ width: 350px;
+}
+
+@media only screen and (max-width: 575px) {
+ .import-account__input {
+ width: 100%;
+ }
+}
+
+.import-account__file-input {
+ display: none;
+}
+
+.import-account__file-input-label {
+ height: 53px;
+ width: 148px;
+ border: 1px solid #1B344D;
+ border-radius: 4px;
+ color: #1B344D;
+ font-family: Roboto;
+ font-size: 18px;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.import-account__file-picker-wrapper {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+}
+
+.import-account__file-name {
+ color: #000000;
+ font-family: Roboto;
+ font-size: 18px;
+ line-height: 23px;
+ margin-left: 22px;
+}
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/pages/lock/index.js b/ui/app/pages/lock/index.js
new file mode 100644
index 000000000..7bfe2a61f
--- /dev/null
+++ b/ui/app/pages/lock/index.js
@@ -0,0 +1 @@
+export { default } from './lock.container'
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/pages/unlock-page/index.scss b/ui/app/pages/unlock-page/index.scss
new file mode 100644
index 000000000..3d44bd037
--- /dev/null
+++ b/ui/app/pages/unlock-page/index.scss
@@ -0,0 +1,51 @@
+.unlock-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ width: 357px;
+ padding: 30px;
+ font-weight: 400;
+ color: $silver-chalice;
+
+ &__container {
+ background: $white;
+ display: flex;
+ align-self: stretch;
+ justify-content: center;
+ flex: 1 0 auto;
+ }
+
+ &__mascot-container {
+ margin-top: 24px;
+ }
+
+ &__title {
+ margin-top: 5px;
+ font-size: 2rem;
+ font-weight: 800;
+ color: $tundora;
+ }
+
+ &__form {
+ width: 100%;
+ margin: 56px 0 8px;
+ }
+
+ &__links {
+ margin-top: 25px;
+ width: 100%;
+ }
+
+ &__link {
+ cursor: pointer;
+
+ &--import {
+ color: $ecstasy;
+ }
+
+ &--use-classic {
+ margin-top: 10px;
+ }
+ }
+}
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 e1a982f93..000000000
--- a/ui/app/reducers.js
+++ /dev/null
@@ -1,92 +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'
-
-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)
-
- 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 5c86d397d..000000000
--- a/ui/app/reducers/app.js
+++ /dev/null
@@ -1,778 +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: '',
- },
- 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`,
- },
- }, 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,
- })
-
- 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 dfa2f3656..000000000
--- a/ui/app/reducers/metamask.js
+++ /dev/null
@@ -1,384 +0,0 @@
-const extend = require('xtend')
-const actions = require('../actions')
-const MetamascaraPlatform = require('../../../app/scripts/platforms/window')
-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,
- isMascara: window.platform instanceof MetamascaraPlatform,
- 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: null,
- from: '',
- to: '',
- amount: '0x0',
- 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,
- },
- }, 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_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: { ...action.payload },
- })
- }
-
- default:
- return metamaskState
-
- }
-}
diff --git a/ui/app/root.js b/ui/app/root.js
deleted file mode 100644
index 09deae1b1..000000000
--- a/ui/app/root.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const { Provider } = require('react-redux')
-const h = require('react-hyperscript')
-const SelectedApp = require('./select-app')
-
-class Root extends Component {
- render () {
- const { store } = this.props
-
- return (
- h(Provider, { store }, [
- h(SelectedApp),
- ])
- )
- }
-}
-
-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 76afed5db..000000000
--- a/ui/app/routes.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const DEFAULT_ROUTE = '/'
-const UNLOCK_ROUTE = '/unlock'
-const SETTINGS_ROUTE = '/settings'
-const INFO_ROUTE = '/settings/info'
-const REVEAL_SEED_ROUTE = '/seed'
-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_CREATE_PASSWORD_ROUTE = '/initialize/create-password'
-const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/import-account'
-const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/import-with-seed-phrase'
-const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/unique-image'
-const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
-const INITIALIZE_BACKUP_PHRASE_ROUTE = '/initialize/backup-phrase'
-const INITIALIZE_CONFIRM_SEED_ROUTE = '/initialize/confirm-phrase'
-
-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,
- SETTINGS_ROUTE,
- INFO_ROUTE,
- REVEAL_SEED_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_CREATE_PASSWORD_ROUTE,
- INITIALIZE_IMPORT_ACCOUNT_ROUTE,
- INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
- INITIALIZE_UNIQUE_IMAGE_ROUTE,
- INITIALIZE_NOTICE_ROUTE,
- INITIALIZE_BACKUP_PHRASE_ROUTE,
- INITIALIZE_CONFIRM_SEED_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,
-}
diff --git a/ui/app/select-app.js b/ui/app/select-app.js
deleted file mode 100644
index f2e8e8d10..000000000
--- a/ui/app/select-app.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const { HashRouter } = require('react-router-dom')
-const App = require('./app')
-const OldApp = require('../../old-ui/app/app')
-const { autoAddToBetaUI } = require('./selectors')
-const { setFeatureFlag } = require('./actions')
-const I18nProvider = require('./i18n-provider')
-
-function mapStateToProps (state) {
- return {
- betaUI: state.metamask.featureFlags.betaUI,
- autoAdd: autoAddToBetaUI(state),
- isUnlocked: state.metamask.isUnlocked,
- isMascara: state.metamask.isMascara,
- firstTime: Object.keys(state.metamask.identities).length === 0,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setFeatureFlagWithModal: () => {
- return dispatch(setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
- },
- setFeatureFlagWithoutModal: () => {
- return dispatch(setFeatureFlag('betaUI', true))
- },
- }
-}
-module.exports = connect(mapStateToProps, mapDispatchToProps)(SelectedApp)
-
-inherits(SelectedApp, Component)
-function SelectedApp () {
- Component.call(this)
-}
-
-SelectedApp.prototype.componentWillReceiveProps = function (nextProps) {
- // Code commented out until we begin auto adding users to NewUI
- const {
- // isUnlocked,
- // setFeatureFlagWithModal,
- setFeatureFlagWithoutModal,
- isMascara,
- // firstTime,
- } = this.props
-
- // if (isMascara || firstTime) {
- if (isMascara) {
- setFeatureFlagWithoutModal()
- }
- // } else if (!isUnlocked && nextProps.isUnlocked && (nextProps.autoAdd)) {
- // setFeatureFlagWithModal()
- // }
-}
-
-SelectedApp.prototype.render = function () {
- // Code commented out until we begin auto adding users to NewUI
- // const { betaUI, isMascara, firstTime } = this.props
- // const Selected = betaUI || isMascara || firstTime ? App : OldApp
-
- const { betaUI, isMascara } = this.props
-
- return betaUI || isMascara
- ? h(HashRouter, {
- hashType: 'noslash',
- }, [
- h(I18nProvider, [ h(App) ]),
- ])
- : h(OldApp)
-}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
deleted file mode 100644
index 7209f19d1..000000000
--- a/ui/app/selectors.js
+++ /dev/null
@@ -1,207 +0,0 @@
-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,
- autoAddToBetaUI,
- getSendMaxModeState,
- getCurrentViewContext,
- getTotalUnapprovedCount,
- preferencesSelector,
-}
-
-module.exports = selectors
-
-function getSelectedAddress (state) {
- const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
-
- return selectedAddress
-}
-
-function getSelectedIdentity (state) {
- const selectedAddress = getSelectedAddress(state)
- const identities = state.metamask.identities
-
- return identities[selectedAddress]
-}
-
-function getSelectedAccount (state) {
- const accounts = state.metamask.accounts
- 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,
- 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 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 autoAddToBetaUI (state) {
- const autoAddTransactionThreshold = 12
- const autoAddAccountsThreshold = 2
- const autoAddTokensThreshold = 1
-
- const numberOfTransactions = state.metamask.selectedAddressTxList.length
- const numberOfAccounts = Object.keys(state.metamask.accounts).length
- const numberOfTokensAdded = state.metamask.tokens.length
-
- const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
- (numberOfAccounts > autoAddAccountsThreshold) &&
- (numberOfTokensAdded > autoAddTokensThreshold)
- const userIsNotInBeta = !state.metamask.featureFlags.betaUI
-
- return userIsNotInBeta && userPassesThreshold
-}
-
-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 preferencesSelector ({ metamask }) {
- return metamask.preferences
-}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index 90924c036..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
@@ -95,7 +95,7 @@ export const currentCurrencySelector = state => state.metamask.currentCurrency
export const conversionRateSelector = state => state.metamask.conversionRate
export const getNativeCurrency = state => state.metamask.nativeCurrency
-const txDataSelector = state => state.confirmTransaction.txData
+export const txDataSelector = state => state.confirmTransaction.txData
const tokenDataSelector = state => state.confirmTransaction.tokenData
const tokenPropsSelector = state => state.confirmTransaction.tokenProps
@@ -137,11 +137,12 @@ export const tokenAmountAndToAddressSelector = createSelector(
const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE)
toAddress = toParam ? toParam.value : params[0].value
const value = valueParam ? Number(valueParam.value) : Number(params[1].value)
- tokenAmount = roundExponential(value)
if (tokenDecimals) {
- tokenAmount = calcTokenAmount(value, tokenDecimals)
+ tokenAmount = calcTokenAmount(value, tokenDecimals).toNumber()
}
+
+ tokenAmount = roundExponential(tokenAmount)
}
return {
@@ -163,7 +164,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector(
const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
if (tokenDecimals) {
- tokenAmount = calcTokenAmount(value, tokenDecimals)
+ tokenAmount = calcTokenAmount(value, tokenDecimals).toNumber()
}
tokenAmount = roundExponential(tokenAmount)
@@ -188,7 +189,7 @@ export const sendTokenTokenAmountAndToAddressSelector = createSelector(
let value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
if (tokenDecimals) {
- value = calcTokenAmount(value, tokenDecimals)
+ value = calcTokenAmount(value, tokenDecimals).toNumber()
}
tokenAmount = roundExponential(value)
diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js
new file mode 100644
index 000000000..ecffb37ca
--- /dev/null
+++ b/ui/app/selectors/custom-gas.js
@@ -0,0 +1,325 @@
+import { pipe, partialRight } from 'ramda'
+import {
+ conversionUtil,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} from '../helpers/utils/conversion-util'
+import {
+ getCurrentCurrency, getIsMainnet, preferencesSelector,
+} from './selectors'
+import {
+ formatCurrency,
+} from '../helpers/utils/confirm-tx.util'
+import {
+ decEthToConvertedCurrency as ethTotalToConvertedCurrency,
+} from '../helpers/utils/conversions.util'
+import {
+ formatETHFee,
+} from '../helpers/utils/formatters'
+import {
+ calcGasTotal,
+} from '../components/app/send/send.utils'
+import { addHexPrefix } from 'ethereumjs-util'
+
+const selectors = {
+ formatTimeEstimate,
+ getAveragePriceEstimateInHexWEI,
+ getFastPriceEstimateInHexWEI,
+ getBasicGasEstimateLoadingStatus,
+ getBasicGasEstimateBlockTime,
+ getCustomGasErrors,
+ getCustomGasLimit,
+ getCustomGasPrice,
+ getCustomGasTotal,
+ getDefaultActiveButtonIndex,
+ getEstimatedGasPrices,
+ getEstimatedGasTimes,
+ getGasEstimatesLoadingStatus,
+ getPriceAndTimeEstimates,
+ getRenderableBasicEstimateData,
+ getRenderableEstimateDataForSmallButtonsFromGWEI,
+ priceEstimateToWei,
+ getSafeLowEstimate,
+ isCustomPriceSafe,
+}
+
+module.exports = selectors
+
+const NUMBER_OF_DECIMALS_SM_BTNS = 5
+
+function getCustomGasErrors (state) {
+ return state.gas.errors
+}
+
+function getCustomGasLimit (state) {
+ return state.gas.customData.limit
+}
+
+function getCustomGasPrice (state) {
+ return state.gas.customData.price
+}
+
+function getCustomGasTotal (state) {
+ return state.gas.customData.total
+}
+
+function getBasicGasEstimateLoadingStatus (state) {
+ return state.gas.basicEstimateIsLoading
+}
+
+function getGasEstimatesLoadingStatus (state) {
+ return state.gas.gasEstimatesLoading
+}
+
+function getPriceAndTimeEstimates (state) {
+ return state.gas.priceAndTimeEstimates
+}
+
+function getEstimatedGasPrices (state) {
+ return getPriceAndTimeEstimates(state).map(({ gasprice }) => gasprice)
+}
+
+function getEstimatedGasTimes (state) {
+ return getPriceAndTimeEstimates(state).map(({ expectedTime }) => expectedTime)
+}
+
+function getAveragePriceEstimateInHexWEI (state) {
+ const averagePriceEstimate = state.gas.basicEstimates.average
+ return getGasPriceInHexWei(averagePriceEstimate || '0x0')
+}
+
+function getFastPriceEstimateInHexWEI (state) {
+ const fastPriceEstimate = state.gas.basicEstimates.fast
+ return getGasPriceInHexWei(fastPriceEstimate || '0x0')
+}
+
+function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) {
+ return gasButtonInfo.findIndex(({ priceInHexWei }) => {
+ return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice)
+ })
+}
+
+function getSafeLowEstimate (state) {
+ const {
+ gas: {
+ basicEstimates: {
+ safeLow,
+ },
+ },
+ } = state
+
+ return safeLow
+}
+
+function isCustomPriceSafe (state) {
+ const safeLow = getSafeLowEstimate(state)
+ const customGasPrice = getCustomGasPrice(state)
+
+ if (!customGasPrice) {
+ return true
+ }
+
+ const customPriceSafe = conversionGreaterThan(
+ {
+ value: customGasPrice,
+ fromNumericBase: 'hex',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ },
+ { value: safeLow, fromNumericBase: 'dec' }
+ )
+
+ return customPriceSafe
+}
+
+function getBasicGasEstimateBlockTime (state) {
+ return state.gas.basicEstimates.blockTime
+}
+
+function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) {
+ return conversionUtil(calcGasTotal(gasLimit, estimate), {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'GWEI',
+ numberOfDecimals,
+ })
+}
+
+function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) {
+ return pipe(
+ x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
+ partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]),
+ formatETHFee
+ )(estimate, gasLimit)
+}
+
+
+function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) {
+ return pipe(
+ x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
+ partialRight(basicPriceEstimateToETHTotal, [gasLimit]),
+ partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]),
+ partialRight(formatCurrency, [convertedCurrency])
+ )(estimate, gasLimit, convertedCurrency, conversionRate)
+}
+
+function getTimeEstimateInSeconds (blockWaitEstimate) {
+ return multiplyCurrencies(blockWaitEstimate, 60, {
+ toNumericBase: 'dec',
+ multiplicandBase: 10,
+ multiplierBase: 10,
+ numberOfDecimals: 1,
+ })
+}
+
+function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) {
+ const minutes = Math.floor(totalSeconds / 60)
+ const seconds = Math.floor(totalSeconds % 60)
+
+ if (!minutes && !seconds) {
+ return '...'
+ }
+
+ let symbol = '~'
+ if (greaterThanMax) {
+ symbol = '< '
+ } else if (lessThanMin) {
+ symbol = '> '
+ }
+
+ const formattedMin = `${minutes ? minutes + ' min' : ''}`
+ const formattedSec = `${seconds ? seconds + ' sec' : ''}`
+ const formattedCombined = formattedMin && formattedSec
+ ? `${symbol}${formattedMin} ${formattedSec}`
+ : symbol + [formattedMin, formattedSec].find(t => t)
+
+ return formattedCombined
+}
+
+function getRenderableTimeEstimate (blockWaitEstimate) {
+ return pipe(
+ getTimeEstimateInSeconds,
+ formatTimeEstimate
+ )(blockWaitEstimate)
+}
+
+function priceEstimateToWei (priceEstimate) {
+ return conversionUtil(priceEstimate, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ numberOfDecimals: 9,
+ })
+}
+
+function getGasPriceInHexWei (price) {
+ return pipe(
+ x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
+ priceEstimateToWei,
+ addHexPrefix
+ )(price)
+}
+
+function getRenderableBasicEstimateData (state, gasLimit) {
+ if (getBasicGasEstimateLoadingStatus(state)) {
+ return []
+ }
+
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const showFiat = (isMainnet || !!showFiatInTestnets)
+ const conversionRate = state.metamask.conversionRate
+ const currentCurrency = getCurrentCurrency(state)
+ const {
+ gas: {
+ basicEstimates: {
+ safeLow,
+ fast,
+ fastest,
+ safeLowWait,
+ fastestWait,
+ fastWait,
+ },
+ },
+ } = state
+
+ return [
+ {
+ labelKey: 'slow',
+ feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit),
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate)
+ : '',
+ timeEstimate: safeLowWait && getRenderableTimeEstimate(safeLowWait),
+ priceInHexWei: getGasPriceInHexWei(safeLow),
+ },
+ {
+ labelKey: 'average',
+ feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit),
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate)
+ : '',
+ timeEstimate: fastWait && getRenderableTimeEstimate(fastWait),
+ priceInHexWei: getGasPriceInHexWei(fast),
+ },
+ {
+ labelKey: 'fast',
+ feeInPrimaryCurrency: getRenderableEthFee(fastest, gasLimit),
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate)
+ : '',
+ timeEstimate: fastestWait && getRenderableTimeEstimate(fastestWait),
+ priceInHexWei: getGasPriceInHexWei(fastest),
+ },
+ ]
+}
+
+function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {
+ if (getBasicGasEstimateLoadingStatus(state)) {
+ return []
+ }
+
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const showFiat = (isMainnet || !!showFiatInTestnets)
+ const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state) || '0x5208'
+ const conversionRate = state.metamask.conversionRate
+ const currentCurrency = getCurrentCurrency(state)
+ const {
+ gas: {
+ basicEstimates: {
+ safeLow,
+ fast,
+ fastest,
+ },
+ },
+ } = state
+
+ return [
+ {
+ labelKey: 'slow',
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate)
+ : '',
+ feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
+ priceInHexWei: getGasPriceInHexWei(safeLow, true),
+ },
+ {
+ labelKey: 'average',
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate)
+ : '',
+ feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
+ priceInHexWei: getGasPriceInHexWei(fast, true),
+ },
+ {
+ labelKey: 'fast',
+ feeInSecondaryCurrency: showFiat
+ ? getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate)
+ : '',
+ feeInPrimaryCurrency: getRenderableEthFee(fastest, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
+ priceInHexWei: getGasPriceInHexWei(fastest, true),
+ },
+ ]
+}
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/transactions.js b/ui/app/selectors/transactions.js
index 479002794..b1d27b333 100644
--- a/ui/app/selectors/transactions.js
+++ b/ui/app/selectors/transactions.js
@@ -1,16 +1,44 @@
import { createSelector } from 'reselect'
-import { valuesFor } from '../util'
import {
UNAPPROVED_STATUS,
APPROVED_STATUS,
SUBMITTED_STATUS,
-} from '../constants/transactions'
+ CONFIRMED_STATUS,
+} from '../helpers/constants/transactions'
+import {
+ TRANSACTION_TYPE_CANCEL,
+ TRANSACTION_TYPE_RETRY,
+} from '../../../app/scripts/controllers/transactions/enums'
+import { hexToDecimal } from '../helpers/utils/conversions.util'
import { selectedTokenAddressSelector } from './tokens'
+import txHelper from '../../lib/tx-helper'
export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
+export const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs
+export const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages
+export const networkSelector = state => state.metamask.network
+
+export const unapprovedMessagesSelector = createSelector(
+ unapprovedMsgsSelector,
+ unapprovedPersonalMsgsSelector,
+ unapprovedTypedMessagesSelector,
+ networkSelector,
+ (
+ unapprovedMsgs = {},
+ unapprovedPersonalMsgs = {},
+ unapprovedTypedMessages = {},
+ network
+ ) => txHelper(
+ {},
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ network
+ ) || []
+)
const pendingStatusHash = {
[UNAPPROVED_STATUS]: true,
@@ -18,14 +46,18 @@ const pendingStatusHash = {
[SUBMITTED_STATUS]: true,
}
+const priorityStatusHash = {
+ ...pendingStatusHash,
+ [CONFIRMED_STATUS]: true,
+}
+
export const transactionsSelector = createSelector(
selectedTokenAddressSelector,
- unapprovedMsgsSelector,
+ unapprovedMessagesSelector,
shapeShiftTxListSelector,
selectedAddressTxListSelector,
- (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
- const unapprovedMsgsList = valuesFor(unapprovedMsgs)
- const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
+ (selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], transactions = []) => {
+ const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList)
return selectedTokenAddress
? txsToRender
@@ -36,23 +68,219 @@ export const transactionsSelector = createSelector(
}
)
-export const pendingTransactionsSelector = createSelector(
+/**
+ * @name insertOrderedNonce
+ * @private
+ * @description Inserts (mutates) a nonce into an array of ordered nonces, sorted in ascending
+ * order.
+ * @param {string[]} nonces - Array of nonce strings in hex
+ * @param {string} nonceToInsert - Nonce string in hex to be inserted into the array of nonces.
+ * @returns {string[]}
+ */
+const insertOrderedNonce = (nonces, nonceToInsert) => {
+ let insertIndex = nonces.length
+
+ for (let i = 0; i < nonces.length; i++) {
+ const nonce = nonces[i]
+
+ if (Number(hexToDecimal(nonce)) > Number(hexToDecimal(nonceToInsert))) {
+ insertIndex = i
+ break
+ }
+ }
+
+ nonces.splice(insertIndex, 0, nonceToInsert)
+}
+
+/**
+ * @name insertTransactionByTime
+ * @private
+ * @description Inserts (mutates) a transaction object into an array of ordered transactions, sorted
+ * in ascending order by time.
+ * @param {Object[]} transactions - Array of transaction objects.
+ * @param {Object} transaction - Transaction object to be inserted into the array of transactions.
+ * @returns {Object[]}
+ */
+const insertTransactionByTime = (transactions, transaction) => {
+ const { time } = transaction
+
+ let insertIndex = transactions.length
+
+ for (let i = 0; i < transactions.length; i++) {
+ const tx = transactions[i]
+
+ if (tx.time > time) {
+ insertIndex = i
+ break
+ }
+ }
+
+ transactions.splice(insertIndex, 0, transaction)
+}
+
+/**
+ * Contains transactions and properties associated with those transactions of the same nonce.
+ * @typedef {Object} transactionGroup
+ * @property {string} nonce - The nonce that the transactions within this transactionGroup share.
+ * @property {Object[]} transactions - An array of transaction (txMeta) objects.
+ * @property {Object} initialTransaction - The transaction (txMeta) with the lowest "time".
+ * @property {Object} primaryTransaction - Either the latest transaction or the confirmed
+ * transaction.
+ * @property {boolean} hasRetried - True if a transaction in the group was a retry transaction.
+ * @property {boolean} hasCancelled - True if a transaction in the group was a cancel transaction.
+ */
+
+/**
+ * @name insertTransactionGroupByTime
+ * @private
+ * @description Inserts (mutates) a transactionGroup object into an array of ordered
+ * transactionGroups, sorted in ascending order by nonce.
+ * @param {transactionGroup[]} transactionGroups - Array of transactionGroup objects.
+ * @param {transactionGroup} transactionGroup - transactionGroup object to be inserted into the
+ * array of transactionGroups.
+ */
+const insertTransactionGroupByTime = (transactionGroups, transactionGroup) => {
+ const { primaryTransaction: { time: groupToInsertTime } = {} } = transactionGroup
+
+ let insertIndex = transactionGroups.length
+
+ for (let i = 0; i < transactionGroups.length; i++) {
+ const txGroup = transactionGroups[i]
+ const { primaryTransaction: { time } = {} } = txGroup
+
+ if (time > groupToInsertTime) {
+ insertIndex = i
+ break
+ }
+ }
+
+ transactionGroups.splice(insertIndex, 0, transactionGroup)
+}
+
+/**
+ * @name mergeShapeshiftTransactionGroups
+ * @private
+ * @description Inserts (mutates) shapeshift transactionGroups into an array of nonce-ordered
+ * transactionGroups by time. Shapeshift transactionGroups need to be sorted by time within the list
+ * of transactions as they do not have nonces.
+ * @param {transactionGroup[]} orderedTransactionGroups - Array of transactionGroups ordered by
+ * nonce.
+ * @param {transactionGroup[]} shapeshiftTransactionGroups - Array of shapeshift transactionGroups
+ */
+const mergeShapeshiftTransactionGroups = (orderedTransactionGroups, shapeshiftTransactionGroups) => {
+ shapeshiftTransactionGroups.forEach(shapeshiftGroup => {
+ insertTransactionGroupByTime(orderedTransactionGroups, shapeshiftGroup)
+ })
+}
+
+/**
+ * @name nonceSortedTransactionsSelector
+ * @description Returns an array of transactionGroups sorted by nonce in ascending order.
+ * @returns {transactionGroup[]}
+ */
+export const nonceSortedTransactionsSelector = createSelector(
transactionsSelector,
+ (transactions = []) => {
+ const unapprovedTransactionGroups = []
+ const shapeshiftTransactionGroups = []
+ const orderedNonces = []
+ const nonceToTransactionsMap = {}
+
+ transactions.forEach(transaction => {
+ const { txParams: { nonce } = {}, status, type, time: txTime, key } = transaction
+
+ if (typeof nonce === 'undefined') {
+ const transactionGroup = {
+ transactions: [transaction],
+ initialTransaction: transaction,
+ primaryTransaction: transaction,
+ hasRetried: false,
+ hasCancelled: false,
+ }
+
+ if (key === 'shapeshift') {
+ shapeshiftTransactionGroups.push(transactionGroup)
+ } else {
+ insertTransactionGroupByTime(unapprovedTransactionGroups, transactionGroup)
+ }
+ } else if (nonce in nonceToTransactionsMap) {
+ const nonceProps = nonceToTransactionsMap[nonce]
+ insertTransactionByTime(nonceProps.transactions, transaction)
+
+ if (status in priorityStatusHash) {
+ const { primaryTransaction: { time: primaryTxTime = 0 } = {} } = nonceProps
+
+ if (status === CONFIRMED_STATUS || txTime > primaryTxTime) {
+ nonceProps.primaryTransaction = transaction
+ }
+ }
+
+ const { initialTransaction: { time: initialTxTime = 0 } = {} } = nonceProps
+
+ // Used to display the transaction action, since we don't want to overwrite the action if
+ // it was replaced with a cancel attempt transaction.
+ if (txTime < initialTxTime) {
+ nonceProps.initialTransaction = transaction
+ }
+
+ if (type === TRANSACTION_TYPE_RETRY) {
+ nonceProps.hasRetried = true
+ }
+
+ if (type === TRANSACTION_TYPE_CANCEL) {
+ nonceProps.hasCancelled = true
+ }
+ } else {
+ nonceToTransactionsMap[nonce] = {
+ nonce,
+ transactions: [transaction],
+ initialTransaction: transaction,
+ primaryTransaction: transaction,
+ hasRetried: transaction.type === TRANSACTION_TYPE_RETRY,
+ hasCancelled: transaction.type === TRANSACTION_TYPE_CANCEL,
+ }
+
+ insertOrderedNonce(orderedNonces, nonce)
+ }
+ })
+
+ const orderedTransactionGroups = orderedNonces.map(nonce => nonceToTransactionsMap[nonce])
+ mergeShapeshiftTransactionGroups(orderedTransactionGroups, shapeshiftTransactionGroups)
+ return unapprovedTransactionGroups.concat(orderedTransactionGroups)
+ }
+)
+
+/**
+ * @name nonceSortedPendingTransactionsSelector
+ * @description Returns an array of transactionGroups where transactions are still pending sorted by
+ * nonce in descending order.
+ * @returns {transactionGroup[]}
+ */
+export const nonceSortedPendingTransactionsSelector = createSelector(
+ nonceSortedTransactionsSelector,
(transactions = []) => (
- transactions.filter(transaction => transaction.status in pendingStatusHash).reverse()
+ transactions.filter(({ primaryTransaction }) => primaryTransaction.status in pendingStatusHash)
)
)
-export const submittedPendingTransactionsSelector = createSelector(
- transactionsSelector,
+/**
+ * @name nonceSortedCompletedTransactionsSelector
+ * @description Returns an array of transactionGroups where transactions are confirmed sorted by
+ * nonce in descending order.
+ * @returns {transactionGroup[]}
+ */
+export const nonceSortedCompletedTransactionsSelector = createSelector(
+ nonceSortedTransactionsSelector,
(transactions = []) => (
- transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
+ transactions
+ .filter(({ primaryTransaction }) => !(primaryTransaction.status in pendingStatusHash))
+ .reverse()
)
)
-export const completedTransactionsSelector = createSelector(
+export const submittedPendingTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
- transactions.filter(transaction => !(transaction.status in pendingStatusHash))
+ transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
)
)
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..b2aa28c93
--- /dev/null
+++ b/ui/app/store/actions.js
@@ -0,0 +1,2765 @@
+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.completeOnboarding(err => {
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.completeOnboarding())
+ 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/token-util.js b/ui/app/token-util.js
deleted file mode 100644
index 6e4992763..000000000
--- a/ui/app/token-util.js
+++ /dev/null
@@ -1,118 +0,0 @@
-const log = require('loglevel')
-const util = require('./util')
-const BigNumber = require('bignumber.js')
-import contractMap from 'eth-contract-metadata'
-
-const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
- return {
- ...acc,
- [base.toLowerCase()]: contractMap[base],
- }
-}, {})
-
-const DEFAULT_SYMBOL = ''
-const DEFAULT_DECIMALS = '0'
-
-async function getSymbolFromContract (tokenAddress) {
- const token = util.getContractAtAddress(tokenAddress)
-
- try {
- const result = await token.symbol()
- return result[0]
- } catch (error) {
- log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error)
- }
-}
-
-async function getDecimalsFromContract (tokenAddress) {
- const token = util.getContractAtAddress(tokenAddress)
-
- try {
- const result = await token.decimals()
- const decimalsBN = result[0]
- return decimalsBN && decimalsBN.toString()
- } catch (error) {
- log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error)
- }
-}
-
-function getContractMetadata (tokenAddress) {
- return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]
-}
-
-async function getSymbol (tokenAddress) {
- let symbol = await getSymbolFromContract(tokenAddress)
-
- if (!symbol) {
- const contractMetadataInfo = getContractMetadata(tokenAddress)
-
- if (contractMetadataInfo) {
- symbol = contractMetadataInfo.symbol
- }
- }
-
- return symbol
-}
-
-async function getDecimals (tokenAddress) {
- let decimals = await getDecimalsFromContract(tokenAddress)
-
- if (!decimals || decimals === '0') {
- const contractMetadataInfo = getContractMetadata(tokenAddress)
-
- if (contractMetadataInfo) {
- decimals = contractMetadataInfo.decimals
- }
- }
-
- return decimals
-}
-
-export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
- const existingToken = existingTokens.find(({ address }) => tokenAddress === address)
-
- if (existingToken) {
- return {
- symbol: existingToken.symbol,
- decimals: existingToken.decimals,
- }
- }
-
- let symbol, decimals
-
- try {
- symbol = await getSymbol(tokenAddress)
- decimals = await getDecimals(tokenAddress)
- } catch (error) {
- log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error)
- }
-
- return {
- symbol: symbol || DEFAULT_SYMBOL,
- decimals: decimals || DEFAULT_DECIMALS,
- }
-}
-
-export function tokenInfoGetter () {
- const tokens = {}
-
- return async (address) => {
- if (tokens[address]) {
- return tokens[address]
- }
-
- tokens[address] = await getSymbolAndDecimals(address)
-
- return tokens[address]
- }
-}
-
-export function calcTokenAmount (value, decimals) {
- const multiplier = Math.pow(10, Number(decimals || 0))
- return new BigNumber(String(value)).div(multiplier).toNumber()
-}
-
-export function getTokenValue (tokenParams = []) {
- const valueData = tokenParams.find(param => param.name === '_value')
- return valueData && valueData.value
-}
diff --git a/ui/app/util.js b/ui/app/util.js
deleted file mode 100644
index b19a028cc..000000000
--- a/ui/app/util.js
+++ /dev/null
@@ -1,314 +0,0 @@
-const abi = require('human-standard-token-abi')
-const ethUtil = require('ethereumjs-util')
-const hexToBn = require('../../app/scripts/lib/hex-to-bn')
-const vreme = new (require('vreme'))()
-
-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) {
- return vreme.format(new Date(date), '3/16/2014 at 14:30')
-}
-
-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,
-}
-
-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) {
- 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
- * @returns {String} - checksummed address
- */
-function checksumAddress (address) {
- return address ? ethUtil.toChecksumAddress(address) : ''
-}
-
-function addressSlicer (address = '') {
- if (address.length < 11) {
- return address
- }
-
- return `${address.slice(0, 6)}...${address.slice(-4)}`
-}
diff --git a/ui/app/welcome-screen.js b/ui/app/welcome-screen.js
deleted file mode 100644
index 63512cd50..000000000
--- a/ui/app/welcome-screen.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import EventEmitter from 'events'
-import h from 'react-hyperscript'
-import { Component } from 'react'
-import PropTypes from 'prop-types'
-import {connect} from 'react-redux'
-import { withRouter } from 'react-router-dom'
-import { compose } from 'recompose'
-import {closeWelcomeScreen} from './actions'
-import Mascot from './components/mascot'
-import { INITIALIZE_CREATE_PASSWORD_ROUTE } from './routes'
-
-class WelcomeScreen extends Component {
- static propTypes = {
- closeWelcomeScreen: PropTypes.func.isRequired,
- welcomeScreenSeen: PropTypes.bool,
- history: PropTypes.object,
- t: PropTypes.func,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- constructor (props) {
- super(props)
- this.animationEventEmitter = new EventEmitter()
- }
-
- componentWillMount () {
- const { history, welcomeScreenSeen } = this.props
-
- if (welcomeScreenSeen) {
- history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
- }
- }
-
- initiateAccountCreation = () => {
- this.props.closeWelcomeScreen()
- this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
- }
-
- render () {
- return h('div.welcome-screen', [
-
- h('div.welcome-screen__info', [
-
- h(Mascot, {
- animationEventEmitter: this.animationEventEmitter,
- width: '225',
- height: '225',
- }),
-
- h('div.welcome-screen__info__header', this.context.t('welcomeBeta')),
-
- h('div.welcome-screen__info__copy', this.context.t('metamaskDescription')),
-
- h('div.welcome-screen__info__copy', this.context.t('holdEther')),
-
- h('button.welcome-screen__button', {
- onClick: this.initiateAccountCreation,
- }, this.context.t('continue')),
-
- ]),
-
- ])
- }
-}
-
-const mapStateToProps = ({ metamask: { welcomeScreenSeen } }) => {
- return {
- welcomeScreenSeen,
- }
-}
-
-export default compose(
- withRouter,
- connect(
- mapStateToProps,
- dispatch => ({
- closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
- })
- )
-)(WelcomeScreen)
diff --git a/ui/css.js b/ui/css.js
index 0d0f60806..d8f954434 100644
--- a/ui/css.js
+++ b/ui/css.js
@@ -5,7 +5,6 @@ module.exports = bundleCss
var cssFiles = {
'index.css': fs.readFileSync(path.join(__dirname, '/app/css/output/index.css'), 'utf8'),
- 'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'),
'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'),
'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'),
}
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/local-storage-helpers.js b/ui/lib/local-storage-helpers.js
new file mode 100644
index 000000000..287586c49
--- /dev/null
+++ b/ui/lib/local-storage-helpers.js
@@ -0,0 +1,20 @@
+export function loadLocalStorageData (itemKey) {
+ try {
+ const serializedData = localStorage.getItem(itemKey)
+ if (serializedData === null) {
+ return undefined
+ }
+ return JSON.parse(serializedData)
+ } catch (err) {
+ return undefined
+ }
+}
+
+export function saveLocalStorageData (data, itemKey) {
+ try {
+ const serializedData = JSON.stringify(data)
+ localStorage.setItem(itemKey, serializedData)
+ } catch (err) {
+ console.warn(err)
+ }
+}
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/shallow-with-context.js b/ui/lib/shallow-with-context.js
new file mode 100644
index 000000000..cf83dd76e
--- /dev/null
+++ b/ui/lib/shallow-with-context.js
@@ -0,0 +1,7 @@
+import { shallow } from 'enzyme'
+
+export default function (jsxComponent) {
+ return shallow(jsxComponent, {
+ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 },
+ })
+}
diff --git a/ui/lib/test-timeout.js b/ui/lib/test-timeout.js
new file mode 100644
index 000000000..957b0fce2
--- /dev/null
+++ b/ui/lib/test-timeout.js
@@ -0,0 +1,5 @@
+export default function timeout (time) {
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, time || 1500)
+ })
+}
diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js
index 0a6f55a63..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) {
@@ -21,7 +21,7 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMes
allValues = allValues.concat(typedValues)
allValues = allValues.sort((a, b) => {
- return a.time > b.time
+ return a.time - b.time
})
return allValues