From e1af6312cea0d2b07b7b3a4fae4c4ca5857a03eb Mon Sep 17 00:00:00 2001 From: fixanoid Date: Thu, 21 Mar 2019 10:57:26 -0400 Subject: Fixing spelling of Ethereum in MetaMetrics copy (#6329) --- .../metametrics-opt-in-modal/metametrics-opt-in-modal.component.js | 2 +- .../first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js index de3d1d3a3..1a224eb12 100644 --- a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js +++ b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js @@ -40,7 +40,7 @@ export default class MetaMetricsOptInModal extends Component {
MetaMask would like to gather usage data to better understand how our users interact with the extension. This data - will be used to continually improve the usability and user experience of our product and the etheruem ecosystem. + will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
MetaMask will.. diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index 58a03944e..2b4af27ad 100644 --- a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -48,7 +48,7 @@ export default class MetaMetricsOptIn extends Component {
MetaMask would like to gather usage data to better understand how our users interact with the extension. This data - will be used to continually improve the usability and user experience of our product and the Etheruem ecosystem. + will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
MetaMask will.. -- cgit From 2aaec1d9fbaa340ee9971278b6fafd1ad49bdbf2 Mon Sep 17 00:00:00 2001 From: Josh Stevens Date: Thu, 21 Mar 2019 19:42:23 +0000 Subject: Stop reloading dapps on network change allowing dapps to decide if it should refresh or not (#6330) * feat: `inpageProvider.autoRefreshOnNetworkChange` to allow dapps to control if it refreshes or not * feat: check the `autoRefreshOnNetworkChange` before a refresh * fix linting error * fix: use `window.ethereum` now `web3.ethereum` --- app/scripts/inpage.js | 4 ++++ app/scripts/lib/auto-reload.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index c7f0c5669..68394d1ae 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -91,6 +91,10 @@ inpageProvider.enable = function ({ force } = {}) { }) } +// give the dapps control of a refresh they can toggle this off on the window.ethereum +// this will be default true so it does not break any old apps. +inpageProvider.autoRefreshOnNetworkChange = true + // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 558391a06..44fbe847c 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -20,6 +20,10 @@ function setupDappAutoReload (web3, observable) { }) observable.subscribe(function (state) { + // if the auto refresh on network change is false do not + // do anything + if (!window.ethereum.autoRefreshOnNetworkChange) return + // if reload in progress, no need to check reload logic if (reloadInProgress) return -- cgit From 7287133e15fab22299e07704206e85bc855d1064 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri Date: Thu, 21 Mar 2019 15:43:10 -0400 Subject: Enable mobile sync (#6332) * enable mobile sync * remove mobile sync as a preference * Fix typo --- docs/secret-preferences.md | 2 +- .../pages/settings/settings-tab/settings-tab.component.js | 7 +------ .../pages/settings/settings-tab/settings-tab.container.js | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/secret-preferences.md b/docs/secret-preferences.md index f9d01a503..58a4554c4 100644 --- a/docs/secret-preferences.md +++ b/docs/secret-preferences.md @@ -6,5 +6,5 @@ One example is our "sync with mobile" feature, which didn't make sense to roll o To enable features like this, first open the background console, and then you can use the global method `global.setPreference(key, value)`. -For example, if the feature flag was a booelan was called `mobileSync`, you might type `setPreference('mobileSync', true)`. +For example, if the feature flag was a boolean was called `useNativeCurrencyAsPrimaryCurrency`, you might type `setPreference('useNativeCurrencyAsPrimaryCurrency', true)`. diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js index abddaaee8..ba2e81754 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js @@ -63,7 +63,6 @@ export default class SettingsTab extends PureComponent { setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, setAdvancedInlineGasFeatureFlag: PropTypes.func, advancedInlineGas: PropTypes.bool, - mobileSync: PropTypes.bool, showFiatInTestnets: PropTypes.bool, setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, participateInMetaMetrics: PropTypes.bool, @@ -378,11 +377,7 @@ export default class SettingsTab extends PureComponent { renderMobileSync () { const { t } = this.context - const { history, mobileSync } = this.props - - if (!mobileSync) { - return - } + const { history } = this.props return (
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 index 64c256412..e266c8c9a 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js @@ -28,7 +28,6 @@ const mapStateToProps = state => { sendHexData, privacyMode, advancedInlineGas, - mobileSync, } = {}, provider = {}, currentLocale, @@ -48,7 +47,6 @@ const mapStateToProps = state => { privacyMode, provider, useNativeCurrencyAsPrimaryCurrency, - mobileSync, showFiatInTestnets, participateInMetaMetrics, } -- cgit From 31175625b446cb5d18b17db23018bca8b14d280c Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 21 Mar 2019 16:03:30 -0700 Subject: Folder restructure (#6304) * Remove ui/app/keychains/ * Remove ui/app/img/ (unused images) * Move conversion-util to helpers/utils/ * Move token-util to helpers/utils/ * Move /helpers/*.js inside /helpers/utils/ * Move util tests inside /helpers/utils/ * Renameand move confirm-transaction/util.js to helpers/utils/ * Move higher-order-components to helpers/higher-order-components/ * Move infura-conversion.json to helpers/constants/ * Move all utility functions to helpers/utils/ * Move pages directory to top-level * Move all constants to helpers/constants/ * Move metametrics inside helpers/ * Move app and root inside pages/ * Move routes inside helpers/ * Re-organize ducks/ * Move reducers to ducks/ * Move selectors inside selectors/ * Move test out of test folder * Move action, reducer, store inside store/ * Move ui components inside ui/ * Move UI components inside ui/ * Move connected components inside components/app/ * Move i18n-helper inside helpers/ * Fix unit tests * Fix unit test * Move pages components * Rename routes component * Move reducers to ducks/index * Fix bad path in unit test --- .../controllers/transactions/tx-gas-utils.js | 2 +- development/mock-dev.js | 6 +- development/ui-dev.js | 2 +- development/uiStore.js | 2 +- docs/adding-new-networks.md | 2 +- mascara/src/app/buy-ether-widget/index.js | 2 +- mascara/src/app/shapeshift-form/index.js | 4 +- test/unit/actions/config_test.js | 4 +- test/unit/actions/set_account_label_test.js | 4 +- test/unit/actions/set_selected_account_test.js | 4 +- test/unit/actions/tx_test.js | 2 +- test/unit/actions/view_info_test.js | 4 +- test/unit/actions/warning_test.js | 4 +- test/unit/balance-formatter-test.js | 2 +- test/unit/reducers/unlock_vault_test.js | 4 +- test/unit/responsive/components/dropdown-test.js | 2 +- test/unit/ui/app/actions.spec.js | 2 +- test/unit/ui/app/components/token-cell.spec.js | 4 +- test/unit/ui/app/reducers/app.spec.js | 4 +- test/unit/ui/app/reducers/metamask.spec.js | 4 +- test/unit/ui/app/selectors.spec.js | 2 +- test/unit/util_test.js | 2 +- ui/.gitignore | 66 - ui/app/accounts/new-account/index.js | 87 - ui/app/actions.js | 2761 -------------------- ui/app/app.js | 441 ---- .../account-dropdown-mini.component.js | 84 - ui/app/components/account-dropdown-mini/index.js | 1 - .../tests/account-dropdown-mini.component.test.js | 107 - ui/app/components/account-dropdowns.js | 338 --- .../account-menu/account-menu.component.js | 340 --- .../account-menu/account-menu.container.js | 62 - ui/app/components/account-menu/index.js | 1 - ui/app/components/account-menu/index.scss | 177 -- ui/app/components/account-panel.js | 86 - .../add-token-button/add-token-button.component.js | 34 - ui/app/components/add-token-button/index.js | 1 - ui/app/components/add-token-button/index.scss | 26 - ui/app/components/alert/index.js | 62 - .../components/app-header/app-header.component.js | 127 - .../components/app-header/app-header.container.js | 40 - ui/app/components/app-header/index.js | 1 - ui/app/components/app-header/index.scss | 90 - ui/app/components/app/account-dropdowns.js | 338 +++ .../app/account-menu/account-menu.component.js | 340 +++ .../app/account-menu/account-menu.container.js | 62 + ui/app/components/app/account-menu/index.js | 1 + ui/app/components/app/account-menu/index.scss | 177 ++ ui/app/components/app/account-panel.js | 86 + .../add-token-button/add-token-button.component.js | 34 + ui/app/components/app/add-token-button/index.js | 1 + ui/app/components/app/add-token-button/index.scss | 26 + .../app/app-header/app-header.component.js | 127 + .../app/app-header/app-header.container.js | 40 + ui/app/components/app/app-header/index.js | 1 + ui/app/components/app/app-header/index.scss | 90 + ui/app/components/app/bn-as-decimal-input.js | 188 ++ ui/app/components/app/coinbase-form.js | 69 + .../confirm-detail-row.component.js | 84 + .../confirm-detail-row/index.js | 1 + .../confirm-detail-row/index.scss | 50 + .../tests/confirm-detail-row.component.test.js | 64 + .../confirm-page-container-content.component.js | 110 + .../confirm-page-container-summary.component.js | 71 + .../confirm-page-container-summary/index.js | 1 + .../confirm-page-container-summary/index.scss | 54 + .../confirm-page-container-warning.component.js | 22 + .../confirm-page-container-warning/index.js | 1 + .../confirm-page-container-warning/index.scss | 18 + .../confirm-page-container-content/index.js | 3 + .../confirm-page-container-content/index.scss | 68 + .../confirm-page-container-header.component.js | 63 + .../confirm-page-container-header/index.js | 1 + .../confirm-page-container-header/index.scss | 27 + .../confirm-page-container-navigation.component.js | 69 + .../confirm-page-container-navigation/index.js | 1 + .../confirm-page-container-navigation/index.scss | 54 + .../confirm-page-container.component.js | 170 ++ .../components/app/confirm-page-container/index.js | 10 + .../app/confirm-page-container/index.scss | 7 + ui/app/components/app/copyable.js | 53 + .../app/customize-gas-modal/gas-modal-card.js | 54 + .../app/customize-gas-modal/gas-slider.js | 50 + ui/app/components/app/customize-gas-modal/index.js | 396 +++ .../app/dropdowns/account-details-dropdown.js | 131 + .../app/dropdowns/components/account-dropdowns.js | 473 ++++ .../app/dropdowns/components/dropdown.js | 112 + ui/app/components/app/dropdowns/components/menu.js | 51 + .../dropdowns/components/network-dropdown-icon.js | 47 + ui/app/components/app/dropdowns/index.js | 11 + .../components/app/dropdowns/network-dropdown.js | 378 +++ ui/app/components/app/dropdowns/simple-dropdown.js | 92 + .../app/dropdowns/tests/dropdown.test.js | 37 + ui/app/components/app/dropdowns/tests/menu.test.js | 87 + .../dropdowns/tests/network-dropdown-icon.test.js | 25 + .../app/dropdowns/tests/network-dropdown.test.js | 97 + .../app/dropdowns/token-menu-dropdown.js | 68 + ui/app/components/app/ens-input.js | 181 ++ .../advanced-gas-inputs.component.js | 156 ++ .../advanced-gas-inputs.container.js | 38 + .../gas-customization/advanced-gas-inputs/index.js | 1 + .../advanced-gas-inputs/index.scss | 133 + .../advanced-tab-content.component.js | 219 ++ .../advanced-tab-content/index.js | 1 + .../advanced-tab-content/index.scss | 203 ++ .../tests/advanced-tab-content-component.test.js | 364 +++ .../advanced-tab-content/time-remaining/index.js | 1 + .../advanced-tab-content/time-remaining/index.scss | 17 + .../tests/time-remaining-component.test.js | 30 + .../time-remaining/time-remaining.component.js | 33 + .../time-remaining/time-remaining.utils.js | 11 + .../basic-tab-content.component.js | 35 + .../basic-tab-content/index.js | 1 + .../basic-tab-content/index.scss | 28 + .../tests/basic-tab-content-component.test.js | 82 + .../gas-modal-page-container.component.js | 186 ++ .../gas-modal-page-container.container.js | 291 +++ .../gas-modal-page-container/index.js | 1 + .../gas-modal-page-container/index.scss | 146 ++ .../gas-modal-page-container-component.test.js | 274 ++ .../gas-modal-page-container-container.test.js | 425 +++ .../gas-price-button-group.component.js | 89 + .../gas-price-button-group/index.js | 1 + .../gas-price-button-group/index.scss | 238 ++ .../tests/gas-price-button-group-component.test.js | 233 ++ .../gas-price-chart/gas-price-chart.component.js | 108 + .../gas-price-chart/gas-price-chart.utils.js | 354 +++ .../app/gas-customization/gas-price-chart/index.js | 1 + .../gas-customization/gas-price-chart/index.scss | 132 + .../tests/gas-price-chart.component.test.js | 218 ++ .../gas-slider/gas-slider.component.js | 48 + .../app/gas-customization/gas-slider/index.js | 1 + .../app/gas-customization/gas-slider/index.scss | 54 + .../app/gas-customization/gas.selectors.js | 14 + ui/app/components/app/gas-customization/index.scss | 7 + ui/app/components/app/index.scss | 81 + ui/app/components/app/info-box/index.js | 2 + ui/app/components/app/info-box/index.scss | 24 + .../components/app/info-box/info-box.component.js | 49 + ui/app/components/app/input-number.js | 81 + .../components/app/loading-network-screen/index.js | 1 + .../loading-network-screen.component.js | 138 + .../loading-network-screen.container.js | 41 + ui/app/components/app/menu-bar/index.js | 1 + ui/app/components/app/menu-bar/index.scss | 23 + .../components/app/menu-bar/menu-bar.component.js | 79 + .../components/app/menu-bar/menu-bar.container.js | 26 + ui/app/components/app/menu-droppo.js | 134 + ui/app/components/app/modal/index.js | 2 + ui/app/components/app/modal/index.scss | 62 + ui/app/components/app/modal/modal-content/index.js | 1 + .../components/app/modal/modal-content/index.scss | 19 + .../modal/modal-content/modal-content.component.js | 32 + .../tests/modal-content.component.test.js | 44 + ui/app/components/app/modal/modal.component.js | 83 + .../app/modal/tests/modal.component.test.js | 133 + .../components/app/modals/account-details-modal.js | 101 + .../app/modals/account-modal-container.js | 80 + ui/app/components/app/modals/buy-options-modal.js | 101 + .../cancel-transaction-gas-fee.component.js | 29 + .../cancel-transaction-gas-fee/index.js | 1 + .../cancel-transaction-gas-fee/index.scss | 17 + .../cancel-transaction-gas-fee.component.test.js | 26 + .../cancel-transaction.component.js | 76 + .../cancel-transaction.container.js | 60 + .../app/modals/cancel-transaction/index.js | 1 + .../app/modals/cancel-transaction/index.scss | 18 + .../tests/cancel-transaction.component.test.js | 57 + .../clear-approved-origins.component.js | 39 + .../clear-approved-origins.container.js | 16 + .../app/modals/clear-approved-origins/index.js | 1 + .../confirm-remove-account.component.js | 89 + .../confirm-remove-account.container.js | 22 + .../app/modals/confirm-remove-account/index.js | 1 + .../app/modals/confirm-remove-account/index.scss | 58 + .../confirm-reset-account.component.js | 38 + .../confirm-reset-account.container.js | 16 + .../app/modals/confirm-reset-account/index.js | 1 + .../customize-gas/customize-gas.component.js | 162 ++ .../customize-gas/customize-gas.container.js | 22 + .../app/modals/customize-gas/customize-gas.util.js | 34 + .../components/app/modals/customize-gas/index.js | 1 + .../components/app/modals/customize-gas/index.scss | 110 + .../components/app/modals/deposit-ether-modal.js | 220 ++ .../app/modals/edit-account-name-modal.js | 83 + .../app/modals/export-private-key-modal.js | 177 ++ .../app/modals/hide-token-confirmation-modal.js | 83 + ui/app/components/app/modals/index.js | 5 + ui/app/components/app/modals/index.scss | 11 + .../app/modals/loading-network-error/index.js | 1 + .../loading-network-error.component.js | 29 + .../loading-network-error.container.js | 4 + .../app/modals/metametrics-opt-in-modal/index.js | 1 + .../app/modals/metametrics-opt-in-modal/index.scss | 30 + .../metametrics-opt-in-modal.component.js | 141 + .../metametrics-opt-in-modal.container.js | 24 + ui/app/components/app/modals/modal.js | 511 ++++ ui/app/components/app/modals/new-account-modal.js | 112 + ui/app/components/app/modals/notification-modal.js | 81 + ui/app/components/app/modals/qr-scanner/index.js | 2 + ui/app/components/app/modals/qr-scanner/index.scss | 83 + .../app/modals/qr-scanner/qr-scanner.component.js | 216 ++ .../app/modals/qr-scanner/qr-scanner.container.js | 24 + .../app/modals/reject-transactions/index.js | 1 + .../app/modals/reject-transactions/index.scss | 6 + .../reject-transactions.component.js | 45 + .../reject-transactions.container.js | 17 + .../app/modals/shapeshift-deposit-tx-modal.js | 40 + .../app/modals/transaction-confirmed/index.js | 1 + .../app/modals/transaction-confirmed/index.scss | 22 + .../transaction-confirmed.component.js | 45 + .../transaction-confirmed.container.js | 4 + ui/app/components/app/network-display/index.js | 2 + ui/app/components/app/network-display/index.scss | 57 + .../network-display/network-display.component.js | 76 + .../network-display/network-display.container.js | 11 + ui/app/components/app/network.js | 149 ++ ui/app/components/app/notice.js | 138 + .../app/provider-page-container/index.js | 3 + .../app/provider-page-container/index.scss | 121 + .../provider-page-container-content/index.js | 1 + .../provider-page-container-content.component.js | 77 + .../provider-page-container-content.container.js | 11 + .../provider-page-container-header/index.js | 1 + .../provider-page-container-header.component.js | 12 + .../provider-page-container.component.js | 76 + ui/app/components/app/selected-account/index.js | 2 + ui/app/components/app/selected-account/index.scss | 38 + .../selected-account/selected-account.component.js | 55 + .../selected-account/selected-account.container.js | 14 + .../tests/selected-account-component.test.js | 16 + ui/app/components/app/send/README.md | 0 .../account-list-item/account-list-item-README.md | 0 .../account-list-item.component.js | 108 + .../account-list-item.container.js | 27 + .../components/app/send/account-list-item/index.js | 1 + .../tests/account-list-item-component.test.js | 148 ++ .../tests/account-list-item-container.test.js | 73 + ui/app/components/app/send/index.js | 1 + ui/app/components/app/send/send-content/index.js | 1 + .../send/send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 65 + .../amount-max-button.container.js | 40 + .../amount-max-button.selectors.js | 9 + .../amount-max-button/amount-max-button.utils.js | 29 + .../send-amount-row/amount-max-button/index.js | 1 + .../tests/amount-max-button-component.test.js | 89 + .../tests/amount-max-button-container.test.js | 91 + .../tests/amount-max-button-selectors.test.js | 22 + .../tests/amount-max-button-utils.test.js | 27 + .../app/send/send-content/send-amount-row/index.js | 1 + .../send-amount-row/send-amount-row.component.js | 119 + .../send-amount-row/send-amount-row.container.js | 54 + .../send-amount-row/send-amount-row.scss | 0 .../send-amount-row/send-amount-row.selectors.js | 9 + .../tests/send-amount-row-component.test.js | 187 ++ .../tests/send-amount-row-container.test.js | 125 + .../tests/send-amount-row-selectors.test.js | 34 + .../send/send-content/send-content.component.js | 41 + .../send/send-content/send-dropdown-list/index.js | 1 + .../send-dropdown-list.component.js | 52 + .../tests/send-dropdown-list-component.test.js | 105 + .../app/send/send-content/send-from-row/index.js | 1 + .../send-from-row/send-from-row.component.js | 27 + .../send-from-row/send-from-row.container.js | 11 + .../send-from-row/send-from-row.selectors.js | 9 + .../tests/send-from-row-component.test.js | 31 + .../tests/send-from-row-container.test.js | 26 + .../tests/send-from-row-selectors.test.js | 20 + .../app/send/send-content/send-gas-row/README.md | 0 .../gas-fee-display/gas-fee-display.component.js | 57 + .../send-gas-row/gas-fee-display/index.js | 1 + .../test/gas-fee-display.component.test.js | 61 + .../app/send/send-content/send-gas-row/index.js | 1 + .../send-gas-row/send-gas-row.component.js | 131 + .../send-gas-row/send-gas-row.container.js | 118 + .../send-content/send-gas-row/send-gas-row.scss | 0 .../send-gas-row/send-gas-row.selectors.js | 19 + .../tests/send-gas-row-component.test.js | 104 + .../tests/send-gas-row-container.test.js | 200 ++ .../tests/send-gas-row-selectors.test.js | 62 + .../send/send-content/send-hex-data-row/index.js | 1 + .../send-hex-data-row.component.js | 42 + .../send-hex-data-row.container.js | 21 + .../send/send-content/send-row-wrapper/index.js | 1 + .../send-row-error-message/index.js | 1 + .../send-row-error-message-README.md | 0 .../send-row-error-message.component.js | 27 + .../send-row-error-message.container.js | 12 + .../send-row-error-message.scss | 0 .../tests/send-row-error-message-component.test.js | 28 + .../tests/send-row-error-message-container.test.js | 28 + .../send-row-warning-message/index.js | 1 + .../send-row-warning-message.component.js | 27 + .../send-row-warning-message.container.js | 12 + .../send-row-warning-message.scss | 0 .../send-row-warning-message-component.test.js | 28 + .../send-row-warning-message-container.test.js | 28 + .../send-row-wrapper/send-row-wrapper-README.md | 0 .../send-row-wrapper/send-row-wrapper.component.js | 48 + .../send-row-wrapper/send-row-wrapper.scss | 0 .../tests/send-row-wrapper-component.test.js | 79 + .../app/send/send-content/send-to-row/index.js | 1 + .../send-content/send-to-row/send-to-row-README.md | 0 .../send-to-row/send-to-row.component.js | 91 + .../send-to-row/send-to-row.container.js | 54 + .../send-to-row/send-to-row.selectors.js | 24 + .../send-content/send-to-row/send-to-row.utils.js | 36 + .../tests/send-to-row-component.test.js | 166 ++ .../tests/send-to-row-container.test.js | 134 + .../tests/send-to-row-selectors.test.js | 59 + .../send-to-row/tests/send-to-row-utils.test.js | 107 + .../tests/send-content-component.test.js | 50 + ui/app/components/app/send/send-footer/README.md | 0 ui/app/components/app/send/send-footer/index.js | 1 + .../app/send/send-footer/send-footer.component.js | 137 + .../app/send/send-footer/send-footer.container.js | 107 + .../app/send/send-footer/send-footer.scss | 0 .../app/send/send-footer/send-footer.selectors.js | 11 + .../app/send/send-footer/send-footer.utils.js | 85 + .../tests/send-footer-component.test.js | 233 ++ .../tests/send-footer-container.test.js | 200 ++ .../tests/send-footer-selectors.test.js | 24 + .../send-footer/tests/send-footer-utils.test.js | 234 ++ ui/app/components/app/send/send-header/README.md | 0 ui/app/components/app/send/send-header/index.js | 1 + .../app/send/send-header/send-header.component.js | 34 + .../app/send/send-header/send-header.container.js | 19 + .../app/send/send-header/send-header.selectors.js | 37 + .../tests/send-header-component.test.js | 70 + .../tests/send-header-container.test.js | 59 + .../tests/send-header-selectors.test.js | 47 + ui/app/components/app/send/send.component.js | 218 ++ ui/app/components/app/send/send.constants.js | 61 + ui/app/components/app/send/send.container.js | 112 + ui/app/components/app/send/send.scss | 0 ui/app/components/app/send/send.selectors.js | 291 +++ ui/app/components/app/send/send.utils.js | 332 +++ .../app/send/tests/send-component.test.js | 354 +++ .../app/send/tests/send-container.test.js | 174 ++ .../app/send/tests/send-selectors-test-data.js | 232 ++ .../app/send/tests/send-selectors.test.js | 705 +++++ .../components/app/send/tests/send-utils.test.js | 527 ++++ .../app/send/to-autocomplete.component.js | 141 + .../components/app/send/to-autocomplete/index.js | 1 + .../app/send/to-autocomplete/to-autocomplete.js | 129 + ui/app/components/app/shapeshift-form.js | 256 ++ ui/app/components/app/shift-list-item.js | 204 ++ ui/app/components/app/sidebars/index.js | 1 + ui/app/components/app/sidebars/index.scss | 81 + .../components/app/sidebars/sidebar-content.scss | 112 + .../components/app/sidebars/sidebar.component.js | 69 + .../components/app/sidebars/sidebar.constants.js | 1 + .../app/sidebars/tests/sidebars-component.test.js | 97 + ui/app/components/app/signature-request.js | 316 +++ ui/app/components/app/tab-bar.js | 33 + ui/app/components/app/token-cell.js | 177 ++ ui/app/components/app/token-list.js | 188 ++ ui/app/components/app/transaction-action/index.js | 1 + .../tests/transaction-action.component.test.js | 162 ++ .../transaction-action.component.js | 58 + .../app/transaction-activity-log/index.js | 1 + .../app/transaction-activity-log/index.scss | 84 + .../transaction-activity-log.component.test.js | 101 + .../transaction-activity-log.container.test.js | 28 + .../tests/transaction-activity-log.util.test.js | 335 +++ .../transaction-activity-log-icon/index.js | 1 + .../transaction-activity-log-icon.component.js | 55 + .../transaction-activity-log.component.js | 131 + .../transaction-activity-log.constants.js | 13 + .../transaction-activity-log.container.js | 44 + .../transaction-activity-log.util.js | 224 ++ .../components/app/transaction-breakdown/index.js | 1 + .../app/transaction-breakdown/index.scss | 24 + .../tests/transaction-breakdown.component.test.js | 33 + .../transaction-breakdown-row/index.js | 1 + .../transaction-breakdown-row/index.scss | 19 + .../transaction-breakdown-row.component.test.js | 39 + .../transaction-breakdown-row.component.js | 26 + .../transaction-breakdown.component.js | 106 + .../transaction-breakdown.container.js | 29 + .../app/transaction-list-item-details/index.js | 1 + .../app/transaction-list-item-details/index.scss | 63 + ...transaction-list-item-details.component.test.js | 81 + .../transaction-list-item-details.component.js | 181 ++ .../components/app/transaction-list-item/index.js | 1 + .../app/transaction-list-item/index.scss | 129 + .../transaction-list-item.component.js | 224 ++ .../transaction-list-item.container.js | 82 + ui/app/components/app/transaction-list/index.js | 1 + ui/app/components/app/transaction-list/index.scss | 46 + .../transaction-list/transaction-list.component.js | 126 + .../transaction-list/transaction-list.container.js | 44 + ui/app/components/app/transaction-status/index.js | 1 + .../components/app/transaction-status/index.scss | 45 + .../tests/transaction-status.component.test.js | 33 + .../transaction-status.component.js | 63 + .../app/transaction-view-balance/index.js | 1 + .../app/transaction-view-balance/index.scss | 92 + .../tests/token-view-balance.component.test.js | 72 + .../transaction-view-balance.component.js | 145 + .../transaction-view-balance.container.js | 46 + ui/app/components/app/transaction-view/index.js | 1 + ui/app/components/app/transaction-view/index.scss | 28 + .../transaction-view/transaction-view.component.js | 27 + .../app/ui-migration-annoucement/index.js | 1 + .../app/ui-migration-annoucement/index.scss | 22 + .../ui-migration-annoucement.component.js | 33 + .../ui-migration-announcement.container.js | 21 + .../app/user-preferenced-currency-display/index.js | 1 + ...-preferenced-currency-display.component.test.js | 34 + ...-preferenced-currency-display.container.test.js | 202 ++ .../user-preferenced-currency-display.component.js | 47 + .../user-preferenced-currency-display.container.js | 67 + .../app/user-preferenced-currency-input/index.js | 1 + ...er-preferenced-currency-input.component.test.js | 32 + ...er-preferenced-currency-input.container.test.js | 31 + .../user-preferenced-currency-input.component.js | 20 + .../user-preferenced-currency-input.container.js | 13 + .../app/user-preferenced-token-input/index.js | 1 + .../user-preferenced-token-input.component.test.js | 32 + .../user-preferenced-token-input.container.test.js | 31 + .../user-preferenced-token-input.component.js | 20 + .../user-preferenced-token-input.container.js | 13 + ui/app/components/app/wallet-view.js | 246 ++ ui/app/components/balance/balance.component.js | 92 - ui/app/components/balance/balance.container.js | 32 - ui/app/components/balance/index.js | 1 - ui/app/components/bn-as-decimal-input.js | 188 -- .../breadcrumbs/breadcrumbs.component.js | 29 - ui/app/components/breadcrumbs/index.js | 1 - ui/app/components/breadcrumbs/index.scss | 15 - .../tests/breadcrumbs.component.test.js | 22 - .../button-group/button-group.component.js | 73 - .../button-group/button-group.stories.js | 49 - ui/app/components/button-group/index.js | 1 - ui/app/components/button-group/index.scss | 38 - .../tests/button-group-component.test.js | 111 - ui/app/components/button/button.component.js | 51 - ui/app/components/button/button.stories.js | 58 - ui/app/components/button/index.js | 2 - ui/app/components/card/card.component.js | 25 - ui/app/components/card/index.js | 1 - ui/app/components/card/index.scss | 11 - .../components/card/tests/card.component.test.js | 25 - ui/app/components/coinbase-form.js | 69 - .../confirm-detail-row.component.js | 84 - .../confirm-detail-row/index.js | 1 - .../confirm-detail-row/index.scss | 50 - .../tests/confirm-detail-row.component.test.js | 64 - .../confirm-page-container-content.component.js | 110 - .../confirm-page-container-summary.component.js | 71 - .../confirm-page-container-summary/index.js | 1 - .../confirm-page-container-summary/index.scss | 54 - .../confirm-page-container-warning.component.js | 22 - .../confirm-page-container-warning/index.js | 1 - .../confirm-page-container-warning/index.scss | 18 - .../confirm-page-container-content/index.js | 3 - .../confirm-page-container-content/index.scss | 68 - .../confirm-page-container-header.component.js | 63 - .../confirm-page-container-header/index.js | 1 - .../confirm-page-container-header/index.scss | 27 - .../confirm-page-container-navigation.component.js | 69 - .../confirm-page-container-navigation/index.js | 1 - .../confirm-page-container-navigation/index.scss | 54 - .../confirm-page-container.component.js | 170 -- ui/app/components/confirm-page-container/index.js | 10 - .../components/confirm-page-container/index.scss | 7 - ui/app/components/copyButton.js | 66 - ui/app/components/copyable.js | 53 - .../currency-display/currency-display.component.js | 46 - .../currency-display/currency-display.container.js | 51 - ui/app/components/currency-display/index.js | 1 - ui/app/components/currency-display/index.scss | 14 - .../tests/currency-display.component.test.js | 27 - .../tests/currency-display.container.test.js | 145 - .../currency-input/currency-input.component.js | 160 -- .../currency-input/currency-input.container.js | 31 - ui/app/components/currency-input/index.js | 1 - ui/app/components/currency-input/index.scss | 26 - .../tests/currency-input.component.test.js | 345 --- .../tests/currency-input.container.test.js | 170 -- .../customize-gas-modal/gas-modal-card.js | 54 - .../components/customize-gas-modal/gas-slider.js | 50 - ui/app/components/customize-gas-modal/index.js | 396 --- .../dropdowns/account-details-dropdown.js | 131 - .../dropdowns/components/account-dropdowns.js | 473 ---- ui/app/components/dropdowns/components/dropdown.js | 112 - ui/app/components/dropdowns/components/menu.js | 51 - .../dropdowns/components/network-dropdown-icon.js | 47 - ui/app/components/dropdowns/index.js | 11 - ui/app/components/dropdowns/network-dropdown.js | 378 --- ui/app/components/dropdowns/simple-dropdown.js | 92 - ui/app/components/dropdowns/tests/dropdown.test.js | 37 - ui/app/components/dropdowns/tests/menu.test.js | 87 - .../dropdowns/tests/network-dropdown-icon.test.js | 25 - .../dropdowns/tests/network-dropdown.test.js | 97 - ui/app/components/dropdowns/token-menu-dropdown.js | 68 - ui/app/components/editable-label.js | 88 - ui/app/components/ens-input.js | 181 -- .../error-message/error-message.component.js | 30 - ui/app/components/error-message/index.js | 1 - ui/app/components/error-message/index.scss | 21 - .../tests/error-message.component.test.js | 36 - ui/app/components/eth-balance.js | 102 - .../export-text-container.component.js | 45 - ui/app/components/export-text-container/index.js | 2 - ui/app/components/export-text-container/index.scss | 52 - ui/app/components/fiat-value.js | 66 - .../advanced-gas-inputs.component.js | 156 -- .../advanced-gas-inputs.container.js | 38 - .../gas-customization/advanced-gas-inputs/index.js | 1 - .../advanced-gas-inputs/index.scss | 133 - .../advanced-tab-content.component.js | 219 -- .../advanced-tab-content/index.js | 1 - .../advanced-tab-content/index.scss | 203 -- .../tests/advanced-tab-content-component.test.js | 364 --- .../advanced-tab-content/time-remaining/index.js | 1 - .../advanced-tab-content/time-remaining/index.scss | 17 - .../tests/time-remaining-component.test.js | 30 - .../time-remaining/time-remaining.component.js | 33 - .../time-remaining/time-remaining.utils.js | 11 - .../basic-tab-content.component.js | 35 - .../basic-tab-content/index.js | 1 - .../basic-tab-content/index.scss | 28 - .../tests/basic-tab-content-component.test.js | 82 - .../gas-modal-page-container.component.js | 186 -- .../gas-modal-page-container.container.js | 291 --- .../gas-modal-page-container/index.js | 1 - .../gas-modal-page-container/index.scss | 146 -- .../gas-modal-page-container-component.test.js | 274 -- .../gas-modal-page-container-container.test.js | 425 --- .../gas-price-button-group.component.js | 89 - .../gas-price-button-group/index.js | 1 - .../gas-price-button-group/index.scss | 238 -- .../tests/gas-price-button-group-component.test.js | 233 -- .../gas-price-chart/gas-price-chart.component.js | 108 - .../gas-price-chart/gas-price-chart.utils.js | 354 --- .../gas-customization/gas-price-chart/index.js | 1 - .../gas-customization/gas-price-chart/index.scss | 132 - .../tests/gas-price-chart.component.test.js | 218 -- .../gas-slider/gas-slider.component.js | 48 - .../gas-customization/gas-slider/index.js | 1 - .../gas-customization/gas-slider/index.scss | 54 - .../components/gas-customization/gas.selectors.js | 14 - ui/app/components/gas-customization/index.scss | 7 - .../hex-to-decimal/hex-to-decimal.component.js | 21 - ui/app/components/hex-to-decimal/index.js | 1 - .../tests/hex-to-decimal.component.test.js | 26 - ui/app/components/identicon/identicon.component.js | 99 - ui/app/components/identicon/identicon.container.js | 12 - ui/app/components/identicon/index.js | 1 - ui/app/components/identicon/index.scss | 7 - .../identicon/tests/identicon.component.test.js | 51 - ui/app/components/index.scss | 81 - ui/app/components/info-box/index.js | 2 - ui/app/components/info-box/index.scss | 24 - ui/app/components/info-box/info-box.component.js | 49 - ui/app/components/input-number.js | 81 - ui/app/components/jazzicon/index.js | 1 - ui/app/components/jazzicon/jazzicon.component.js | 69 - ui/app/components/loading-network-screen/index.js | 1 - .../loading-network-screen.component.js | 138 - .../loading-network-screen.container.js | 41 - ui/app/components/loading-screen/index.js | 2 - .../loading-screen/loading-screen.component.js | 31 - ui/app/components/lock-icon/index.js | 1 - ui/app/components/lock-icon/lock-icon.component.js | 32 - ui/app/components/mascot.js | 59 - ui/app/components/menu-bar/index.js | 1 - ui/app/components/menu-bar/index.scss | 23 - ui/app/components/menu-bar/menu-bar.component.js | 79 - ui/app/components/menu-bar/menu-bar.container.js | 26 - ui/app/components/menu-droppo.js | 134 - ui/app/components/modal/index.js | 2 - ui/app/components/modal/index.scss | 62 - ui/app/components/modal/modal-content/index.js | 1 - ui/app/components/modal/modal-content/index.scss | 19 - .../modal/modal-content/modal-content.component.js | 32 - .../tests/modal-content.component.test.js | 44 - ui/app/components/modal/modal.component.js | 83 - .../components/modal/tests/modal.component.test.js | 133 - ui/app/components/modals/account-details-modal.js | 101 - .../components/modals/account-modal-container.js | 80 - ui/app/components/modals/buy-options-modal.js | 101 - .../cancel-transaction-gas-fee.component.js | 29 - .../cancel-transaction-gas-fee/index.js | 1 - .../cancel-transaction-gas-fee/index.scss | 17 - .../cancel-transaction-gas-fee.component.test.js | 26 - .../cancel-transaction.component.js | 76 - .../cancel-transaction.container.js | 60 - .../components/modals/cancel-transaction/index.js | 1 - .../modals/cancel-transaction/index.scss | 18 - .../tests/cancel-transaction.component.test.js | 57 - .../clear-approved-origins.component.js | 39 - .../clear-approved-origins.container.js | 16 - .../modals/clear-approved-origins/index.js | 1 - .../confirm-remove-account.component.js | 89 - .../confirm-remove-account.container.js | 22 - .../modals/confirm-remove-account/index.js | 1 - .../modals/confirm-remove-account/index.scss | 58 - .../confirm-reset-account.component.js | 38 - .../confirm-reset-account.container.js | 16 - .../modals/confirm-reset-account/index.js | 1 - .../customize-gas/customize-gas.component.js | 162 -- .../customize-gas/customize-gas.container.js | 22 - .../modals/customize-gas/customize-gas.util.js | 34 - ui/app/components/modals/customize-gas/index.js | 1 - ui/app/components/modals/customize-gas/index.scss | 110 - ui/app/components/modals/deposit-ether-modal.js | 220 -- .../components/modals/edit-account-name-modal.js | 83 - .../components/modals/export-private-key-modal.js | 177 -- .../modals/hide-token-confirmation-modal.js | 83 - ui/app/components/modals/index.js | 5 - ui/app/components/modals/index.scss | 11 - .../modals/loading-network-error/index.js | 1 - .../loading-network-error.component.js | 29 - .../loading-network-error.container.js | 4 - .../modals/metametrics-opt-in-modal/index.js | 1 - .../modals/metametrics-opt-in-modal/index.scss | 30 - .../metametrics-opt-in-modal.component.js | 141 - .../metametrics-opt-in-modal.container.js | 24 - ui/app/components/modals/modal.js | 511 ---- ui/app/components/modals/new-account-modal.js | 112 - ui/app/components/modals/notification-modal.js | 81 - ui/app/components/modals/qr-scanner/index.js | 2 - ui/app/components/modals/qr-scanner/index.scss | 83 - .../modals/qr-scanner/qr-scanner.component.js | 216 -- .../modals/qr-scanner/qr-scanner.container.js | 24 - .../components/modals/reject-transactions/index.js | 1 - .../modals/reject-transactions/index.scss | 6 - .../reject-transactions.component.js | 45 - .../reject-transactions.container.js | 17 - .../modals/shapeshift-deposit-tx-modal.js | 40 - .../modals/transaction-confirmed/index.js | 1 - .../modals/transaction-confirmed/index.scss | 22 - .../transaction-confirmed.component.js | 45 - .../transaction-confirmed.container.js | 4 - ui/app/components/network-display/index.js | 2 - ui/app/components/network-display/index.scss | 57 - .../network-display/network-display.component.js | 76 - .../network-display/network-display.container.js | 11 - ui/app/components/network.js | 149 -- ui/app/components/notice.js | 138 - ui/app/components/page-container/index.js | 4 - ui/app/components/page-container/index.scss | 219 -- .../page-container-content.component.js | 18 - .../page-container/page-container-footer/index.js | 1 - .../page-container-footer.component.js | 68 - .../tests/page-container-footer.component.test.js | 79 - .../page-container/page-container-header/index.js | 1 - .../page-container-header.component.js | 83 - .../tests/page-container-header.component.test.js | 82 - .../page-container/page-container.component.js | 131 - .../tests/page-container.component.test.js | 0 .../pages/add-token/add-token.component.js | 335 --- .../pages/add-token/add-token.container.js | 22 - ui/app/components/pages/add-token/index.js | 2 - ui/app/components/pages/add-token/index.scss | 45 - .../components/pages/add-token/token-list/index.js | 2 - .../pages/add-token/token-list/index.scss | 65 - .../token-list/token-list-placeholder/index.js | 2 - .../token-list/token-list-placeholder/index.scss | 23 - .../token-list-placeholder.component.js | 27 - .../add-token/token-list/token-list.component.js | 60 - .../add-token/token-list/token-list.container.js | 11 - .../pages/add-token/token-search/index.js | 2 - .../token-search/token-search.component.js | 85 - ui/app/components/pages/add-token/util.js | 13 - .../confirm-add-suggested-token.component.js | 122 - .../confirm-add-suggested-token.container.js | 29 - .../pages/confirm-add-suggested-token/index.js | 2 - .../confirm-add-token.component.js | 117 - .../confirm-add-token.container.js | 20 - ui/app/components/pages/confirm-add-token/index.js | 2 - .../components/pages/confirm-add-token/index.scss | 69 - .../confirm-approve/confirm-approve.component.js | 21 - .../confirm-approve/confirm-approve.container.js | 15 - ui/app/components/pages/confirm-approve/index.js | 1 - .../confirm-deploy-contract.component.js | 64 - .../confirm-deploy-contract.container.js | 12 - .../pages/confirm-deploy-contract/index.js | 1 - .../confirm-send-ether.component.js | 39 - .../confirm-send-ether.container.js | 45 - .../components/pages/confirm-send-ether/index.js | 1 - .../confirm-send-token.component.js | 29 - .../confirm-send-token.container.js | 52 - .../components/pages/confirm-send-token/index.js | 1 - .../confirm-token-transaction-base.component.js | 119 - .../confirm-token-transaction-base.container.js | 34 - .../pages/confirm-token-transaction-base/index.js | 2 - .../confirm-transaction-base.component.js | 574 ---- .../confirm-transaction-base.container.js | 242 -- .../pages/confirm-transaction-base/index.js | 1 - .../confirm-transaction-base.component.test.js | 14 - .../confirm-transaction-switch.component.js | 92 - .../confirm-transaction-switch.container.js | 22 - .../confirm-transaction-switch.util.js | 4 - .../pages/confirm-transaction-switch/index.js | 2 - .../confirm-transaction.component.js | 160 -- .../confirm-transaction.container.js | 37 - .../components/pages/confirm-transaction/index.js | 2 - .../connect-hardware/account-list.js | 205 -- .../connect-hardware/connect-screen.js | 197 -- .../pages/create-account/connect-hardware/index.js | 293 --- .../pages/create-account/import-account/index.js | 96 - .../pages/create-account/import-account/json.js | 170 -- .../create-account/import-account/private-key.js | 128 - .../pages/create-account/import-account/seed.js | 35 - ui/app/components/pages/create-account/index.js | 113 - .../components/pages/create-account/new-account.js | 130 - .../create-password/create-password.component.js | 71 - .../create-password/create-password.container.js | 12 - .../import-with-seed-phrase.component.js | 256 -- .../import-with-seed-phrase/index.js | 1 - .../pages/first-time-flow/create-password/index.js | 1 - .../create-password/new-account/index.js | 1 - .../new-account/new-account.component.js | 225 -- .../create-password/unique-image/index.js | 1 - .../unique-image/unique-image.component.js | 55 - .../unique-image/unique-image.container.js | 12 - .../end-of-flow/end-of-flow.component.js | 93 - .../end-of-flow/end-of-flow.container.js | 25 - .../pages/first-time-flow/end-of-flow/index.js | 1 - .../pages/first-time-flow/end-of-flow/index.scss | 53 - .../first-time-flow-switch.component.js | 57 - .../first-time-flow-switch.container.js | 20 - .../first-time-flow-switch/index.js | 1 - .../first-time-flow/first-time-flow.component.js | 152 -- .../first-time-flow/first-time-flow.container.js | 31 - .../first-time-flow/first-time-flow.selectors.js | 26 - ui/app/components/pages/first-time-flow/index.js | 1 - ui/app/components/pages/first-time-flow/index.scss | 159 -- .../first-time-flow/metametrics-opt-in/index.js | 1 - .../first-time-flow/metametrics-opt-in/index.scss | 136 - .../metametrics-opt-in.component.js | 169 -- .../metametrics-opt-in.container.js | 27 - .../confirm-seed-phrase.component.js | 155 -- .../confirm-seed-phrase.state.js | 41 - .../seed-phrase/confirm-seed-phrase/index.js | 1 - .../seed-phrase/confirm-seed-phrase/index.scss | 48 - .../pages/first-time-flow/seed-phrase/index.js | 1 - .../pages/first-time-flow/seed-phrase/index.scss | 40 - .../seed-phrase/reveal-seed-phrase/index.js | 1 - .../seed-phrase/reveal-seed-phrase/index.scss | 57 - .../reveal-seed-phrase.component.js | 143 - .../seed-phrase/seed-phrase.component.js | 70 - .../pages/first-time-flow/select-action/index.js | 1 - .../pages/first-time-flow/select-action/index.scss | 88 - .../select-action/select-action.component.js | 112 - .../select-action/select-action.container.js | 23 - .../pages/first-time-flow/welcome/index.js | 1 - .../pages/first-time-flow/welcome/index.scss | 42 - .../first-time-flow/welcome/welcome.component.js | 69 - .../first-time-flow/welcome/welcome.container.js | 26 - ui/app/components/pages/home/home.component.js | 77 - ui/app/components/pages/home/home.container.js | 32 - ui/app/components/pages/home/index.js | 1 - ui/app/components/pages/index.scss | 11 - ui/app/components/pages/keychains/index.scss | 197 -- ui/app/components/pages/keychains/restore-vault.js | 197 -- ui/app/components/pages/keychains/reveal-seed.js | 177 -- ui/app/components/pages/lock/index.js | 1 - ui/app/components/pages/lock/lock.component.js | 26 - ui/app/components/pages/lock/lock.container.js | 24 - ui/app/components/pages/mobile-sync/index.js | 387 --- ui/app/components/pages/notice.js | 203 -- ui/app/components/pages/provider-approval/index.js | 1 - .../provider-approval.component.js | 29 - .../provider-approval.container.js | 12 - ui/app/components/pages/settings/index.js | 1 - ui/app/components/pages/settings/index.scss | 80 - ui/app/components/pages/settings/info-tab/index.js | 1 - .../components/pages/settings/info-tab/index.scss | 56 - .../pages/settings/info-tab/info-tab.component.js | 136 - .../pages/settings/settings-tab/index.js | 1 - .../pages/settings/settings-tab/index.scss | 69 - .../settings-tab/settings-tab.component.js | 674 ----- .../settings-tab/settings-tab.container.js | 81 - .../pages/settings/settings.component.js | 54 - ui/app/components/pages/unlock-page/index.js | 2 - ui/app/components/pages/unlock-page/index.scss | 51 - .../pages/unlock-page/unlock-page.component.js | 191 -- .../pages/unlock-page/unlock-page.container.js | 64 - ui/app/components/provider-page-container/index.js | 3 - .../components/provider-page-container/index.scss | 121 - .../provider-page-container-content/index.js | 1 - .../provider-page-container-content.component.js | 77 - .../provider-page-container-content.container.js | 11 - .../provider-page-container-header/index.js | 1 - .../provider-page-container-header.component.js | 12 - .../provider-page-container.component.js | 76 - ui/app/components/qr-code.js | 63 - ui/app/components/readonly-input.js | 33 - ui/app/components/selected-account/index.js | 2 - ui/app/components/selected-account/index.scss | 38 - .../selected-account/selected-account.component.js | 55 - .../selected-account/selected-account.container.js | 14 - .../tests/selected-account-component.test.js | 16 - ui/app/components/send/README.md | 0 .../account-list-item/account-list-item-README.md | 0 .../account-list-item.component.js | 108 - .../account-list-item.container.js | 27 - ui/app/components/send/account-list-item/index.js | 1 - .../tests/account-list-item-component.test.js | 148 -- .../tests/account-list-item-container.test.js | 73 - ui/app/components/send/index.js | 1 - ui/app/components/send/send-content/index.js | 1 - .../send/send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 65 - .../amount-max-button.container.js | 40 - .../amount-max-button.selectors.js | 9 - .../amount-max-button/amount-max-button.utils.js | 29 - .../send-amount-row/amount-max-button/index.js | 1 - .../tests/amount-max-button-component.test.js | 89 - .../tests/amount-max-button-container.test.js | 91 - .../tests/amount-max-button-selectors.test.js | 22 - .../tests/amount-max-button-utils.test.js | 27 - .../send/send-content/send-amount-row/index.js | 1 - .../send-amount-row/send-amount-row.component.js | 119 - .../send-amount-row/send-amount-row.container.js | 54 - .../send-amount-row/send-amount-row.scss | 0 .../send-amount-row/send-amount-row.selectors.js | 9 - .../tests/send-amount-row-component.test.js | 187 -- .../tests/send-amount-row-container.test.js | 125 - .../tests/send-amount-row-selectors.test.js | 34 - .../send/send-content/send-content.component.js | 41 - .../send/send-content/send-dropdown-list/index.js | 1 - .../send-dropdown-list.component.js | 52 - .../tests/send-dropdown-list-component.test.js | 105 - .../send/send-content/send-from-row/index.js | 1 - .../send-from-row/send-from-row.component.js | 27 - .../send-from-row/send-from-row.container.js | 11 - .../send-from-row/send-from-row.selectors.js | 9 - .../tests/send-from-row-component.test.js | 31 - .../tests/send-from-row-container.test.js | 26 - .../tests/send-from-row-selectors.test.js | 20 - .../send/send-content/send-gas-row/README.md | 0 .../gas-fee-display/gas-fee-display.component.js | 57 - .../send-gas-row/gas-fee-display/index.js | 1 - .../test/gas-fee-display.component.test.js | 61 - .../send/send-content/send-gas-row/index.js | 1 - .../send-gas-row/send-gas-row.component.js | 131 - .../send-gas-row/send-gas-row.container.js | 118 - .../send-content/send-gas-row/send-gas-row.scss | 0 .../send-gas-row/send-gas-row.selectors.js | 19 - .../tests/send-gas-row-component.test.js | 104 - .../tests/send-gas-row-container.test.js | 200 -- .../tests/send-gas-row-selectors.test.js | 62 - .../send/send-content/send-hex-data-row/index.js | 1 - .../send-hex-data-row.component.js | 42 - .../send-hex-data-row.container.js | 21 - .../send/send-content/send-row-wrapper/index.js | 1 - .../send-row-error-message/index.js | 1 - .../send-row-error-message-README.md | 0 .../send-row-error-message.component.js | 27 - .../send-row-error-message.container.js | 12 - .../send-row-error-message.scss | 0 .../tests/send-row-error-message-component.test.js | 28 - .../tests/send-row-error-message-container.test.js | 28 - .../send-row-warning-message/index.js | 1 - .../send-row-warning-message.component.js | 27 - .../send-row-warning-message.container.js | 12 - .../send-row-warning-message.scss | 0 .../send-row-warning-message-component.test.js | 28 - .../send-row-warning-message-container.test.js | 28 - .../send-row-wrapper/send-row-wrapper-README.md | 0 .../send-row-wrapper/send-row-wrapper.component.js | 48 - .../send-row-wrapper/send-row-wrapper.scss | 0 .../tests/send-row-wrapper-component.test.js | 79 - .../send/send-content/send-to-row/index.js | 1 - .../send-content/send-to-row/send-to-row-README.md | 0 .../send-to-row/send-to-row.component.js | 91 - .../send-to-row/send-to-row.container.js | 54 - .../send-to-row/send-to-row.selectors.js | 24 - .../send-content/send-to-row/send-to-row.utils.js | 36 - .../tests/send-to-row-component.test.js | 166 -- .../tests/send-to-row-container.test.js | 134 - .../tests/send-to-row-selectors.test.js | 59 - .../send-to-row/tests/send-to-row-utils.test.js | 107 - .../tests/send-content-component.test.js | 50 - ui/app/components/send/send-footer/README.md | 0 ui/app/components/send/send-footer/index.js | 1 - .../send/send-footer/send-footer.component.js | 137 - .../send/send-footer/send-footer.container.js | 107 - .../components/send/send-footer/send-footer.scss | 0 .../send/send-footer/send-footer.selectors.js | 11 - .../send/send-footer/send-footer.utils.js | 85 - .../tests/send-footer-component.test.js | 233 -- .../tests/send-footer-container.test.js | 200 -- .../tests/send-footer-selectors.test.js | 24 - .../send-footer/tests/send-footer-utils.test.js | 234 -- ui/app/components/send/send-header/README.md | 0 ui/app/components/send/send-header/index.js | 1 - .../send/send-header/send-header.component.js | 34 - .../send/send-header/send-header.container.js | 19 - .../send/send-header/send-header.selectors.js | 37 - .../tests/send-header-component.test.js | 70 - .../tests/send-header-container.test.js | 59 - .../tests/send-header-selectors.test.js | 47 - ui/app/components/send/send.component.js | 218 -- ui/app/components/send/send.constants.js | 61 - ui/app/components/send/send.container.js | 112 - ui/app/components/send/send.scss | 0 ui/app/components/send/send.selectors.js | 291 --- ui/app/components/send/send.utils.js | 332 --- .../components/send/tests/send-component.test.js | 354 --- .../components/send/tests/send-container.test.js | 174 -- .../send/tests/send-selectors-test-data.js | 232 -- .../components/send/tests/send-selectors.test.js | 705 ----- ui/app/components/send/tests/send-utils.test.js | 527 ---- .../components/send/to-autocomplete.component.js | 141 - ui/app/components/send/to-autocomplete/index.js | 1 - .../send/to-autocomplete/to-autocomplete.js | 129 - ui/app/components/sender-to-recipient/index.js | 1 - ui/app/components/sender-to-recipient/index.scss | 149 -- .../sender-to-recipient.component.js | 186 -- .../sender-to-recipient.constants.js | 4 - ui/app/components/shapeshift-form.js | 256 -- ui/app/components/shift-list-item.js | 204 -- ui/app/components/sidebars/index.js | 1 - ui/app/components/sidebars/index.scss | 81 - ui/app/components/sidebars/sidebar-content.scss | 112 - ui/app/components/sidebars/sidebar.component.js | 69 - ui/app/components/sidebars/sidebar.constants.js | 1 - .../sidebars/tests/sidebars-component.test.js | 97 - ui/app/components/signature-request.js | 316 --- ui/app/components/spinner/index.js | 2 - ui/app/components/spinner/spinner.component.js | 78 - ui/app/components/tab-bar.js | 33 - ui/app/components/tabs/index.js | 3 - ui/app/components/tabs/index.scss | 11 - ui/app/components/tabs/tab/index.js | 2 - ui/app/components/tabs/tab/index.scss | 15 - ui/app/components/tabs/tab/tab.component.js | 38 - ui/app/components/tabs/tabs.component.js | 62 - ui/app/components/text-field/index.js | 2 - .../components/text-field/text-field.component.js | 109 - ui/app/components/text-field/text-field.stories.js | 53 - ui/app/components/token-balance/index.js | 1 - ui/app/components/token-balance/index.scss | 14 - .../token-balance/token-balance.component.js | 25 - .../token-balance/token-balance.container.js | 16 - ui/app/components/token-cell.js | 177 -- ui/app/components/token-currency-display/index.js | 1 - .../token-currency-display.component.js | 57 - ui/app/components/token-input/index.js | 1 - .../tests/token-input.component.test.js | 350 --- .../tests/token-input.container.test.js | 255 -- .../token-input/token-input.component.js | 145 - .../token-input/token-input.container.js | 30 - ui/app/components/token-list.js | 188 -- ui/app/components/tooltip-v2.js | 70 - ui/app/components/tooltip.js | 22 - ui/app/components/transaction-action/index.js | 1 - .../tests/transaction-action.component.test.js | 162 -- .../transaction-action.component.js | 58 - .../components/transaction-activity-log/index.js | 1 - .../components/transaction-activity-log/index.scss | 84 - .../transaction-activity-log.component.test.js | 101 - .../transaction-activity-log.container.test.js | 28 - .../tests/transaction-activity-log.util.test.js | 335 --- .../transaction-activity-log-icon/index.js | 1 - .../transaction-activity-log-icon.component.js | 55 - .../transaction-activity-log.component.js | 131 - .../transaction-activity-log.constants.js | 13 - .../transaction-activity-log.container.js | 44 - .../transaction-activity-log.util.js | 224 -- ui/app/components/transaction-breakdown/index.js | 1 - ui/app/components/transaction-breakdown/index.scss | 24 - .../tests/transaction-breakdown.component.test.js | 33 - .../transaction-breakdown-row/index.js | 1 - .../transaction-breakdown-row/index.scss | 19 - .../transaction-breakdown-row.component.test.js | 39 - .../transaction-breakdown-row.component.js | 26 - .../transaction-breakdown.component.js | 106 - .../transaction-breakdown.container.js | 29 - .../transaction-list-item-details/index.js | 1 - .../transaction-list-item-details/index.scss | 63 - ...transaction-list-item-details.component.test.js | 81 - .../transaction-list-item-details.component.js | 181 -- ui/app/components/transaction-list-item/index.js | 1 - ui/app/components/transaction-list-item/index.scss | 129 - .../transaction-list-item.component.js | 224 -- .../transaction-list-item.container.js | 82 - ui/app/components/transaction-list/index.js | 1 - ui/app/components/transaction-list/index.scss | 46 - .../transaction-list/transaction-list.component.js | 126 - .../transaction-list/transaction-list.container.js | 44 - ui/app/components/transaction-status/index.js | 1 - ui/app/components/transaction-status/index.scss | 45 - .../tests/transaction-status.component.test.js | 33 - .../transaction-status.component.js | 63 - .../components/transaction-view-balance/index.js | 1 - .../components/transaction-view-balance/index.scss | 92 - .../tests/token-view-balance.component.test.js | 72 - .../transaction-view-balance.component.js | 145 - .../transaction-view-balance.container.js | 46 - ui/app/components/transaction-view/index.js | 1 - ui/app/components/transaction-view/index.scss | 28 - .../transaction-view/transaction-view.component.js | 27 - .../components/ui-migration-annoucement/index.js | 1 - .../components/ui-migration-annoucement/index.scss | 22 - .../ui-migration-annoucement.component.js | 33 - .../ui-migration-announcement.container.js | 21 - .../account-dropdown-mini.component.js | 84 + .../components/ui/account-dropdown-mini/index.js | 1 + .../tests/account-dropdown-mini.component.test.js | 107 + ui/app/components/ui/alert/index.js | 62 + ui/app/components/ui/balance/balance.component.js | 92 + ui/app/components/ui/balance/balance.container.js | 32 + ui/app/components/ui/balance/index.js | 1 + .../ui/breadcrumbs/breadcrumbs.component.js | 29 + ui/app/components/ui/breadcrumbs/index.js | 1 + ui/app/components/ui/breadcrumbs/index.scss | 15 + .../tests/breadcrumbs.component.test.js | 22 + .../ui/button-group/button-group.component.js | 73 + .../ui/button-group/button-group.stories.js | 49 + ui/app/components/ui/button-group/index.js | 1 + ui/app/components/ui/button-group/index.scss | 38 + .../tests/button-group-component.test.js | 111 + ui/app/components/ui/button/button.component.js | 51 + ui/app/components/ui/button/button.stories.js | 58 + ui/app/components/ui/button/index.js | 2 + ui/app/components/ui/card/card.component.js | 25 + ui/app/components/ui/card/index.js | 1 + ui/app/components/ui/card/index.scss | 11 + .../ui/card/tests/card.component.test.js | 25 + ui/app/components/ui/copyButton.js | 66 + .../currency-display/currency-display.component.js | 46 + .../currency-display/currency-display.container.js | 51 + ui/app/components/ui/currency-display/index.js | 1 + ui/app/components/ui/currency-display/index.scss | 14 + .../tests/currency-display.component.test.js | 27 + .../tests/currency-display.container.test.js | 145 + .../ui/currency-input/currency-input.component.js | 160 ++ .../ui/currency-input/currency-input.container.js | 31 + ui/app/components/ui/currency-input/index.js | 1 + ui/app/components/ui/currency-input/index.scss | 26 + .../tests/currency-input.component.test.js | 345 +++ .../tests/currency-input.container.test.js | 170 ++ ui/app/components/ui/editable-label.js | 88 + .../ui/error-message/error-message.component.js | 30 + ui/app/components/ui/error-message/index.js | 1 + ui/app/components/ui/error-message/index.scss | 21 + .../tests/error-message.component.test.js | 36 + ui/app/components/ui/eth-balance.js | 102 + .../export-text-container.component.js | 45 + .../components/ui/export-text-container/index.js | 2 + .../components/ui/export-text-container/index.scss | 52 + ui/app/components/ui/fiat-value.js | 66 + .../ui/hex-to-decimal/hex-to-decimal.component.js | 21 + ui/app/components/ui/hex-to-decimal/index.js | 1 + .../tests/hex-to-decimal.component.test.js | 26 + .../components/ui/identicon/identicon.component.js | 99 + .../components/ui/identicon/identicon.container.js | 12 + ui/app/components/ui/identicon/index.js | 1 + ui/app/components/ui/identicon/index.scss | 7 + .../ui/identicon/tests/identicon.component.test.js | 51 + ui/app/components/ui/jazzicon/index.js | 1 + .../components/ui/jazzicon/jazzicon.component.js | 69 + ui/app/components/ui/loading-screen/index.js | 2 + .../ui/loading-screen/loading-screen.component.js | 31 + ui/app/components/ui/lock-icon/index.js | 1 + .../components/ui/lock-icon/lock-icon.component.js | 32 + ui/app/components/ui/mascot.js | 59 + ui/app/components/ui/page-container/index.js | 4 + ui/app/components/ui/page-container/index.scss | 219 ++ .../page-container-content.component.js | 18 + .../page-container/page-container-footer/index.js | 1 + .../page-container-footer.component.js | 68 + .../tests/page-container-footer.component.test.js | 79 + .../page-container/page-container-header/index.js | 1 + .../page-container-header.component.js | 83 + .../tests/page-container-header.component.test.js | 82 + .../ui/page-container/page-container.component.js | 131 + .../tests/page-container.component.test.js | 0 ui/app/components/ui/qr-code.js | 63 + ui/app/components/ui/readonly-input.js | 33 + ui/app/components/ui/sender-to-recipient/index.js | 1 + .../components/ui/sender-to-recipient/index.scss | 149 ++ .../sender-to-recipient.component.js | 186 ++ .../sender-to-recipient.constants.js | 4 + ui/app/components/ui/spinner/index.js | 2 + ui/app/components/ui/spinner/spinner.component.js | 78 + ui/app/components/ui/tabs/index.js | 3 + ui/app/components/ui/tabs/index.scss | 11 + ui/app/components/ui/tabs/tab/index.js | 2 + ui/app/components/ui/tabs/tab/index.scss | 15 + ui/app/components/ui/tabs/tab/tab.component.js | 38 + ui/app/components/ui/tabs/tabs.component.js | 62 + ui/app/components/ui/text-field/index.js | 2 + .../ui/text-field/text-field.component.js | 109 + .../components/ui/text-field/text-field.stories.js | 53 + ui/app/components/ui/token-balance/index.js | 1 + ui/app/components/ui/token-balance/index.scss | 14 + .../ui/token-balance/token-balance.component.js | 25 + .../ui/token-balance/token-balance.container.js | 16 + .../components/ui/token-currency-display/index.js | 1 + .../token-currency-display.component.js | 57 + ui/app/components/ui/token-input/index.js | 1 + .../tests/token-input.component.test.js | 350 +++ .../tests/token-input.container.test.js | 255 ++ .../ui/token-input/token-input.component.js | 145 + .../ui/token-input/token-input.container.js | 30 + ui/app/components/ui/tooltip-v2.js | 70 + ui/app/components/ui/tooltip.js | 22 + ui/app/components/ui/unit-input/index.js | 1 + ui/app/components/ui/unit-input/index.scss | 55 + .../unit-input/tests/unit-input.component.test.js | 146 ++ .../ui/unit-input/unit-input.component.js | 108 + ui/app/components/unit-input/index.js | 1 - ui/app/components/unit-input/index.scss | 55 - .../unit-input/tests/unit-input.component.test.js | 146 -- .../components/unit-input/unit-input.component.js | 108 - .../user-preferenced-currency-display/index.js | 1 - ...-preferenced-currency-display.component.test.js | 34 - ...-preferenced-currency-display.container.test.js | 202 -- .../user-preferenced-currency-display.component.js | 47 - .../user-preferenced-currency-display.container.js | 67 - .../user-preferenced-currency-input/index.js | 1 - ...er-preferenced-currency-input.component.test.js | 32 - ...er-preferenced-currency-input.container.test.js | 31 - .../user-preferenced-currency-input.component.js | 20 - .../user-preferenced-currency-input.container.js | 13 - .../user-preferenced-token-input/index.js | 1 - .../user-preferenced-token-input.component.test.js | 32 - .../user-preferenced-token-input.container.test.js | 31 - .../user-preferenced-token-input.component.js | 20 - .../user-preferenced-token-input.container.js | 13 - ui/app/components/wallet-view.js | 246 -- ui/app/conf-tx.js | 225 -- ui/app/constants/common.js | 10 - ui/app/constants/error-keys.js | 4 - ui/app/constants/transactions.js | 24 - ui/app/conversion-util.js | 251 -- ui/app/conversion-util.test.js | 22 - ui/app/css/itcss/components/index.scss | 2 +- ui/app/ducks/app/app.js | 788 ++++++ ui/app/ducks/confirm-transaction.duck.js | 420 --- .../confirm-transaction.duck.js | 420 +++ .../confirm-transaction.duck.test.js | 685 +++++ ui/app/ducks/gas.duck.js | 517 ---- ui/app/ducks/gas/gas-duck.test.js | 600 +++++ ui/app/ducks/gas/gas.duck.js | 517 ++++ ui/app/ducks/index.js | 95 + ui/app/ducks/locale/locale.js | 17 + ui/app/ducks/metamask/metamask.js | 419 +++ ui/app/ducks/mock-gas-estimate-data.js | 3 - ui/app/ducks/send.duck.js | 106 - ui/app/ducks/send/send-duck.test.js | 186 ++ ui/app/ducks/send/send.duck.js | 106 + .../ducks/tests/confirm-transaction.duck.test.js | 685 ----- ui/app/ducks/tests/gas-duck.test.js | 600 ----- ui/app/ducks/tests/send-duck.test.js | 186 -- ui/app/helpers/common.util.js | 5 - ui/app/helpers/confirm-transaction/util.js | 131 - ui/app/helpers/confirm-transaction/util.test.js | 137 - ui/app/helpers/constants/common.js | 10 + ui/app/helpers/constants/error-keys.js | 4 + ui/app/helpers/constants/infura-conversion.json | 653 +++++ ui/app/helpers/constants/routes.js | 83 + ui/app/helpers/constants/transactions.js | 24 + ui/app/helpers/conversions.util.js | 122 - ui/app/helpers/formatters.js | 3 - .../authenticated/authenticated.component.js | 22 + .../authenticated/authenticated.container.js | 12 + .../higher-order-components/authenticated/index.js | 1 + .../higher-order-components/i18n-provider.js | 55 + .../higher-order-components/initialized/index.js | 1 + .../initialized/initialized.component.js | 14 + .../initialized/initialized.container.js | 12 + .../metametrics/metametrics.provider.js | 106 + .../with-method-data/index.js | 1 + .../with-method-data/with-method-data.component.js | 65 + .../with-modal-props/index.js | 1 + .../tests/with-modal-props.test.js | 43 + .../with-modal-props/with-modal-props.js | 21 + .../with-token-tracker/index.js | 1 + .../with-token-tracker.component.js | 106 + ui/app/helpers/tests/common.util.test.js | 27 - ui/app/helpers/tests/transactions.util.test.js | 57 - ui/app/helpers/transactions.util.js | 179 -- ui/app/helpers/utils/common.util.js | 5 + ui/app/helpers/utils/common.util.test.js | 27 + ui/app/helpers/utils/confirm-tx.util.js | 131 + ui/app/helpers/utils/confirm-tx.util.test.js | 137 + ui/app/helpers/utils/conversion-util.js | 251 ++ ui/app/helpers/utils/conversion-util.test.js | 22 + ui/app/helpers/utils/conversions.util.js | 122 + ui/app/helpers/utils/formatters.js | 3 + ui/app/helpers/utils/i18n-helper.js | 44 + ui/app/helpers/utils/metametrics.util.js | 184 ++ ui/app/helpers/utils/token-util.js | 118 + ui/app/helpers/utils/transactions.util.js | 179 ++ ui/app/helpers/utils/transactions.util.test.js | 57 + ui/app/helpers/utils/util.js | 326 +++ .../authenticated/authenticated.component.js | 22 - .../authenticated/authenticated.container.js | 12 - .../higher-order-components/authenticated/index.js | 1 - .../higher-order-components/initialized/index.js | 1 - .../initialized/initialized.component.js | 14 - .../initialized/initialized.container.js | 12 - .../with-method-data/index.js | 1 - .../with-method-data/with-method-data.component.js | 65 - .../with-modal-props/index.js | 1 - .../tests/with-modal-props.test.js | 43 - .../with-modal-props/with-modal-props.js | 21 - .../with-token-tracker/index.js | 1 - .../with-token-tracker.component.js | 106 - ui/app/i18n-provider.js | 55 - ui/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes ui/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes ui/app/infura-conversion.json | 653 ----- ui/app/keychains/hd/create-vault-complete.js | 91 - ui/app/keychains/hd/restore-vault.js | 181 -- ui/app/metametrics/metametrics.provider.js | 106 - ui/app/metametrics/metametrics.util.js | 184 -- ui/app/pages/add-token/add-token.component.js | 335 +++ ui/app/pages/add-token/add-token.container.js | 22 + ui/app/pages/add-token/index.js | 2 + ui/app/pages/add-token/index.scss | 45 + ui/app/pages/add-token/token-list/index.js | 2 + ui/app/pages/add-token/token-list/index.scss | 65 + .../token-list/token-list-placeholder/index.js | 2 + .../token-list/token-list-placeholder/index.scss | 23 + .../token-list-placeholder.component.js | 27 + .../add-token/token-list/token-list.component.js | 60 + .../add-token/token-list/token-list.container.js | 11 + ui/app/pages/add-token/token-search/index.js | 2 + .../token-search/token-search.component.js | 85 + ui/app/pages/add-token/util.js | 13 + .../confirm-add-suggested-token.component.js | 122 + .../confirm-add-suggested-token.container.js | 29 + ui/app/pages/confirm-add-suggested-token/index.js | 2 + .../confirm-add-token.component.js | 117 + .../confirm-add-token.container.js | 20 + ui/app/pages/confirm-add-token/index.js | 2 + ui/app/pages/confirm-add-token/index.scss | 69 + .../confirm-approve/confirm-approve.component.js | 21 + .../confirm-approve/confirm-approve.container.js | 15 + ui/app/pages/confirm-approve/index.js | 1 + .../confirm-deploy-contract.component.js | 64 + .../confirm-deploy-contract.container.js | 12 + ui/app/pages/confirm-deploy-contract/index.js | 1 + .../confirm-send-ether.component.js | 39 + .../confirm-send-ether.container.js | 45 + ui/app/pages/confirm-send-ether/index.js | 1 + .../confirm-send-token.component.js | 29 + .../confirm-send-token.container.js | 52 + ui/app/pages/confirm-send-token/index.js | 1 + .../confirm-token-transaction-base.component.js | 119 + .../confirm-token-transaction-base.container.js | 34 + .../pages/confirm-token-transaction-base/index.js | 2 + .../confirm-transaction-base.component.js | 574 ++++ .../confirm-transaction-base.container.js | 242 ++ ui/app/pages/confirm-transaction-base/index.js | 1 + .../confirm-transaction-base.component.test.js | 14 + .../confirm-transaction-switch.component.js | 92 + .../confirm-transaction-switch.container.js | 22 + .../confirm-transaction-switch.util.js | 4 + ui/app/pages/confirm-transaction-switch/index.js | 2 + ui/app/pages/confirm-transaction/conf-tx.js | 225 ++ .../confirm-transaction.component.js | 160 ++ .../confirm-transaction.container.js | 37 + ui/app/pages/confirm-transaction/index.js | 2 + .../connect-hardware/account-list.js | 205 ++ .../connect-hardware/connect-screen.js | 197 ++ .../pages/create-account/connect-hardware/index.js | 293 +++ .../pages/create-account/import-account/index.js | 96 + ui/app/pages/create-account/import-account/json.js | 170 ++ .../create-account/import-account/private-key.js | 128 + ui/app/pages/create-account/import-account/seed.js | 35 + ui/app/pages/create-account/index.js | 113 + ui/app/pages/create-account/new-account.js | 130 + .../create-password/create-password.component.js | 71 + .../create-password/create-password.container.js | 12 + .../import-with-seed-phrase.component.js | 256 ++ .../import-with-seed-phrase/index.js | 1 + .../pages/first-time-flow/create-password/index.js | 1 + .../create-password/new-account/index.js | 1 + .../new-account/new-account.component.js | 225 ++ .../create-password/unique-image/index.js | 1 + .../unique-image/unique-image.component.js | 55 + .../unique-image/unique-image.container.js | 12 + .../end-of-flow/end-of-flow.component.js | 93 + .../end-of-flow/end-of-flow.container.js | 25 + ui/app/pages/first-time-flow/end-of-flow/index.js | 1 + .../pages/first-time-flow/end-of-flow/index.scss | 53 + .../first-time-flow-switch.component.js | 57 + .../first-time-flow-switch.container.js | 20 + .../first-time-flow-switch/index.js | 1 + .../first-time-flow/first-time-flow.component.js | 152 ++ .../first-time-flow/first-time-flow.container.js | 31 + .../first-time-flow/first-time-flow.selectors.js | 26 + ui/app/pages/first-time-flow/index.js | 1 + ui/app/pages/first-time-flow/index.scss | 159 ++ .../first-time-flow/metametrics-opt-in/index.js | 1 + .../first-time-flow/metametrics-opt-in/index.scss | 136 + .../metametrics-opt-in.component.js | 169 ++ .../metametrics-opt-in.container.js | 27 + .../confirm-seed-phrase.component.js | 155 ++ .../confirm-seed-phrase.state.js | 41 + .../seed-phrase/confirm-seed-phrase/index.js | 1 + .../seed-phrase/confirm-seed-phrase/index.scss | 48 + ui/app/pages/first-time-flow/seed-phrase/index.js | 1 + .../pages/first-time-flow/seed-phrase/index.scss | 40 + .../seed-phrase/reveal-seed-phrase/index.js | 1 + .../seed-phrase/reveal-seed-phrase/index.scss | 57 + .../reveal-seed-phrase.component.js | 143 + .../seed-phrase/seed-phrase.component.js | 70 + .../pages/first-time-flow/select-action/index.js | 1 + .../pages/first-time-flow/select-action/index.scss | 88 + .../select-action/select-action.component.js | 112 + .../select-action/select-action.container.js | 23 + ui/app/pages/first-time-flow/welcome/index.js | 1 + ui/app/pages/first-time-flow/welcome/index.scss | 42 + .../first-time-flow/welcome/welcome.component.js | 69 + .../first-time-flow/welcome/welcome.container.js | 26 + ui/app/pages/home/home.component.js | 77 + ui/app/pages/home/home.container.js | 32 + ui/app/pages/home/index.js | 1 + ui/app/pages/index.js | 31 + ui/app/pages/index.scss | 11 + ui/app/pages/keychains/index.scss | 197 ++ ui/app/pages/keychains/restore-vault.js | 197 ++ ui/app/pages/keychains/reveal-seed.js | 177 ++ ui/app/pages/lock/index.js | 1 + ui/app/pages/lock/lock.component.js | 26 + ui/app/pages/lock/lock.container.js | 24 + ui/app/pages/mobile-sync/index.js | 387 +++ ui/app/pages/notice/notice.js | 203 ++ ui/app/pages/provider-approval/index.js | 1 + .../provider-approval.component.js | 29 + .../provider-approval.container.js | 12 + ui/app/pages/routes/index.js | 441 ++++ ui/app/pages/settings/index.js | 1 + ui/app/pages/settings/index.scss | 80 + ui/app/pages/settings/info-tab/index.js | 1 + ui/app/pages/settings/info-tab/index.scss | 56 + .../pages/settings/info-tab/info-tab.component.js | 136 + ui/app/pages/settings/settings-tab/index.js | 1 + ui/app/pages/settings/settings-tab/index.scss | 69 + .../settings-tab/settings-tab.component.js | 674 +++++ .../settings-tab/settings-tab.container.js | 81 + ui/app/pages/settings/settings.component.js | 54 + ui/app/pages/unlock-page/index.js | 2 + ui/app/pages/unlock-page/index.scss | 51 + ui/app/pages/unlock-page/unlock-page.component.js | 191 ++ ui/app/pages/unlock-page/unlock-page.container.js | 64 + ui/app/reducers.js | 95 - ui/app/reducers/app.js | 788 ------ ui/app/reducers/locale.js | 17 - ui/app/reducers/metamask.js | 419 --- ui/app/root.js | 34 - ui/app/routes.js | 83 - ui/app/selectors.js | 301 --- ui/app/selectors/confirm-transaction.js | 4 +- ui/app/selectors/custom-gas.js | 12 +- ui/app/selectors/custom-gas.test.js | 595 +++++ ui/app/selectors/selectors.js | 301 +++ ui/app/selectors/tests/custom-gas.test.js | 595 ----- ui/app/selectors/transactions.js | 4 +- ui/app/store.js | 21 - ui/app/store/actions.js | 2761 ++++++++++++++++++++ ui/app/store/store.js | 21 + ui/app/token-util.js | 118 - ui/app/util.js | 326 --- ui/i18n-helper.js | 44 - ui/index.js | 8 +- ui/lib/icon-factory.js | 2 +- ui/lib/lost-accounts-notice.js | 2 +- ui/lib/tx-helper.js | 2 +- 1373 files changed, 53706 insertions(+), 54137 deletions(-) delete mode 100644 ui/.gitignore delete mode 100644 ui/app/accounts/new-account/index.js delete mode 100644 ui/app/actions.js delete mode 100644 ui/app/app.js delete mode 100644 ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js delete mode 100644 ui/app/components/account-dropdown-mini/index.js delete mode 100644 ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js delete mode 100644 ui/app/components/account-dropdowns.js delete mode 100644 ui/app/components/account-menu/account-menu.component.js delete mode 100644 ui/app/components/account-menu/account-menu.container.js delete mode 100644 ui/app/components/account-menu/index.js delete mode 100644 ui/app/components/account-menu/index.scss delete mode 100644 ui/app/components/account-panel.js delete mode 100644 ui/app/components/add-token-button/add-token-button.component.js delete mode 100644 ui/app/components/add-token-button/index.js delete mode 100644 ui/app/components/add-token-button/index.scss delete mode 100644 ui/app/components/alert/index.js delete mode 100644 ui/app/components/app-header/app-header.component.js delete mode 100644 ui/app/components/app-header/app-header.container.js delete mode 100644 ui/app/components/app-header/index.js delete mode 100644 ui/app/components/app-header/index.scss create mode 100644 ui/app/components/app/account-dropdowns.js create mode 100644 ui/app/components/app/account-menu/account-menu.component.js create mode 100644 ui/app/components/app/account-menu/account-menu.container.js create mode 100644 ui/app/components/app/account-menu/index.js create mode 100644 ui/app/components/app/account-menu/index.scss create mode 100644 ui/app/components/app/account-panel.js create mode 100644 ui/app/components/app/add-token-button/add-token-button.component.js create mode 100644 ui/app/components/app/add-token-button/index.js create mode 100644 ui/app/components/app/add-token-button/index.scss create mode 100644 ui/app/components/app/app-header/app-header.component.js create mode 100644 ui/app/components/app/app-header/app-header.container.js create mode 100644 ui/app/components/app/app-header/index.js create mode 100644 ui/app/components/app/app-header/index.scss create mode 100644 ui/app/components/app/bn-as-decimal-input.js create mode 100644 ui/app/components/app/coinbase-form.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-detail-row/index.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss create mode 100644 ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss create mode 100755 ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js create mode 100755 ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js create mode 100755 ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss create mode 100644 ui/app/components/app/confirm-page-container/confirm-page-container.component.js create mode 100644 ui/app/components/app/confirm-page-container/index.js create mode 100644 ui/app/components/app/confirm-page-container/index.scss create mode 100644 ui/app/components/app/copyable.js create mode 100644 ui/app/components/app/customize-gas-modal/gas-modal-card.js create mode 100644 ui/app/components/app/customize-gas-modal/gas-slider.js create mode 100644 ui/app/components/app/customize-gas-modal/index.js create mode 100644 ui/app/components/app/dropdowns/account-details-dropdown.js create mode 100644 ui/app/components/app/dropdowns/components/account-dropdowns.js create mode 100644 ui/app/components/app/dropdowns/components/dropdown.js create mode 100644 ui/app/components/app/dropdowns/components/menu.js create mode 100644 ui/app/components/app/dropdowns/components/network-dropdown-icon.js create mode 100644 ui/app/components/app/dropdowns/index.js create mode 100644 ui/app/components/app/dropdowns/network-dropdown.js create mode 100644 ui/app/components/app/dropdowns/simple-dropdown.js create mode 100644 ui/app/components/app/dropdowns/tests/dropdown.test.js create mode 100644 ui/app/components/app/dropdowns/tests/menu.test.js create mode 100644 ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js create mode 100644 ui/app/components/app/dropdowns/tests/network-dropdown.test.js create mode 100644 ui/app/components/app/dropdowns/token-menu-dropdown.js create mode 100644 ui/app/components/app/ens-input.js create mode 100644 ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js create mode 100644 ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js create mode 100644 ui/app/components/app/gas-customization/advanced-gas-inputs/index.js create mode 100644 ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/index.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js create mode 100644 ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js create mode 100644 ui/app/components/app/gas-customization/gas-price-button-group/index.js create mode 100644 ui/app/components/app/gas-customization/gas-price-button-group/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js create mode 100644 ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js create mode 100644 ui/app/components/app/gas-customization/gas-price-chart/index.js create mode 100644 ui/app/components/app/gas-customization/gas-price-chart/index.scss create mode 100644 ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js create mode 100644 ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js create mode 100644 ui/app/components/app/gas-customization/gas-slider/index.js create mode 100644 ui/app/components/app/gas-customization/gas-slider/index.scss create mode 100644 ui/app/components/app/gas-customization/gas.selectors.js create mode 100644 ui/app/components/app/gas-customization/index.scss create mode 100644 ui/app/components/app/index.scss create mode 100644 ui/app/components/app/info-box/index.js create mode 100644 ui/app/components/app/info-box/index.scss create mode 100644 ui/app/components/app/info-box/info-box.component.js create mode 100644 ui/app/components/app/input-number.js create mode 100644 ui/app/components/app/loading-network-screen/index.js create mode 100644 ui/app/components/app/loading-network-screen/loading-network-screen.component.js create mode 100644 ui/app/components/app/loading-network-screen/loading-network-screen.container.js create mode 100644 ui/app/components/app/menu-bar/index.js create mode 100644 ui/app/components/app/menu-bar/index.scss create mode 100644 ui/app/components/app/menu-bar/menu-bar.component.js create mode 100644 ui/app/components/app/menu-bar/menu-bar.container.js create mode 100644 ui/app/components/app/menu-droppo.js create mode 100644 ui/app/components/app/modal/index.js create mode 100644 ui/app/components/app/modal/index.scss create mode 100644 ui/app/components/app/modal/modal-content/index.js create mode 100644 ui/app/components/app/modal/modal-content/index.scss create mode 100644 ui/app/components/app/modal/modal-content/modal-content.component.js create mode 100644 ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js create mode 100644 ui/app/components/app/modal/modal.component.js create mode 100644 ui/app/components/app/modal/tests/modal.component.test.js create mode 100644 ui/app/components/app/modals/account-details-modal.js create mode 100644 ui/app/components/app/modals/account-modal-container.js create mode 100644 ui/app/components/app/modals/buy-options-modal.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js create mode 100644 ui/app/components/app/modals/cancel-transaction/index.js create mode 100644 ui/app/components/app/modals/cancel-transaction/index.scss create mode 100644 ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js create mode 100644 ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js create mode 100644 ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js create mode 100644 ui/app/components/app/modals/clear-approved-origins/index.js create mode 100644 ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js create mode 100644 ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js create mode 100644 ui/app/components/app/modals/confirm-remove-account/index.js create mode 100644 ui/app/components/app/modals/confirm-remove-account/index.scss create mode 100644 ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js create mode 100644 ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js create mode 100644 ui/app/components/app/modals/confirm-reset-account/index.js create mode 100644 ui/app/components/app/modals/customize-gas/customize-gas.component.js create mode 100644 ui/app/components/app/modals/customize-gas/customize-gas.container.js create mode 100644 ui/app/components/app/modals/customize-gas/customize-gas.util.js create mode 100644 ui/app/components/app/modals/customize-gas/index.js create mode 100644 ui/app/components/app/modals/customize-gas/index.scss create mode 100644 ui/app/components/app/modals/deposit-ether-modal.js create mode 100644 ui/app/components/app/modals/edit-account-name-modal.js create mode 100644 ui/app/components/app/modals/export-private-key-modal.js create mode 100644 ui/app/components/app/modals/hide-token-confirmation-modal.js create mode 100644 ui/app/components/app/modals/index.js create mode 100644 ui/app/components/app/modals/index.scss create mode 100644 ui/app/components/app/modals/loading-network-error/index.js create mode 100644 ui/app/components/app/modals/loading-network-error/loading-network-error.component.js create mode 100644 ui/app/components/app/modals/loading-network-error/loading-network-error.container.js create mode 100644 ui/app/components/app/modals/metametrics-opt-in-modal/index.js create mode 100644 ui/app/components/app/modals/metametrics-opt-in-modal/index.scss create mode 100644 ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js create mode 100644 ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js create mode 100644 ui/app/components/app/modals/modal.js create mode 100644 ui/app/components/app/modals/new-account-modal.js create mode 100644 ui/app/components/app/modals/notification-modal.js create mode 100644 ui/app/components/app/modals/qr-scanner/index.js create mode 100644 ui/app/components/app/modals/qr-scanner/index.scss create mode 100644 ui/app/components/app/modals/qr-scanner/qr-scanner.component.js create mode 100644 ui/app/components/app/modals/qr-scanner/qr-scanner.container.js create mode 100644 ui/app/components/app/modals/reject-transactions/index.js create mode 100644 ui/app/components/app/modals/reject-transactions/index.scss create mode 100644 ui/app/components/app/modals/reject-transactions/reject-transactions.component.js create mode 100644 ui/app/components/app/modals/reject-transactions/reject-transactions.container.js create mode 100644 ui/app/components/app/modals/shapeshift-deposit-tx-modal.js create mode 100644 ui/app/components/app/modals/transaction-confirmed/index.js create mode 100644 ui/app/components/app/modals/transaction-confirmed/index.scss create mode 100644 ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js create mode 100644 ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js create mode 100644 ui/app/components/app/network-display/index.js create mode 100644 ui/app/components/app/network-display/index.scss create mode 100644 ui/app/components/app/network-display/network-display.component.js create mode 100644 ui/app/components/app/network-display/network-display.container.js create mode 100644 ui/app/components/app/network.js create mode 100644 ui/app/components/app/notice.js create mode 100644 ui/app/components/app/provider-page-container/index.js create mode 100644 ui/app/components/app/provider-page-container/index.scss create mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/index.js create mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js create mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js create mode 100644 ui/app/components/app/provider-page-container/provider-page-container-header/index.js create mode 100644 ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js create mode 100644 ui/app/components/app/provider-page-container/provider-page-container.component.js create mode 100644 ui/app/components/app/selected-account/index.js create mode 100644 ui/app/components/app/selected-account/index.scss create mode 100644 ui/app/components/app/selected-account/selected-account.component.js create mode 100644 ui/app/components/app/selected-account/selected-account.container.js create mode 100644 ui/app/components/app/selected-account/tests/selected-account-component.test.js create mode 100644 ui/app/components/app/send/README.md create mode 100644 ui/app/components/app/send/account-list-item/account-list-item-README.md create mode 100644 ui/app/components/app/send/account-list-item/account-list-item.component.js create mode 100644 ui/app/components/app/send/account-list-item/account-list-item.container.js create mode 100644 ui/app/components/app/send/account-list-item/index.js create mode 100644 ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js create mode 100644 ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js create mode 100644 ui/app/components/app/send/index.js create mode 100644 ui/app/components/app/send/send-content/index.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/README.md create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/index.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss create mode 100644 ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js create mode 100644 ui/app/components/app/send/send-content/send-content.component.js create mode 100644 ui/app/components/app/send/send-content/send-dropdown-list/index.js create mode 100644 ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js create mode 100644 ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/index.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/README.md create mode 100644 ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/index.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss create mode 100644 ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js create mode 100644 ui/app/components/app/send/send-content/send-hex-data-row/index.js create mode 100644 ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js create mode 100644 ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/index.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss create mode 100644 ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/index.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md create mode 100644 ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js create mode 100644 ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js create mode 100644 ui/app/components/app/send/send-content/tests/send-content-component.test.js create mode 100644 ui/app/components/app/send/send-footer/README.md create mode 100644 ui/app/components/app/send/send-footer/index.js create mode 100644 ui/app/components/app/send/send-footer/send-footer.component.js create mode 100644 ui/app/components/app/send/send-footer/send-footer.container.js create mode 100644 ui/app/components/app/send/send-footer/send-footer.scss create mode 100644 ui/app/components/app/send/send-footer/send-footer.selectors.js create mode 100644 ui/app/components/app/send/send-footer/send-footer.utils.js create mode 100644 ui/app/components/app/send/send-footer/tests/send-footer-component.test.js create mode 100644 ui/app/components/app/send/send-footer/tests/send-footer-container.test.js create mode 100644 ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js create mode 100644 ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js create mode 100644 ui/app/components/app/send/send-header/README.md create mode 100644 ui/app/components/app/send/send-header/index.js create mode 100644 ui/app/components/app/send/send-header/send-header.component.js create mode 100644 ui/app/components/app/send/send-header/send-header.container.js create mode 100644 ui/app/components/app/send/send-header/send-header.selectors.js create mode 100644 ui/app/components/app/send/send-header/tests/send-header-component.test.js create mode 100644 ui/app/components/app/send/send-header/tests/send-header-container.test.js create mode 100644 ui/app/components/app/send/send-header/tests/send-header-selectors.test.js create mode 100644 ui/app/components/app/send/send.component.js create mode 100644 ui/app/components/app/send/send.constants.js create mode 100644 ui/app/components/app/send/send.container.js create mode 100644 ui/app/components/app/send/send.scss create mode 100644 ui/app/components/app/send/send.selectors.js create mode 100644 ui/app/components/app/send/send.utils.js create mode 100644 ui/app/components/app/send/tests/send-component.test.js create mode 100644 ui/app/components/app/send/tests/send-container.test.js create mode 100644 ui/app/components/app/send/tests/send-selectors-test-data.js create mode 100644 ui/app/components/app/send/tests/send-selectors.test.js create mode 100644 ui/app/components/app/send/tests/send-utils.test.js create mode 100644 ui/app/components/app/send/to-autocomplete.component.js create mode 100644 ui/app/components/app/send/to-autocomplete/index.js create mode 100644 ui/app/components/app/send/to-autocomplete/to-autocomplete.js create mode 100644 ui/app/components/app/shapeshift-form.js create mode 100644 ui/app/components/app/shift-list-item.js create mode 100644 ui/app/components/app/sidebars/index.js create mode 100644 ui/app/components/app/sidebars/index.scss create mode 100644 ui/app/components/app/sidebars/sidebar-content.scss create mode 100644 ui/app/components/app/sidebars/sidebar.component.js create mode 100644 ui/app/components/app/sidebars/sidebar.constants.js create mode 100644 ui/app/components/app/sidebars/tests/sidebars-component.test.js create mode 100644 ui/app/components/app/signature-request.js create mode 100644 ui/app/components/app/tab-bar.js create mode 100644 ui/app/components/app/token-cell.js create mode 100644 ui/app/components/app/token-list.js create mode 100644 ui/app/components/app/transaction-action/index.js create mode 100644 ui/app/components/app/transaction-action/tests/transaction-action.component.test.js create mode 100644 ui/app/components/app/transaction-action/transaction-action.component.js create mode 100644 ui/app/components/app/transaction-activity-log/index.js create mode 100644 ui/app/components/app/transaction-activity-log/index.scss create mode 100644 ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js create mode 100644 ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js create mode 100644 ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js create mode 100644 ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js create mode 100644 ui/app/components/app/transaction-breakdown/index.js create mode 100644 ui/app/components/app/transaction-breakdown/index.scss create mode 100644 ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js create mode 100644 ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js create mode 100644 ui/app/components/app/transaction-list-item-details/index.js create mode 100644 ui/app/components/app/transaction-list-item-details/index.scss create mode 100644 ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js create mode 100644 ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js create mode 100644 ui/app/components/app/transaction-list-item/index.js create mode 100644 ui/app/components/app/transaction-list-item/index.scss create mode 100644 ui/app/components/app/transaction-list-item/transaction-list-item.component.js create mode 100644 ui/app/components/app/transaction-list-item/transaction-list-item.container.js create mode 100644 ui/app/components/app/transaction-list/index.js create mode 100644 ui/app/components/app/transaction-list/index.scss create mode 100644 ui/app/components/app/transaction-list/transaction-list.component.js create mode 100644 ui/app/components/app/transaction-list/transaction-list.container.js create mode 100644 ui/app/components/app/transaction-status/index.js create mode 100644 ui/app/components/app/transaction-status/index.scss create mode 100644 ui/app/components/app/transaction-status/tests/transaction-status.component.test.js create mode 100644 ui/app/components/app/transaction-status/transaction-status.component.js create mode 100644 ui/app/components/app/transaction-view-balance/index.js create mode 100644 ui/app/components/app/transaction-view-balance/index.scss create mode 100644 ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js create mode 100644 ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js create mode 100644 ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js create mode 100644 ui/app/components/app/transaction-view/index.js create mode 100644 ui/app/components/app/transaction-view/index.scss create mode 100644 ui/app/components/app/transaction-view/transaction-view.component.js create mode 100644 ui/app/components/app/ui-migration-annoucement/index.js create mode 100644 ui/app/components/app/ui-migration-annoucement/index.scss create mode 100644 ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js create mode 100644 ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/index.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/index.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js create mode 100644 ui/app/components/app/user-preferenced-token-input/index.js create mode 100644 ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js create mode 100644 ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js create mode 100644 ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js create mode 100644 ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js create mode 100644 ui/app/components/app/wallet-view.js delete mode 100644 ui/app/components/balance/balance.component.js delete mode 100644 ui/app/components/balance/balance.container.js delete mode 100644 ui/app/components/balance/index.js delete mode 100644 ui/app/components/bn-as-decimal-input.js delete mode 100644 ui/app/components/breadcrumbs/breadcrumbs.component.js delete mode 100644 ui/app/components/breadcrumbs/index.js delete mode 100644 ui/app/components/breadcrumbs/index.scss delete mode 100644 ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js delete mode 100644 ui/app/components/button-group/button-group.component.js delete mode 100644 ui/app/components/button-group/button-group.stories.js delete mode 100644 ui/app/components/button-group/index.js delete mode 100644 ui/app/components/button-group/index.scss delete mode 100644 ui/app/components/button-group/tests/button-group-component.test.js delete mode 100644 ui/app/components/button/button.component.js delete mode 100644 ui/app/components/button/button.stories.js delete mode 100644 ui/app/components/button/index.js delete mode 100644 ui/app/components/card/card.component.js delete mode 100644 ui/app/components/card/index.js delete mode 100644 ui/app/components/card/index.scss delete mode 100644 ui/app/components/card/tests/card.component.test.js delete mode 100644 ui/app/components/coinbase-form.js delete mode 100644 ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js delete mode 100644 ui/app/components/confirm-page-container/confirm-detail-row/index.js delete mode 100644 ui/app/components/confirm-page-container/confirm-detail-row/index.scss delete mode 100644 ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/index.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-content/index.scss delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-header/index.js delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container-header/index.scss delete mode 100755 ui/app/components/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js delete mode 100755 ui/app/components/confirm-page-container/confirm-page-container-navigation/index.js delete mode 100755 ui/app/components/confirm-page-container/confirm-page-container-navigation/index.scss delete mode 100644 ui/app/components/confirm-page-container/confirm-page-container.component.js delete mode 100644 ui/app/components/confirm-page-container/index.js delete mode 100644 ui/app/components/confirm-page-container/index.scss delete mode 100644 ui/app/components/copyButton.js delete mode 100644 ui/app/components/copyable.js delete mode 100644 ui/app/components/currency-display/currency-display.component.js delete mode 100644 ui/app/components/currency-display/currency-display.container.js delete mode 100644 ui/app/components/currency-display/index.js delete mode 100644 ui/app/components/currency-display/index.scss delete mode 100644 ui/app/components/currency-display/tests/currency-display.component.test.js delete mode 100644 ui/app/components/currency-display/tests/currency-display.container.test.js delete mode 100644 ui/app/components/currency-input/currency-input.component.js delete mode 100644 ui/app/components/currency-input/currency-input.container.js delete mode 100644 ui/app/components/currency-input/index.js delete mode 100644 ui/app/components/currency-input/index.scss delete mode 100644 ui/app/components/currency-input/tests/currency-input.component.test.js delete mode 100644 ui/app/components/currency-input/tests/currency-input.container.test.js delete mode 100644 ui/app/components/customize-gas-modal/gas-modal-card.js delete mode 100644 ui/app/components/customize-gas-modal/gas-slider.js delete mode 100644 ui/app/components/customize-gas-modal/index.js delete mode 100644 ui/app/components/dropdowns/account-details-dropdown.js delete mode 100644 ui/app/components/dropdowns/components/account-dropdowns.js delete mode 100644 ui/app/components/dropdowns/components/dropdown.js delete mode 100644 ui/app/components/dropdowns/components/menu.js delete mode 100644 ui/app/components/dropdowns/components/network-dropdown-icon.js delete mode 100644 ui/app/components/dropdowns/index.js delete mode 100644 ui/app/components/dropdowns/network-dropdown.js delete mode 100644 ui/app/components/dropdowns/simple-dropdown.js delete mode 100644 ui/app/components/dropdowns/tests/dropdown.test.js delete mode 100644 ui/app/components/dropdowns/tests/menu.test.js delete mode 100644 ui/app/components/dropdowns/tests/network-dropdown-icon.test.js delete mode 100644 ui/app/components/dropdowns/tests/network-dropdown.test.js delete mode 100644 ui/app/components/dropdowns/token-menu-dropdown.js delete mode 100644 ui/app/components/editable-label.js delete mode 100644 ui/app/components/ens-input.js delete mode 100644 ui/app/components/error-message/error-message.component.js delete mode 100644 ui/app/components/error-message/index.js delete mode 100644 ui/app/components/error-message/index.scss delete mode 100644 ui/app/components/error-message/tests/error-message.component.test.js delete mode 100644 ui/app/components/eth-balance.js delete mode 100644 ui/app/components/export-text-container/export-text-container.component.js delete mode 100644 ui/app/components/export-text-container/index.js delete mode 100644 ui/app/components/export-text-container/index.scss delete mode 100644 ui/app/components/fiat-value.js delete mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js delete mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js delete mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/index.js delete mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/index.scss delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/index.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/index.scss delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js delete mode 100644 ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js delete mode 100644 ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js delete mode 100644 ui/app/components/gas-customization/gas-price-button-group/index.js delete mode 100644 ui/app/components/gas-customization/gas-price-button-group/index.scss delete mode 100644 ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js delete mode 100644 ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js delete mode 100644 ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js delete mode 100644 ui/app/components/gas-customization/gas-price-chart/index.js delete mode 100644 ui/app/components/gas-customization/gas-price-chart/index.scss delete mode 100644 ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js delete mode 100644 ui/app/components/gas-customization/gas-slider/gas-slider.component.js delete mode 100644 ui/app/components/gas-customization/gas-slider/index.js delete mode 100644 ui/app/components/gas-customization/gas-slider/index.scss delete mode 100644 ui/app/components/gas-customization/gas.selectors.js delete mode 100644 ui/app/components/gas-customization/index.scss delete mode 100644 ui/app/components/hex-to-decimal/hex-to-decimal.component.js delete mode 100644 ui/app/components/hex-to-decimal/index.js delete mode 100644 ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js delete mode 100644 ui/app/components/identicon/identicon.component.js delete mode 100644 ui/app/components/identicon/identicon.container.js delete mode 100644 ui/app/components/identicon/index.js delete mode 100644 ui/app/components/identicon/index.scss delete mode 100644 ui/app/components/identicon/tests/identicon.component.test.js delete mode 100644 ui/app/components/index.scss delete mode 100644 ui/app/components/info-box/index.js delete mode 100644 ui/app/components/info-box/index.scss delete mode 100644 ui/app/components/info-box/info-box.component.js delete mode 100644 ui/app/components/input-number.js delete mode 100644 ui/app/components/jazzicon/index.js delete mode 100644 ui/app/components/jazzicon/jazzicon.component.js delete mode 100644 ui/app/components/loading-network-screen/index.js delete mode 100644 ui/app/components/loading-network-screen/loading-network-screen.component.js delete mode 100644 ui/app/components/loading-network-screen/loading-network-screen.container.js delete mode 100644 ui/app/components/loading-screen/index.js delete mode 100644 ui/app/components/loading-screen/loading-screen.component.js delete mode 100644 ui/app/components/lock-icon/index.js delete mode 100644 ui/app/components/lock-icon/lock-icon.component.js delete mode 100644 ui/app/components/mascot.js delete mode 100644 ui/app/components/menu-bar/index.js delete mode 100644 ui/app/components/menu-bar/index.scss delete mode 100644 ui/app/components/menu-bar/menu-bar.component.js delete mode 100644 ui/app/components/menu-bar/menu-bar.container.js delete mode 100644 ui/app/components/menu-droppo.js delete mode 100644 ui/app/components/modal/index.js delete mode 100644 ui/app/components/modal/index.scss delete mode 100644 ui/app/components/modal/modal-content/index.js delete mode 100644 ui/app/components/modal/modal-content/index.scss delete mode 100644 ui/app/components/modal/modal-content/modal-content.component.js delete mode 100644 ui/app/components/modal/modal-content/tests/modal-content.component.test.js delete mode 100644 ui/app/components/modal/modal.component.js delete mode 100644 ui/app/components/modal/tests/modal.component.test.js delete mode 100644 ui/app/components/modals/account-details-modal.js delete mode 100644 ui/app/components/modals/account-modal-container.js delete mode 100644 ui/app/components/modals/buy-options-modal.js delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction.component.js delete mode 100644 ui/app/components/modals/cancel-transaction/cancel-transaction.container.js delete mode 100644 ui/app/components/modals/cancel-transaction/index.js delete mode 100644 ui/app/components/modals/cancel-transaction/index.scss delete mode 100644 ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js delete mode 100644 ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js delete mode 100644 ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js delete mode 100644 ui/app/components/modals/clear-approved-origins/index.js delete mode 100644 ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js delete mode 100644 ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js delete mode 100644 ui/app/components/modals/confirm-remove-account/index.js delete mode 100644 ui/app/components/modals/confirm-remove-account/index.scss delete mode 100644 ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js delete mode 100644 ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js delete mode 100644 ui/app/components/modals/confirm-reset-account/index.js delete mode 100644 ui/app/components/modals/customize-gas/customize-gas.component.js delete mode 100644 ui/app/components/modals/customize-gas/customize-gas.container.js delete mode 100644 ui/app/components/modals/customize-gas/customize-gas.util.js delete mode 100644 ui/app/components/modals/customize-gas/index.js delete mode 100644 ui/app/components/modals/customize-gas/index.scss delete mode 100644 ui/app/components/modals/deposit-ether-modal.js delete mode 100644 ui/app/components/modals/edit-account-name-modal.js delete mode 100644 ui/app/components/modals/export-private-key-modal.js delete mode 100644 ui/app/components/modals/hide-token-confirmation-modal.js delete mode 100644 ui/app/components/modals/index.js delete mode 100644 ui/app/components/modals/index.scss delete mode 100644 ui/app/components/modals/loading-network-error/index.js delete mode 100644 ui/app/components/modals/loading-network-error/loading-network-error.component.js delete mode 100644 ui/app/components/modals/loading-network-error/loading-network-error.container.js delete mode 100644 ui/app/components/modals/metametrics-opt-in-modal/index.js delete mode 100644 ui/app/components/modals/metametrics-opt-in-modal/index.scss delete mode 100644 ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js delete mode 100644 ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js delete mode 100644 ui/app/components/modals/modal.js delete mode 100644 ui/app/components/modals/new-account-modal.js delete mode 100644 ui/app/components/modals/notification-modal.js delete mode 100644 ui/app/components/modals/qr-scanner/index.js delete mode 100644 ui/app/components/modals/qr-scanner/index.scss delete mode 100644 ui/app/components/modals/qr-scanner/qr-scanner.component.js delete mode 100644 ui/app/components/modals/qr-scanner/qr-scanner.container.js delete mode 100644 ui/app/components/modals/reject-transactions/index.js delete mode 100644 ui/app/components/modals/reject-transactions/index.scss delete mode 100644 ui/app/components/modals/reject-transactions/reject-transactions.component.js delete mode 100644 ui/app/components/modals/reject-transactions/reject-transactions.container.js delete mode 100644 ui/app/components/modals/shapeshift-deposit-tx-modal.js delete mode 100644 ui/app/components/modals/transaction-confirmed/index.js delete mode 100644 ui/app/components/modals/transaction-confirmed/index.scss delete mode 100644 ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js delete mode 100644 ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js delete mode 100644 ui/app/components/network-display/index.js delete mode 100644 ui/app/components/network-display/index.scss delete mode 100644 ui/app/components/network-display/network-display.component.js delete mode 100644 ui/app/components/network-display/network-display.container.js delete mode 100644 ui/app/components/network.js delete mode 100644 ui/app/components/notice.js delete mode 100644 ui/app/components/page-container/index.js delete mode 100644 ui/app/components/page-container/index.scss delete mode 100644 ui/app/components/page-container/page-container-content.component.js delete mode 100644 ui/app/components/page-container/page-container-footer/index.js delete mode 100644 ui/app/components/page-container/page-container-footer/page-container-footer.component.js delete mode 100644 ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js delete mode 100644 ui/app/components/page-container/page-container-header/index.js delete mode 100644 ui/app/components/page-container/page-container-header/page-container-header.component.js delete mode 100644 ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js delete mode 100644 ui/app/components/page-container/page-container.component.js delete mode 100644 ui/app/components/page-container/tests/page-container.component.test.js delete mode 100644 ui/app/components/pages/add-token/add-token.component.js delete mode 100644 ui/app/components/pages/add-token/add-token.container.js delete mode 100644 ui/app/components/pages/add-token/index.js delete mode 100644 ui/app/components/pages/add-token/index.scss delete mode 100644 ui/app/components/pages/add-token/token-list/index.js delete mode 100644 ui/app/components/pages/add-token/token-list/index.scss delete mode 100644 ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js delete mode 100644 ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss delete mode 100644 ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js delete mode 100644 ui/app/components/pages/add-token/token-list/token-list.component.js delete mode 100644 ui/app/components/pages/add-token/token-list/token-list.container.js delete mode 100644 ui/app/components/pages/add-token/token-search/index.js delete mode 100644 ui/app/components/pages/add-token/token-search/token-search.component.js delete mode 100644 ui/app/components/pages/add-token/util.js delete mode 100644 ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js delete mode 100644 ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js delete mode 100644 ui/app/components/pages/confirm-add-suggested-token/index.js delete mode 100644 ui/app/components/pages/confirm-add-token/confirm-add-token.component.js delete mode 100644 ui/app/components/pages/confirm-add-token/confirm-add-token.container.js delete mode 100644 ui/app/components/pages/confirm-add-token/index.js delete mode 100644 ui/app/components/pages/confirm-add-token/index.scss delete mode 100644 ui/app/components/pages/confirm-approve/confirm-approve.component.js delete mode 100644 ui/app/components/pages/confirm-approve/confirm-approve.container.js delete mode 100644 ui/app/components/pages/confirm-approve/index.js delete mode 100644 ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js delete mode 100644 ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js delete mode 100644 ui/app/components/pages/confirm-deploy-contract/index.js delete mode 100644 ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js delete mode 100644 ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js delete mode 100644 ui/app/components/pages/confirm-send-ether/index.js delete mode 100644 ui/app/components/pages/confirm-send-token/confirm-send-token.component.js delete mode 100644 ui/app/components/pages/confirm-send-token/confirm-send-token.container.js delete mode 100644 ui/app/components/pages/confirm-send-token/index.js delete mode 100644 ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js delete mode 100644 ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js delete mode 100644 ui/app/components/pages/confirm-token-transaction-base/index.js delete mode 100644 ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js delete mode 100644 ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js delete mode 100644 ui/app/components/pages/confirm-transaction-base/index.js delete mode 100644 ui/app/components/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js delete mode 100644 ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js delete mode 100644 ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js delete mode 100644 ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js delete mode 100644 ui/app/components/pages/confirm-transaction-switch/index.js delete mode 100644 ui/app/components/pages/confirm-transaction/confirm-transaction.component.js delete mode 100644 ui/app/components/pages/confirm-transaction/confirm-transaction.container.js delete mode 100644 ui/app/components/pages/confirm-transaction/index.js delete mode 100644 ui/app/components/pages/create-account/connect-hardware/account-list.js delete mode 100644 ui/app/components/pages/create-account/connect-hardware/connect-screen.js delete mode 100644 ui/app/components/pages/create-account/connect-hardware/index.js delete mode 100644 ui/app/components/pages/create-account/import-account/index.js delete mode 100644 ui/app/components/pages/create-account/import-account/json.js delete mode 100644 ui/app/components/pages/create-account/import-account/private-key.js delete mode 100644 ui/app/components/pages/create-account/import-account/seed.js delete mode 100644 ui/app/components/pages/create-account/index.js delete mode 100644 ui/app/components/pages/create-account/new-account.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/create-password.component.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/create-password.container.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/index.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/new-account/index.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/unique-image/index.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js delete mode 100644 ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js delete mode 100644 ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js delete mode 100644 ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js delete mode 100644 ui/app/components/pages/first-time-flow/end-of-flow/index.js delete mode 100644 ui/app/components/pages/first-time-flow/end-of-flow/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow.component.js delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow.container.js delete mode 100644 ui/app/components/pages/first-time-flow/first-time-flow.selectors.js delete mode 100644 ui/app/components/pages/first-time-flow/index.js delete mode 100644 ui/app/components/pages/first-time-flow/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js delete mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js delete mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/index.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js delete mode 100644 ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js delete mode 100644 ui/app/components/pages/first-time-flow/select-action/index.js delete mode 100644 ui/app/components/pages/first-time-flow/select-action/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/select-action/select-action.component.js delete mode 100644 ui/app/components/pages/first-time-flow/select-action/select-action.container.js delete mode 100644 ui/app/components/pages/first-time-flow/welcome/index.js delete mode 100644 ui/app/components/pages/first-time-flow/welcome/index.scss delete mode 100644 ui/app/components/pages/first-time-flow/welcome/welcome.component.js delete mode 100644 ui/app/components/pages/first-time-flow/welcome/welcome.container.js delete mode 100644 ui/app/components/pages/home/home.component.js delete mode 100644 ui/app/components/pages/home/home.container.js delete mode 100644 ui/app/components/pages/home/index.js delete mode 100644 ui/app/components/pages/index.scss delete mode 100644 ui/app/components/pages/keychains/index.scss delete mode 100644 ui/app/components/pages/keychains/restore-vault.js delete mode 100644 ui/app/components/pages/keychains/reveal-seed.js delete mode 100644 ui/app/components/pages/lock/index.js delete mode 100644 ui/app/components/pages/lock/lock.component.js delete mode 100644 ui/app/components/pages/lock/lock.container.js delete mode 100644 ui/app/components/pages/mobile-sync/index.js delete mode 100644 ui/app/components/pages/notice.js delete mode 100644 ui/app/components/pages/provider-approval/index.js delete mode 100644 ui/app/components/pages/provider-approval/provider-approval.component.js delete mode 100644 ui/app/components/pages/provider-approval/provider-approval.container.js delete mode 100644 ui/app/components/pages/settings/index.js delete mode 100644 ui/app/components/pages/settings/index.scss delete mode 100644 ui/app/components/pages/settings/info-tab/index.js delete mode 100644 ui/app/components/pages/settings/info-tab/index.scss delete mode 100644 ui/app/components/pages/settings/info-tab/info-tab.component.js delete mode 100644 ui/app/components/pages/settings/settings-tab/index.js delete mode 100644 ui/app/components/pages/settings/settings-tab/index.scss delete mode 100644 ui/app/components/pages/settings/settings-tab/settings-tab.component.js delete mode 100644 ui/app/components/pages/settings/settings-tab/settings-tab.container.js delete mode 100644 ui/app/components/pages/settings/settings.component.js delete mode 100644 ui/app/components/pages/unlock-page/index.js delete mode 100644 ui/app/components/pages/unlock-page/index.scss delete mode 100644 ui/app/components/pages/unlock-page/unlock-page.component.js delete mode 100644 ui/app/components/pages/unlock-page/unlock-page.container.js delete mode 100644 ui/app/components/provider-page-container/index.js delete mode 100644 ui/app/components/provider-page-container/index.scss delete mode 100644 ui/app/components/provider-page-container/provider-page-container-content/index.js delete mode 100644 ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js delete mode 100644 ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js delete mode 100644 ui/app/components/provider-page-container/provider-page-container-header/index.js delete mode 100644 ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js delete mode 100644 ui/app/components/provider-page-container/provider-page-container.component.js delete mode 100644 ui/app/components/qr-code.js delete mode 100644 ui/app/components/readonly-input.js delete mode 100644 ui/app/components/selected-account/index.js delete mode 100644 ui/app/components/selected-account/index.scss delete mode 100644 ui/app/components/selected-account/selected-account.component.js delete mode 100644 ui/app/components/selected-account/selected-account.container.js delete mode 100644 ui/app/components/selected-account/tests/selected-account-component.test.js delete mode 100644 ui/app/components/send/README.md delete mode 100644 ui/app/components/send/account-list-item/account-list-item-README.md delete mode 100644 ui/app/components/send/account-list-item/account-list-item.component.js delete mode 100644 ui/app/components/send/account-list-item/account-list-item.container.js delete mode 100644 ui/app/components/send/account-list-item/index.js delete mode 100644 ui/app/components/send/account-list-item/tests/account-list-item-component.test.js delete mode 100644 ui/app/components/send/account-list-item/tests/account-list-item-container.test.js delete mode 100644 ui/app/components/send/index.js delete mode 100644 ui/app/components/send/send-content/index.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/README.md delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/index.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.scss delete mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js delete mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js delete mode 100644 ui/app/components/send/send-content/send-content.component.js delete mode 100644 ui/app/components/send/send-content/send-dropdown-list/index.js delete mode 100644 ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js delete mode 100644 ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js delete mode 100644 ui/app/components/send/send-content/send-from-row/index.js delete mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.component.js delete mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.container.js delete mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js delete mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js delete mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js delete mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/README.md delete mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/index.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.scss delete mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js delete mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js delete mode 100644 ui/app/components/send/send-content/send-hex-data-row/index.js delete mode 100644 ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js delete mode 100644 ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/index.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/index.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss delete mode 100644 ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js delete mode 100644 ui/app/components/send/send-content/send-to-row/index.js delete mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row-README.md delete mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.component.js delete mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.container.js delete mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js delete mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.utils.js delete mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js delete mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js delete mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js delete mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js delete mode 100644 ui/app/components/send/send-content/tests/send-content-component.test.js delete mode 100644 ui/app/components/send/send-footer/README.md delete mode 100644 ui/app/components/send/send-footer/index.js delete mode 100644 ui/app/components/send/send-footer/send-footer.component.js delete mode 100644 ui/app/components/send/send-footer/send-footer.container.js delete mode 100644 ui/app/components/send/send-footer/send-footer.scss delete mode 100644 ui/app/components/send/send-footer/send-footer.selectors.js delete mode 100644 ui/app/components/send/send-footer/send-footer.utils.js delete mode 100644 ui/app/components/send/send-footer/tests/send-footer-component.test.js delete mode 100644 ui/app/components/send/send-footer/tests/send-footer-container.test.js delete mode 100644 ui/app/components/send/send-footer/tests/send-footer-selectors.test.js delete mode 100644 ui/app/components/send/send-footer/tests/send-footer-utils.test.js delete mode 100644 ui/app/components/send/send-header/README.md delete mode 100644 ui/app/components/send/send-header/index.js delete mode 100644 ui/app/components/send/send-header/send-header.component.js delete mode 100644 ui/app/components/send/send-header/send-header.container.js delete mode 100644 ui/app/components/send/send-header/send-header.selectors.js delete mode 100644 ui/app/components/send/send-header/tests/send-header-component.test.js delete mode 100644 ui/app/components/send/send-header/tests/send-header-container.test.js delete mode 100644 ui/app/components/send/send-header/tests/send-header-selectors.test.js delete mode 100644 ui/app/components/send/send.component.js delete mode 100644 ui/app/components/send/send.constants.js delete mode 100644 ui/app/components/send/send.container.js delete mode 100644 ui/app/components/send/send.scss delete mode 100644 ui/app/components/send/send.selectors.js delete mode 100644 ui/app/components/send/send.utils.js delete mode 100644 ui/app/components/send/tests/send-component.test.js delete mode 100644 ui/app/components/send/tests/send-container.test.js delete mode 100644 ui/app/components/send/tests/send-selectors-test-data.js delete mode 100644 ui/app/components/send/tests/send-selectors.test.js delete mode 100644 ui/app/components/send/tests/send-utils.test.js delete mode 100644 ui/app/components/send/to-autocomplete.component.js delete mode 100644 ui/app/components/send/to-autocomplete/index.js delete mode 100644 ui/app/components/send/to-autocomplete/to-autocomplete.js delete mode 100644 ui/app/components/sender-to-recipient/index.js delete mode 100644 ui/app/components/sender-to-recipient/index.scss delete mode 100644 ui/app/components/sender-to-recipient/sender-to-recipient.component.js delete mode 100644 ui/app/components/sender-to-recipient/sender-to-recipient.constants.js delete mode 100644 ui/app/components/shapeshift-form.js delete mode 100644 ui/app/components/shift-list-item.js delete mode 100644 ui/app/components/sidebars/index.js delete mode 100644 ui/app/components/sidebars/index.scss delete mode 100644 ui/app/components/sidebars/sidebar-content.scss delete mode 100644 ui/app/components/sidebars/sidebar.component.js delete mode 100644 ui/app/components/sidebars/sidebar.constants.js delete mode 100644 ui/app/components/sidebars/tests/sidebars-component.test.js delete mode 100644 ui/app/components/signature-request.js delete mode 100644 ui/app/components/spinner/index.js delete mode 100644 ui/app/components/spinner/spinner.component.js delete mode 100644 ui/app/components/tab-bar.js delete mode 100644 ui/app/components/tabs/index.js delete mode 100644 ui/app/components/tabs/index.scss delete mode 100644 ui/app/components/tabs/tab/index.js delete mode 100644 ui/app/components/tabs/tab/index.scss delete mode 100644 ui/app/components/tabs/tab/tab.component.js delete mode 100644 ui/app/components/tabs/tabs.component.js delete mode 100644 ui/app/components/text-field/index.js delete mode 100644 ui/app/components/text-field/text-field.component.js delete mode 100644 ui/app/components/text-field/text-field.stories.js delete mode 100644 ui/app/components/token-balance/index.js delete mode 100644 ui/app/components/token-balance/index.scss delete mode 100644 ui/app/components/token-balance/token-balance.component.js delete mode 100644 ui/app/components/token-balance/token-balance.container.js delete mode 100644 ui/app/components/token-cell.js delete mode 100644 ui/app/components/token-currency-display/index.js delete mode 100644 ui/app/components/token-currency-display/token-currency-display.component.js delete mode 100644 ui/app/components/token-input/index.js delete mode 100644 ui/app/components/token-input/tests/token-input.component.test.js delete mode 100644 ui/app/components/token-input/tests/token-input.container.test.js delete mode 100644 ui/app/components/token-input/token-input.component.js delete mode 100644 ui/app/components/token-input/token-input.container.js delete mode 100644 ui/app/components/token-list.js delete mode 100644 ui/app/components/tooltip-v2.js delete mode 100644 ui/app/components/tooltip.js delete mode 100644 ui/app/components/transaction-action/index.js delete mode 100644 ui/app/components/transaction-action/tests/transaction-action.component.test.js delete mode 100644 ui/app/components/transaction-action/transaction-action.component.js delete mode 100644 ui/app/components/transaction-activity-log/index.js delete mode 100644 ui/app/components/transaction-activity-log/index.scss delete mode 100644 ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js delete mode 100644 ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js delete mode 100644 ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log.component.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log.constants.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log.container.js delete mode 100644 ui/app/components/transaction-activity-log/transaction-activity-log.util.js delete mode 100644 ui/app/components/transaction-breakdown/index.js delete mode 100644 ui/app/components/transaction-breakdown/index.scss delete mode 100644 ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown.component.js delete mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown.container.js delete mode 100644 ui/app/components/transaction-list-item-details/index.js delete mode 100644 ui/app/components/transaction-list-item-details/index.scss delete mode 100644 ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js delete mode 100644 ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js delete mode 100644 ui/app/components/transaction-list-item/index.js delete mode 100644 ui/app/components/transaction-list-item/index.scss delete mode 100644 ui/app/components/transaction-list-item/transaction-list-item.component.js delete mode 100644 ui/app/components/transaction-list-item/transaction-list-item.container.js delete mode 100644 ui/app/components/transaction-list/index.js delete mode 100644 ui/app/components/transaction-list/index.scss delete mode 100644 ui/app/components/transaction-list/transaction-list.component.js delete mode 100644 ui/app/components/transaction-list/transaction-list.container.js delete mode 100644 ui/app/components/transaction-status/index.js delete mode 100644 ui/app/components/transaction-status/index.scss delete mode 100644 ui/app/components/transaction-status/tests/transaction-status.component.test.js delete mode 100644 ui/app/components/transaction-status/transaction-status.component.js delete mode 100644 ui/app/components/transaction-view-balance/index.js delete mode 100644 ui/app/components/transaction-view-balance/index.scss delete mode 100644 ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js delete mode 100644 ui/app/components/transaction-view-balance/transaction-view-balance.component.js delete mode 100644 ui/app/components/transaction-view-balance/transaction-view-balance.container.js delete mode 100644 ui/app/components/transaction-view/index.js delete mode 100644 ui/app/components/transaction-view/index.scss delete mode 100644 ui/app/components/transaction-view/transaction-view.component.js delete mode 100644 ui/app/components/ui-migration-annoucement/index.js delete mode 100644 ui/app/components/ui-migration-annoucement/index.scss delete mode 100644 ui/app/components/ui-migration-annoucement/ui-migration-annoucement.component.js delete mode 100644 ui/app/components/ui-migration-annoucement/ui-migration-announcement.container.js create mode 100644 ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js create mode 100644 ui/app/components/ui/account-dropdown-mini/index.js create mode 100644 ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js create mode 100644 ui/app/components/ui/alert/index.js create mode 100644 ui/app/components/ui/balance/balance.component.js create mode 100644 ui/app/components/ui/balance/balance.container.js create mode 100644 ui/app/components/ui/balance/index.js create mode 100644 ui/app/components/ui/breadcrumbs/breadcrumbs.component.js create mode 100644 ui/app/components/ui/breadcrumbs/index.js create mode 100644 ui/app/components/ui/breadcrumbs/index.scss create mode 100644 ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js create mode 100644 ui/app/components/ui/button-group/button-group.component.js create mode 100644 ui/app/components/ui/button-group/button-group.stories.js create mode 100644 ui/app/components/ui/button-group/index.js create mode 100644 ui/app/components/ui/button-group/index.scss create mode 100644 ui/app/components/ui/button-group/tests/button-group-component.test.js create mode 100644 ui/app/components/ui/button/button.component.js create mode 100644 ui/app/components/ui/button/button.stories.js create mode 100644 ui/app/components/ui/button/index.js create mode 100644 ui/app/components/ui/card/card.component.js create mode 100644 ui/app/components/ui/card/index.js create mode 100644 ui/app/components/ui/card/index.scss create mode 100644 ui/app/components/ui/card/tests/card.component.test.js create mode 100644 ui/app/components/ui/copyButton.js create mode 100644 ui/app/components/ui/currency-display/currency-display.component.js create mode 100644 ui/app/components/ui/currency-display/currency-display.container.js create mode 100644 ui/app/components/ui/currency-display/index.js create mode 100644 ui/app/components/ui/currency-display/index.scss create mode 100644 ui/app/components/ui/currency-display/tests/currency-display.component.test.js create mode 100644 ui/app/components/ui/currency-display/tests/currency-display.container.test.js create mode 100644 ui/app/components/ui/currency-input/currency-input.component.js create mode 100644 ui/app/components/ui/currency-input/currency-input.container.js create mode 100644 ui/app/components/ui/currency-input/index.js create mode 100644 ui/app/components/ui/currency-input/index.scss create mode 100644 ui/app/components/ui/currency-input/tests/currency-input.component.test.js create mode 100644 ui/app/components/ui/currency-input/tests/currency-input.container.test.js create mode 100644 ui/app/components/ui/editable-label.js create mode 100644 ui/app/components/ui/error-message/error-message.component.js create mode 100644 ui/app/components/ui/error-message/index.js create mode 100644 ui/app/components/ui/error-message/index.scss create mode 100644 ui/app/components/ui/error-message/tests/error-message.component.test.js create mode 100644 ui/app/components/ui/eth-balance.js create mode 100644 ui/app/components/ui/export-text-container/export-text-container.component.js create mode 100644 ui/app/components/ui/export-text-container/index.js create mode 100644 ui/app/components/ui/export-text-container/index.scss create mode 100644 ui/app/components/ui/fiat-value.js create mode 100644 ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js create mode 100644 ui/app/components/ui/hex-to-decimal/index.js create mode 100644 ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js create mode 100644 ui/app/components/ui/identicon/identicon.component.js create mode 100644 ui/app/components/ui/identicon/identicon.container.js create mode 100644 ui/app/components/ui/identicon/index.js create mode 100644 ui/app/components/ui/identicon/index.scss create mode 100644 ui/app/components/ui/identicon/tests/identicon.component.test.js create mode 100644 ui/app/components/ui/jazzicon/index.js create mode 100644 ui/app/components/ui/jazzicon/jazzicon.component.js create mode 100644 ui/app/components/ui/loading-screen/index.js create mode 100644 ui/app/components/ui/loading-screen/loading-screen.component.js create mode 100644 ui/app/components/ui/lock-icon/index.js create mode 100644 ui/app/components/ui/lock-icon/lock-icon.component.js create mode 100644 ui/app/components/ui/mascot.js create mode 100644 ui/app/components/ui/page-container/index.js create mode 100644 ui/app/components/ui/page-container/index.scss create mode 100644 ui/app/components/ui/page-container/page-container-content.component.js create mode 100644 ui/app/components/ui/page-container/page-container-footer/index.js create mode 100644 ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js create mode 100644 ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js create mode 100644 ui/app/components/ui/page-container/page-container-header/index.js create mode 100644 ui/app/components/ui/page-container/page-container-header/page-container-header.component.js create mode 100644 ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js create mode 100644 ui/app/components/ui/page-container/page-container.component.js create mode 100644 ui/app/components/ui/page-container/tests/page-container.component.test.js create mode 100644 ui/app/components/ui/qr-code.js create mode 100644 ui/app/components/ui/readonly-input.js create mode 100644 ui/app/components/ui/sender-to-recipient/index.js create mode 100644 ui/app/components/ui/sender-to-recipient/index.scss create mode 100644 ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js create mode 100644 ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js create mode 100644 ui/app/components/ui/spinner/index.js create mode 100644 ui/app/components/ui/spinner/spinner.component.js create mode 100644 ui/app/components/ui/tabs/index.js create mode 100644 ui/app/components/ui/tabs/index.scss create mode 100644 ui/app/components/ui/tabs/tab/index.js create mode 100644 ui/app/components/ui/tabs/tab/index.scss create mode 100644 ui/app/components/ui/tabs/tab/tab.component.js create mode 100644 ui/app/components/ui/tabs/tabs.component.js create mode 100644 ui/app/components/ui/text-field/index.js create mode 100644 ui/app/components/ui/text-field/text-field.component.js create mode 100644 ui/app/components/ui/text-field/text-field.stories.js create mode 100644 ui/app/components/ui/token-balance/index.js create mode 100644 ui/app/components/ui/token-balance/index.scss create mode 100644 ui/app/components/ui/token-balance/token-balance.component.js create mode 100644 ui/app/components/ui/token-balance/token-balance.container.js create mode 100644 ui/app/components/ui/token-currency-display/index.js create mode 100644 ui/app/components/ui/token-currency-display/token-currency-display.component.js create mode 100644 ui/app/components/ui/token-input/index.js create mode 100644 ui/app/components/ui/token-input/tests/token-input.component.test.js create mode 100644 ui/app/components/ui/token-input/tests/token-input.container.test.js create mode 100644 ui/app/components/ui/token-input/token-input.component.js create mode 100644 ui/app/components/ui/token-input/token-input.container.js create mode 100644 ui/app/components/ui/tooltip-v2.js create mode 100644 ui/app/components/ui/tooltip.js create mode 100644 ui/app/components/ui/unit-input/index.js create mode 100644 ui/app/components/ui/unit-input/index.scss create mode 100644 ui/app/components/ui/unit-input/tests/unit-input.component.test.js create mode 100644 ui/app/components/ui/unit-input/unit-input.component.js delete mode 100644 ui/app/components/unit-input/index.js delete mode 100644 ui/app/components/unit-input/index.scss delete mode 100644 ui/app/components/unit-input/tests/unit-input.component.test.js delete mode 100644 ui/app/components/unit-input/unit-input.component.js delete mode 100644 ui/app/components/user-preferenced-currency-display/index.js delete mode 100644 ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js delete mode 100644 ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js delete mode 100644 ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js delete mode 100644 ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js delete mode 100644 ui/app/components/user-preferenced-currency-input/index.js delete mode 100644 ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js delete mode 100644 ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js delete mode 100644 ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js delete mode 100644 ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js delete mode 100644 ui/app/components/user-preferenced-token-input/index.js delete mode 100644 ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js delete mode 100644 ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js delete mode 100644 ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js delete mode 100644 ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js delete mode 100644 ui/app/components/wallet-view.js delete mode 100644 ui/app/conf-tx.js delete mode 100644 ui/app/constants/common.js delete mode 100644 ui/app/constants/error-keys.js delete mode 100644 ui/app/constants/transactions.js delete mode 100644 ui/app/conversion-util.js delete mode 100644 ui/app/conversion-util.test.js create mode 100644 ui/app/ducks/app/app.js delete mode 100644 ui/app/ducks/confirm-transaction.duck.js create mode 100644 ui/app/ducks/confirm-transaction/confirm-transaction.duck.js create mode 100644 ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js delete mode 100644 ui/app/ducks/gas.duck.js create mode 100644 ui/app/ducks/gas/gas-duck.test.js create mode 100644 ui/app/ducks/gas/gas.duck.js create mode 100644 ui/app/ducks/index.js create mode 100644 ui/app/ducks/locale/locale.js create mode 100644 ui/app/ducks/metamask/metamask.js delete mode 100644 ui/app/ducks/mock-gas-estimate-data.js delete mode 100644 ui/app/ducks/send.duck.js create mode 100644 ui/app/ducks/send/send-duck.test.js create mode 100644 ui/app/ducks/send/send.duck.js delete mode 100644 ui/app/ducks/tests/confirm-transaction.duck.test.js delete mode 100644 ui/app/ducks/tests/gas-duck.test.js delete mode 100644 ui/app/ducks/tests/send-duck.test.js delete mode 100644 ui/app/helpers/common.util.js delete mode 100644 ui/app/helpers/confirm-transaction/util.js delete mode 100644 ui/app/helpers/confirm-transaction/util.test.js create mode 100644 ui/app/helpers/constants/common.js create mode 100644 ui/app/helpers/constants/error-keys.js create mode 100644 ui/app/helpers/constants/infura-conversion.json create mode 100644 ui/app/helpers/constants/routes.js create mode 100644 ui/app/helpers/constants/transactions.js delete mode 100644 ui/app/helpers/conversions.util.js delete mode 100644 ui/app/helpers/formatters.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/authenticated.component.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/authenticated.container.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/index.js create mode 100644 ui/app/helpers/higher-order-components/i18n-provider.js create mode 100644 ui/app/helpers/higher-order-components/initialized/index.js create mode 100644 ui/app/helpers/higher-order-components/initialized/initialized.component.js create mode 100644 ui/app/helpers/higher-order-components/initialized/initialized.container.js create mode 100644 ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js create mode 100644 ui/app/helpers/higher-order-components/with-method-data/index.js create mode 100644 ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/index.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js create mode 100644 ui/app/helpers/higher-order-components/with-token-tracker/index.js create mode 100644 ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js delete mode 100644 ui/app/helpers/tests/common.util.test.js delete mode 100644 ui/app/helpers/tests/transactions.util.test.js delete mode 100644 ui/app/helpers/transactions.util.js create mode 100644 ui/app/helpers/utils/common.util.js create mode 100644 ui/app/helpers/utils/common.util.test.js create mode 100644 ui/app/helpers/utils/confirm-tx.util.js create mode 100644 ui/app/helpers/utils/confirm-tx.util.test.js create mode 100644 ui/app/helpers/utils/conversion-util.js create mode 100644 ui/app/helpers/utils/conversion-util.test.js create mode 100644 ui/app/helpers/utils/conversions.util.js create mode 100644 ui/app/helpers/utils/formatters.js create mode 100644 ui/app/helpers/utils/i18n-helper.js create mode 100644 ui/app/helpers/utils/metametrics.util.js create mode 100644 ui/app/helpers/utils/token-util.js create mode 100644 ui/app/helpers/utils/transactions.util.js create mode 100644 ui/app/helpers/utils/transactions.util.test.js create mode 100644 ui/app/helpers/utils/util.js delete mode 100644 ui/app/higher-order-components/authenticated/authenticated.component.js delete mode 100644 ui/app/higher-order-components/authenticated/authenticated.container.js delete mode 100644 ui/app/higher-order-components/authenticated/index.js delete mode 100644 ui/app/higher-order-components/initialized/index.js delete mode 100644 ui/app/higher-order-components/initialized/initialized.component.js delete mode 100644 ui/app/higher-order-components/initialized/initialized.container.js delete mode 100644 ui/app/higher-order-components/with-method-data/index.js delete mode 100644 ui/app/higher-order-components/with-method-data/with-method-data.component.js delete mode 100644 ui/app/higher-order-components/with-modal-props/index.js delete mode 100644 ui/app/higher-order-components/with-modal-props/tests/with-modal-props.test.js delete mode 100644 ui/app/higher-order-components/with-modal-props/with-modal-props.js delete mode 100644 ui/app/higher-order-components/with-token-tracker/index.js delete mode 100644 ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js delete mode 100644 ui/app/i18n-provider.js delete mode 100644 ui/app/img/identicon-tardigrade.png delete mode 100644 ui/app/img/identicon-walrus.png delete mode 100644 ui/app/infura-conversion.json delete mode 100644 ui/app/keychains/hd/create-vault-complete.js delete mode 100644 ui/app/keychains/hd/restore-vault.js delete mode 100644 ui/app/metametrics/metametrics.provider.js delete mode 100644 ui/app/metametrics/metametrics.util.js create mode 100644 ui/app/pages/add-token/add-token.component.js create mode 100644 ui/app/pages/add-token/add-token.container.js create mode 100644 ui/app/pages/add-token/index.js create mode 100644 ui/app/pages/add-token/index.scss create mode 100644 ui/app/pages/add-token/token-list/index.js create mode 100644 ui/app/pages/add-token/token-list/index.scss create mode 100644 ui/app/pages/add-token/token-list/token-list-placeholder/index.js create mode 100644 ui/app/pages/add-token/token-list/token-list-placeholder/index.scss create mode 100644 ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js create mode 100644 ui/app/pages/add-token/token-list/token-list.component.js create mode 100644 ui/app/pages/add-token/token-list/token-list.container.js create mode 100644 ui/app/pages/add-token/token-search/index.js create mode 100644 ui/app/pages/add-token/token-search/token-search.component.js create mode 100644 ui/app/pages/add-token/util.js create mode 100644 ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js create mode 100644 ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js create mode 100644 ui/app/pages/confirm-add-suggested-token/index.js create mode 100644 ui/app/pages/confirm-add-token/confirm-add-token.component.js create mode 100644 ui/app/pages/confirm-add-token/confirm-add-token.container.js create mode 100644 ui/app/pages/confirm-add-token/index.js create mode 100644 ui/app/pages/confirm-add-token/index.scss create mode 100644 ui/app/pages/confirm-approve/confirm-approve.component.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve.container.js create mode 100644 ui/app/pages/confirm-approve/index.js create mode 100644 ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js create mode 100644 ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js create mode 100644 ui/app/pages/confirm-deploy-contract/index.js create mode 100644 ui/app/pages/confirm-send-ether/confirm-send-ether.component.js create mode 100644 ui/app/pages/confirm-send-ether/confirm-send-ether.container.js create mode 100644 ui/app/pages/confirm-send-ether/index.js create mode 100644 ui/app/pages/confirm-send-token/confirm-send-token.component.js create mode 100644 ui/app/pages/confirm-send-token/confirm-send-token.container.js create mode 100644 ui/app/pages/confirm-send-token/index.js create mode 100644 ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js create mode 100644 ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js create mode 100644 ui/app/pages/confirm-token-transaction-base/index.js create mode 100644 ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js create mode 100644 ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js create mode 100644 ui/app/pages/confirm-transaction-base/index.js create mode 100644 ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js create mode 100644 ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js create mode 100644 ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js create mode 100644 ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js create mode 100644 ui/app/pages/confirm-transaction-switch/index.js create mode 100644 ui/app/pages/confirm-transaction/conf-tx.js create mode 100644 ui/app/pages/confirm-transaction/confirm-transaction.component.js create mode 100644 ui/app/pages/confirm-transaction/confirm-transaction.container.js create mode 100644 ui/app/pages/confirm-transaction/index.js create mode 100644 ui/app/pages/create-account/connect-hardware/account-list.js create mode 100644 ui/app/pages/create-account/connect-hardware/connect-screen.js create mode 100644 ui/app/pages/create-account/connect-hardware/index.js create mode 100644 ui/app/pages/create-account/import-account/index.js create mode 100644 ui/app/pages/create-account/import-account/json.js create mode 100644 ui/app/pages/create-account/import-account/private-key.js create mode 100644 ui/app/pages/create-account/import-account/seed.js create mode 100644 ui/app/pages/create-account/index.js create mode 100644 ui/app/pages/create-account/new-account.js create mode 100644 ui/app/pages/first-time-flow/create-password/create-password.component.js create mode 100644 ui/app/pages/first-time-flow/create-password/create-password.container.js create mode 100644 ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js create mode 100644 ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js create mode 100644 ui/app/pages/first-time-flow/create-password/index.js create mode 100644 ui/app/pages/first-time-flow/create-password/new-account/index.js create mode 100644 ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js create mode 100644 ui/app/pages/first-time-flow/create-password/unique-image/index.js create mode 100644 ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js create mode 100644 ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js create mode 100644 ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js create mode 100644 ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js create mode 100644 ui/app/pages/first-time-flow/end-of-flow/index.js create mode 100644 ui/app/pages/first-time-flow/end-of-flow/index.scss create mode 100644 ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js create mode 100644 ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js create mode 100644 ui/app/pages/first-time-flow/first-time-flow-switch/index.js create mode 100644 ui/app/pages/first-time-flow/first-time-flow.component.js create mode 100644 ui/app/pages/first-time-flow/first-time-flow.container.js create mode 100644 ui/app/pages/first-time-flow/first-time-flow.selectors.js create mode 100644 ui/app/pages/first-time-flow/index.js create mode 100644 ui/app/pages/first-time-flow/index.scss create mode 100644 ui/app/pages/first-time-flow/metametrics-opt-in/index.js create mode 100644 ui/app/pages/first-time-flow/metametrics-opt-in/index.scss create mode 100644 ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js create mode 100644 ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss create mode 100644 ui/app/pages/first-time-flow/seed-phrase/index.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/index.scss create mode 100644 ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss create mode 100644 ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js create mode 100644 ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js create mode 100644 ui/app/pages/first-time-flow/select-action/index.js create mode 100644 ui/app/pages/first-time-flow/select-action/index.scss create mode 100644 ui/app/pages/first-time-flow/select-action/select-action.component.js create mode 100644 ui/app/pages/first-time-flow/select-action/select-action.container.js create mode 100644 ui/app/pages/first-time-flow/welcome/index.js create mode 100644 ui/app/pages/first-time-flow/welcome/index.scss create mode 100644 ui/app/pages/first-time-flow/welcome/welcome.component.js create mode 100644 ui/app/pages/first-time-flow/welcome/welcome.container.js create mode 100644 ui/app/pages/home/home.component.js create mode 100644 ui/app/pages/home/home.container.js create mode 100644 ui/app/pages/home/index.js create mode 100644 ui/app/pages/index.js create mode 100644 ui/app/pages/index.scss create mode 100644 ui/app/pages/keychains/index.scss create mode 100644 ui/app/pages/keychains/restore-vault.js create mode 100644 ui/app/pages/keychains/reveal-seed.js create mode 100644 ui/app/pages/lock/index.js create mode 100644 ui/app/pages/lock/lock.component.js create mode 100644 ui/app/pages/lock/lock.container.js create mode 100644 ui/app/pages/mobile-sync/index.js create mode 100644 ui/app/pages/notice/notice.js create mode 100644 ui/app/pages/provider-approval/index.js create mode 100644 ui/app/pages/provider-approval/provider-approval.component.js create mode 100644 ui/app/pages/provider-approval/provider-approval.container.js create mode 100644 ui/app/pages/routes/index.js create mode 100644 ui/app/pages/settings/index.js create mode 100644 ui/app/pages/settings/index.scss create mode 100644 ui/app/pages/settings/info-tab/index.js create mode 100644 ui/app/pages/settings/info-tab/index.scss create mode 100644 ui/app/pages/settings/info-tab/info-tab.component.js create mode 100644 ui/app/pages/settings/settings-tab/index.js create mode 100644 ui/app/pages/settings/settings-tab/index.scss create mode 100644 ui/app/pages/settings/settings-tab/settings-tab.component.js create mode 100644 ui/app/pages/settings/settings-tab/settings-tab.container.js create mode 100644 ui/app/pages/settings/settings.component.js create mode 100644 ui/app/pages/unlock-page/index.js create mode 100644 ui/app/pages/unlock-page/index.scss create mode 100644 ui/app/pages/unlock-page/unlock-page.component.js create mode 100644 ui/app/pages/unlock-page/unlock-page.container.js delete mode 100644 ui/app/reducers.js delete mode 100644 ui/app/reducers/app.js delete mode 100644 ui/app/reducers/locale.js delete mode 100644 ui/app/reducers/metamask.js delete mode 100644 ui/app/root.js delete mode 100644 ui/app/routes.js delete mode 100644 ui/app/selectors.js create mode 100644 ui/app/selectors/custom-gas.test.js create mode 100644 ui/app/selectors/selectors.js delete mode 100644 ui/app/selectors/tests/custom-gas.test.js delete mode 100644 ui/app/store.js create mode 100644 ui/app/store/actions.js create mode 100644 ui/app/store/store.js delete mode 100644 ui/app/token-util.js delete mode 100644 ui/app/util.js delete mode 100644 ui/i18n-helper.js diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index b296dc5eb..765551167 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -7,7 +7,7 @@ const { const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. -import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/constants/error-keys' +import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys' /** tx-gas-utils are gas utility methods for Transaction manager diff --git a/development/mock-dev.js b/development/mock-dev.js index 1af10a131..4a3217a06 100644 --- a/development/mock-dev.js +++ b/development/mock-dev.js @@ -14,9 +14,9 @@ const render = require('react-dom').render const h = require('react-hyperscript') -const Root = require('../ui/app/root') -const configureStore = require('../ui/app/store') -const actions = require('../ui/app/actions') +const Root = require('../ui/app/pages') +const configureStore = require('../ui/app/store/store') +const actions = require('../ui/app/store/actions') const states = require('./states') const backGroundConnectionModifiers = require('./backGroundConnectionModifiers') const Selector = require('./selector') diff --git a/development/ui-dev.js b/development/ui-dev.js index bae0ce50e..70f513972 100644 --- a/development/ui-dev.js +++ b/development/ui-dev.js @@ -17,7 +17,7 @@ const render = require('react-dom').render const h = require('react-hyperscript') -const Root = require('../ui/app/root') +const Root = require('../ui/app/pages') const configureStore = require('./uiStore') const states = require('./states') const Selector = require('./selector') diff --git a/development/uiStore.js b/development/uiStore.js index c71d66d3b..bfec8f5e4 100644 --- a/development/uiStore.js +++ b/development/uiStore.js @@ -2,7 +2,7 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware const thunkMiddleware = require('redux-thunk').default const createLogger = require('redux-logger').createLogger -const rootReducer = require('../ui/app/reducers') +const rootReducer = require('../ui/app/ducks') module.exports = configureStore diff --git a/docs/adding-new-networks.md b/docs/adding-new-networks.md index b74233fa6..40925f135 100644 --- a/docs/adding-new-networks.md +++ b/docs/adding-new-networks.md @@ -5,7 +5,7 @@ To add another network to our dropdown menu, make sure the following files are a ``` app/scripts/config.js app/scripts/lib/buy-eth-url.js -ui/app/app.js +ui/app/index.js ui/app/components/buy-button-subview.js ui/app/components/drop-menu-item.js ui/app/components/network.js diff --git a/mascara/src/app/buy-ether-widget/index.js b/mascara/src/app/buy-ether-widget/index.js index c8530ba4c..d0d6ff343 100644 --- a/mascara/src/app/buy-ether-widget/index.js +++ b/mascara/src/app/buy-ether-widget/index.js @@ -5,7 +5,7 @@ import {connect} from 'react-redux' import {qrcode} from 'qrcode-generator' import copyToClipboard from 'copy-to-clipboard' import ShapeShiftForm from '../shapeshift-form' -import {buyEth, showAccountDetail} from '../../../../ui/app/actions' +import {buyEth, showAccountDetail} from '../../../../ui/app/store/actions' const OPTION_VALUES = { COINBASE: 'coinbase', diff --git a/mascara/src/app/shapeshift-form/index.js b/mascara/src/app/shapeshift-form/index.js index fe7f7ffcb..c044f9ecc 100644 --- a/mascara/src/app/shapeshift-form/index.js +++ b/mascara/src/app/shapeshift-form/index.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import qrcode from 'qrcode-generator' import {connect} from 'react-redux' -import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/actions' -import {isValidAddress} from '../../../../ui/app/util' +import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/store/actions' +import {isValidAddress} from '../../../../ui/app/helpers/utils/util' export class ShapeShiftForm extends Component { static propTypes = { diff --git a/test/unit/actions/config_test.js b/test/unit/actions/config_test.js index 648f456fb..9127474a8 100644 --- a/test/unit/actions/config_test.js +++ b/test/unit/actions/config_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('config view actions', function () { var initialState = { diff --git a/test/unit/actions/set_account_label_test.js b/test/unit/actions/set_account_label_test.js index 53ea1d130..1601d6383 100644 --- a/test/unit/actions/set_account_label_test.js +++ b/test/unit/actions/set_account_label_test.js @@ -2,8 +2,8 @@ const assert = require('assert') const freeze = require('deep-freeze-strict') const path = require('path') -const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('SET_ACCOUNT_LABEL', function () { it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { diff --git a/test/unit/actions/set_selected_account_test.js b/test/unit/actions/set_selected_account_test.js index 28b47d09d..36d312d7b 100644 --- a/test/unit/actions/set_selected_account_test.js +++ b/test/unit/actions/set_selected_account_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('SET_SELECTED_ACCOUNT', function () { it('sets the state.appState.activeAddress property of the state to the action.value', function () { diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 160cd4552..8c64d844f 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -4,7 +4,7 @@ var path = require('path') import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -const actions = require(path.join(__dirname, '../../../ui/app/actions.js')) +const actions = require(path.join(__dirname, '../../../ui/app/store/actions.js')) const middlewares = [thunk] const mockStore = configureMockStore(middlewares) diff --git a/test/unit/actions/view_info_test.js b/test/unit/actions/view_info_test.js index 69895d801..5785a368c 100644 --- a/test/unit/actions/view_info_test.js +++ b/test/unit/actions/view_info_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('SHOW_INFO_PAGE', function () { it('sets the state.appState.currentView.name property to info', function () { diff --git a/test/unit/actions/warning_test.js b/test/unit/actions/warning_test.js index 28b565499..e57374cda 100644 --- a/test/unit/actions/warning_test.js +++ b/test/unit/actions/warning_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('action DISPLAY_WARNING', function () { it('sets appState.warning to provided value', function () { diff --git a/test/unit/balance-formatter-test.js b/test/unit/balance-formatter-test.js index ab6daa19c..bd0eb5008 100644 --- a/test/unit/balance-formatter-test.js +++ b/test/unit/balance-formatter-test.js @@ -1,6 +1,6 @@ const assert = require('assert') const currencyFormatter = require('currency-formatter') -const infuraConversion = require('../../ui/app/infura-conversion.json') +const infuraConversion = require('../../ui/app/helpers/constants/infura-conversion.json') describe('currencyFormatting', function () { it('be able to format any infura currency', function (done) { diff --git a/test/unit/reducers/unlock_vault_test.js b/test/unit/reducers/unlock_vault_test.js index d66e8edbb..d66891a63 100644 --- a/test/unit/reducers/unlock_vault_test.js +++ b/test/unit/reducers/unlock_vault_test.js @@ -4,8 +4,8 @@ var assert = require('assert') var path = require('path') var sinon = require('sinon') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'store', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'ducks', 'index.js')) describe('#unlockMetamask(selectedAccount)', function () { beforeEach(function () { diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js index f3f236d90..1fadbfb60 100644 --- a/test/unit/responsive/components/dropdown-test.js +++ b/test/unit/responsive/components/dropdown-test.js @@ -3,7 +3,7 @@ const assert = require('assert') const h = require('react-hyperscript') const sinon = require('sinon') const path = require('path') -const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'app', 'dropdowns', 'index.js')).Dropdown const { createMockStore } = require('redux-test-utils') const { mountWithStore } = require('../../../lib/render-helpers') diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 8d7de8b02..46e94bb32 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -16,7 +16,7 @@ const { createTestProviderTools } = require('../../../stub/provider') const provider = createTestProviderTools({ scaffold: {}}).provider const enLocale = require('../../../../app/_locales/en/messages.json') -const actions = require('../../../../ui/app/actions') +const actions = require('../../../../ui/app/store/actions') const MetaMaskController = require('../../../../app/scripts/metamask-controller') const firstTimeState = require('../../../unit/localhostState') diff --git a/test/unit/ui/app/components/token-cell.spec.js b/test/unit/ui/app/components/token-cell.spec.js index 6145c6924..23e76c418 100644 --- a/test/unit/ui/app/components/token-cell.spec.js +++ b/test/unit/ui/app/components/token-cell.spec.js @@ -5,8 +5,8 @@ import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' import { mount } from 'enzyme' -import TokenCell from '../../../../../ui/app/components/token-cell' -import Identicon from '../../../../../ui/app/components/identicon' +import TokenCell from '../../../../../ui/app/components/app/token-cell' +import Identicon from '../../../../../ui/app/components/ui/identicon' describe('Token Cell', () => { let wrapper diff --git a/test/unit/ui/app/reducers/app.spec.js b/test/unit/ui/app/reducers/app.spec.js index bee4963e5..6c77e0ef9 100644 --- a/test/unit/ui/app/reducers/app.spec.js +++ b/test/unit/ui/app/reducers/app.spec.js @@ -1,6 +1,6 @@ import assert from 'assert' -import reduceApp from '../../../../../ui/app/reducers/app' -import * as actions from '../../../../../ui/app/actions' +import reduceApp from '../../../../../ui/app/ducks/app/app' +import * as actions from '../../../../../ui/app/store/actions' describe('App State', () => { diff --git a/test/unit/ui/app/reducers/metamask.spec.js b/test/unit/ui/app/reducers/metamask.spec.js index 8cdb780fe..388c67c76 100644 --- a/test/unit/ui/app/reducers/metamask.spec.js +++ b/test/unit/ui/app/reducers/metamask.spec.js @@ -1,6 +1,6 @@ import assert from 'assert' -import reduceMetamask from '../../../../../ui/app/reducers/metamask' -import * as actions from '../../../../../ui/app/actions' +import reduceMetamask from '../../../../../ui/app/ducks/metamask/metamask' +import * as actions from '../../../../../ui/app/store/actions' describe('MetaMask Reducers', () => { diff --git a/test/unit/ui/app/selectors.spec.js b/test/unit/ui/app/selectors.spec.js index 070de0bcd..b4aa8e272 100644 --- a/test/unit/ui/app/selectors.spec.js +++ b/test/unit/ui/app/selectors.spec.js @@ -1,5 +1,5 @@ const assert = require('assert') -const selectors = require('../../../../ui/app/selectors') +const selectors = require('../../../../ui/app/selectors/selectors') const mockState = require('../../../data/mock-state.json') const Eth = require('ethjs') diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 39473854f..87f57b218 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -3,7 +3,7 @@ var sinon = require('sinon') const ethUtil = require('ethereumjs-util') var path = require('path') -var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'util.js')) +var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'helpers', 'utils', 'util.js')) describe('util', function () { var ethInWei = '1' diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index c6b1254b5..000000000 --- a/ui/.gitignore +++ /dev/null @@ -1,66 +0,0 @@ - -# Created by https://www.gitignore.io/api/osx,node - -### OSX ### -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - -### Node ### -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - diff --git a/ui/app/accounts/new-account/index.js b/ui/app/accounts/new-account/index.js deleted file mode 100644 index 795bd7ce6..000000000 --- a/ui/app/accounts/new-account/index.js +++ /dev/null @@ -1,87 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../../actions') -const { getCurrentViewContext } = require('../../selectors') -const classnames = require('classnames') - -const NewAccountCreateForm = require('./create-form') -const NewAccountImportForm = require('../import') - -function mapStateToProps (state) { - return { - displayedForm: getCurrentViewContext(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - displayForm: form => dispatch(actions.setNewAccountForm(form)), - showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), - showExportPrivateKeyModal: () => { - dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) - }, - hideModal: () => dispatch(actions.hideModal()), - setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), - } -} - -inherits(AccountDetailsModal, Component) -function AccountDetailsModal (props) { - Component.call(this) - - this.state = { - displayedForm: props.displayedForm, - } -} - -AccountDetailsModal.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal) - - -AccountDetailsModal.prototype.render = function () { - const { displayedForm, displayForm } = this.props - - return h('div.new-account', {}, [ - - h('div.new-account__header', [ - - h('div.new-account__title', this.context.t('newAccount')), - - h('div.new-account__tabs', [ - - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': displayedForm === 'CREATE', - 'new-account__tabs__unselected cursor-pointer': displayedForm !== 'CREATE', - }), - onClick: () => displayForm('CREATE'), - }, this.context.t('createDen')), - - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': displayedForm === 'IMPORT', - 'new-account__tabs__unselected cursor-pointer': displayedForm !== 'IMPORT', - }), - onClick: () => displayForm('IMPORT'), - }, this.context.t('import')), - - ]), - - ]), - - h('div.new-account__form', [ - - displayedForm === 'CREATE' - ? h(NewAccountCreateForm) - : h(NewAccountImportForm), - - ]), - - ]) -} diff --git a/ui/app/actions.js b/ui/app/actions.js deleted file mode 100644 index d8363eba6..000000000 --- a/ui/app/actions.js +++ /dev/null @@ -1,2761 +0,0 @@ -const abi = require('human-standard-token-abi') -const pify = require('pify') -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') -const { getTokenAddressFromTokenObject } = require('./util') -const { - calcTokenBalance, - estimateGas, -} = require('./components/send/send.utils') -const ethUtil = require('ethereumjs-util') -const { fetchLocale } = require('../i18n-helper') -const log = require('loglevel') -const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums') -const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') -const gasDuck = require('./ducks/gas.duck') -const WebcamUtils = require('../lib/webcam-utils') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // modal state - MODAL_OPEN: 'UI_MODAL_OPEN', - MODAL_CLOSE: 'UI_MODAL_CLOSE', - showModal: showModal, - hideModal: hideModal, - // sidebar state - SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN', - SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', - showSidebar: showSidebar, - hideSidebar: hideSidebar, - // sidebar state - ALERT_OPEN: 'UI_ALERT_OPEN', - ALERT_CLOSE: 'UI_ALERT_CLOSE', - showAlert: showAlert, - hideAlert: hideAlert, - QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED', - qrCodeDetected, - // network dropdown open - NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN', - NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE', - showNetworkDropdown: showNetworkDropdown, - hideNetworkDropdown: hideNetworkDropdown, - // menu state/ - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - fetchInfoToSync, - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - markPasswordForgotten, - unMarkPasswordForgotten, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - SHOW_NEW_ACCOUNT_PAGE: 'SHOW_NEW_ACCOUNT_PAGE', - SET_NEW_ACCOUNT_FORM: 'SET_NEW_ACCOUNT_FORM', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - unlockSucceeded, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - showNewAccountPage, - setNewAccountForm, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - createNewVaultAndGetSeedPhrase, - unlockAndGetSeedPhrase, - addNewKeyring, - importNewAccount, - addNewAccount, - connectHardware, - checkHardwareStatus, - forgetDevice, - unlockHardwareWalletAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - resetAccount, - removeAccount, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN', - closeWelcomeScreen, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - requestRevealSeedWords, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_SUCCEEDED: 'UNLOCK_SUCCEEDED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN', - setSelectedToken, - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - showQrScanner, - setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE', - showSendTokenPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - exportAccountComplete, - SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL', - setAccountLabel, - updateNetworkNonce, - SET_NETWORK_NONCE: 'SET_NETWORK_NONCE', - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - EDIT_TX: 'EDIT_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - signTypedMsg, - cancelTypedMsg, - sendTx: sendTx, - signTx: signTx, - signTokenTx: signTokenTx, - updateTransaction, - updateAndApproveTx, - cancelTx: cancelTx, - cancelTxs, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - editTx, - previousTx: previousTx, - cancelAllTx: cancelAllTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - updateTransactionParams, - UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', - // send screen - UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', - UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', - UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', - UPDATE_SEND_FROM: 'UPDATE_SEND_FROM', - UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA', - UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE', - UPDATE_SEND_TO: 'UPDATE_SEND_TO', - UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', - UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO', - UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS', - UPDATE_SEND_WARNINGS: 'UPDATE_SEND_WARNINGS', - UPDATE_MAX_MODE: 'UPDATE_MAX_MODE', - UPDATE_SEND: 'UPDATE_SEND', - CLEAR_SEND: 'CLEAR_SEND', - OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', - CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', - GAS_LOADING_STARTED: 'GAS_LOADING_STARTED', - GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED', - setGasLimit, - setGasPrice, - updateGasData, - setGasTotal, - setSendTokenBalance, - updateSendTokenBalance, - updateSendHexData, - updateSendTo, - updateSendAmount, - updateSendMemo, - setMaxModeTo, - updateSend, - updateSendErrors, - updateSendWarnings, - clearSend, - setSelectedAddress, - gasLoadingStarted, - gasLoadingFinished, - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - SET_PREVIOUS_PROVIDER: 'SET_PREVIOUS_PROVIDER', - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE', - showAddTokenPage, - showAddSuggestedTokenPage, - addToken, - addTokens, - removeToken, - updateTokens, - removeSuggestedTokens, - addKnownMethodData, - UPDATE_TOKENS: 'UPDATE_TOKENS', - updateAndSetCustomRpc: updateAndSetCustomRpc, - setRpcTarget: setRpcTarget, - delRpcTarget: delRpcTarget, - setProviderType: setProviderType, - SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', - setHardwareWalletDefaultHdPath, - updateProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - onboardingBuyEthView, - ONBOARDING_BUY_ETH_VIEW: 'ONBOARDING_BUY_ETH_VIEW', - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - buyWithShapeShift, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, - - TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU', - toggleAccountMenu, - - useEtherscanProvider, - - SET_USE_BLOCKIE: 'SET_USE_BLOCKIE', - setUseBlockie, - - SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS', - SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT', - setParticipateInMetaMetrics, - setMetaMetricsSendCount, - - // locale - SET_CURRENT_LOCALE: 'SET_CURRENT_LOCALE', - SET_LOCALE_MESSAGES: 'SET_LOCALE_MESSAGES', - setCurrentLocale, - updateCurrentLocale, - setLocaleMessages, - // - // Feature Flags - setFeatureFlag, - updateFeatureFlags, - UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS', - - // Preferences - setPreference, - updatePreferences, - UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', - setUseNativeCurrencyAsPrimaryCurrencyPreference, - setShowFiatConversionOnTestnetsPreference, - - // Migration of users to new UI - setCompletedUiMigration, - completeUiMigration, - COMPLETE_UI_MIGRATION: 'COMPLETE_UI_MIGRATION', - - // Onboarding - setCompletedOnboarding, - completeOnboarding, - COMPLETE_ONBOARDING: 'COMPLETE_ONBOARDING', - - setMouseUserState, - SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', - - // Network - updateNetworkEndpointType, - UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE', - - retryTransaction, - SET_PENDING_TOKENS: 'SET_PENDING_TOKENS', - CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS', - setPendingTokens, - clearPendingTokens, - - createCancelTransaction, - createSpeedUpTransaction, - - approveProviderRequest, - rejectProviderRequest, - clearApprovedOrigins, - - setFirstTimeFlowType, - SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - - return new Promise((resolve, reject) => { - background.submitPassword(password, error => { - if (error) { - return reject(error) - } - - resolve() - }) - }) - .then(() => { - dispatch(actions.unlockSucceeded()) - return forceUpdateMetamaskState(dispatch) - }) - .then(() => { - return new Promise((resolve, reject) => { - background.verifySeedPhrase(err => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - resolve() - }) - }) - }) - .then(() => { - dispatch(actions.transitionForward()) - dispatch(actions.hideLoadingIndication()) - }) - .catch(err => { - dispatch(actions.unlockFailed(err.message)) - dispatch(actions.hideLoadingIndication()) - return Promise.reject(err) - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return dispatch => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - return new Promise((resolve, reject) => { - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountsPage()) - resolve(account) - }) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - - return new Promise((resolve, reject) => { - background.clearSeedWordCache((err) => { - if (err) { - return reject(err) - } - - background.createNewVaultAndRestore(password, seed, (err) => { - if (err) { - return reject(err) - } - - resolve() - }) - }) - }) - .then(() => dispatch(actions.unMarkPasswordForgotten())) - .then(() => { - dispatch(actions.showAccountsPage()) - dispatch(actions.hideLoadingIndication()) - }) - .catch(err => { - dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - return Promise.reject(err) - }) - } -} - -function createNewVaultAndKeychain (password) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - - return new Promise((resolve, reject) => { - background.createNewVaultAndKeychain(password, err => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - log.debug(`background.placeSeedWords`) - - background.placeSeedWords((err) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - resolve() - }) - }) - }) - .then(() => forceUpdateMetamaskState(dispatch)) - .then(() => dispatch(actions.hideLoadingIndication())) - .catch(() => dispatch(actions.hideLoadingIndication())) - } -} - -function createNewVaultAndGetSeedPhrase (password) { - return async dispatch => { - dispatch(actions.showLoadingIndication()) - - try { - await createNewVault(password) - const seedWords = await verifySeedPhrase() - dispatch(actions.hideLoadingIndication()) - return seedWords - } catch (error) { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.displayWarning(error.message)) - throw new Error(error.message) - } - } -} - -function unlockAndGetSeedPhrase (password) { - return async dispatch => { - dispatch(actions.showLoadingIndication()) - - try { - await submitPassword(password) - const seedWords = await verifySeedPhrase() - await forceUpdateMetamaskState(dispatch) - dispatch(actions.hideLoadingIndication()) - return seedWords - } catch (error) { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.displayWarning(error.message)) - throw new Error(error.message) - } - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function submitPassword (password) { - return new Promise((resolve, reject) => { - background.submitPassword(password, error => { - if (error) { - return reject(error) - } - - resolve() - }) - }) -} - -function createNewVault (password) { - return new Promise((resolve, reject) => { - background.createNewVaultAndKeychain(password, error => { - if (error) { - return reject(error) - } - - resolve(true) - }) - }) -} - -function verifyPassword (password) { - return new Promise((resolve, reject) => { - background.submitPassword(password, error => { - if (error) { - return reject(error) - } - - resolve(true) - }) - }) -} - -function verifySeedPhrase () { - return new Promise((resolve, reject) => { - background.verifySeedPhrase((error, seedWords) => { - if (error) { - return reject(error) - } - - resolve(seedWords) - }) - }) -} - -function requestRevealSeed (password) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - return new Promise((resolve, reject) => { - background.submitPassword(password, err => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.showNewVaultSeed(result)) - dispatch(actions.hideLoadingIndication()) - resolve() - }) - }) - }) - } -} - -function requestRevealSeedWords (password) { - return async dispatch => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - - try { - await verifyPassword(password) - const seedWords = await verifySeedPhrase() - dispatch(actions.hideLoadingIndication()) - return seedWords - } catch (error) { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.displayWarning(error.message)) - throw new Error(error.message) - } - } -} - -function fetchInfoToSync () { - return dispatch => { - log.debug(`background.fetchInfoToSync`) - return new Promise((resolve, reject) => { - background.fetchInfoToSync((err, result) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - resolve(result) - }) - }) - } -} - -function resetAccount () { - return dispatch => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - background.resetAccount((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - log.info('Transaction history reset for ' + account) - dispatch(actions.showAccountsPage()) - resolve(account) - }) - }) - } -} - -function removeAccount (address) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - background.removeAccount(address, (err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - log.info('Account removed: ' + account) - dispatch(actions.showAccountsPage()) - resolve() - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return async (dispatch) => { - let newState - dispatch(actions.showLoadingIndication('This may take a while, please be patient.')) - try { - log.debug(`background.importAccountWithStrategy`) - await pify(background.importAccountWithStrategy).call(background, strategy, args) - log.debug(`background.getState`) - newState = await pify(background.getState).call(background) - } catch (err) { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.displayWarning(err.message)) - throw err - } - dispatch(actions.hideLoadingIndication()) - dispatch(actions.updateMetamaskState(newState)) - if (newState.selectedAddress) { - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - } - return newState - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return (dispatch, getState) => { - const oldIdentities = getState().metamask.identities - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.addNewAccount((err, { identities: newIdentities}) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - const newAccountAddress = Object.keys(newIdentities).find(address => !oldIdentities[address]) - - dispatch(actions.hideLoadingIndication()) - - forceUpdateMetamaskState(dispatch) - return resolve(newAccountAddress) - }) - }) - } -} - -function checkHardwareStatus (deviceName, hdPath) { - log.debug(`background.checkHardwareStatus`, deviceName, hdPath) - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.hideLoadingIndication()) - - forceUpdateMetamaskState(dispatch) - return resolve(unlocked) - }) - }) - } -} - -function forgetDevice (deviceName) { - log.debug(`background.forgetDevice`, deviceName) - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.forgetDevice(deviceName, (err, response) => { - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.hideLoadingIndication()) - - forceUpdateMetamaskState(dispatch) - return resolve() - }) - }) - } -} - -function connectHardware (deviceName, page, hdPath) { - log.debug(`background.connectHardware`, deviceName, page, hdPath) - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.connectHardware(deviceName, page, hdPath, (err, accounts) => { - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.hideLoadingIndication()) - - forceUpdateMetamaskState(dispatch) - return resolve(accounts) - }) - }) - } -} - -function unlockHardwareWalletAccount (index, deviceName, hdPath) { - log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.hideLoadingIndication()) - return resolve() - }) - }) - } -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function showQrScanner (ROUTE) { - return (dispatch, getState) => { - return WebcamUtils.checkStatus() - .then(status => { - if (!status.environmentReady) { - // We need to switch to fullscreen mode to ask for permission - global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`) - } else { - dispatch(actions.showModal({ - name: 'QR_SCANNER', - })) - } - }).catch(e => { - dispatch(actions.showModal({ - name: 'QR_SCANNER', - error: true, - errorType: e.type, - })) - }) - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.completedTx(msgData.metamaskId)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.completedTx(msgData.metamaskId)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function signTypedMsg (msgData) { - log.debug('action - signTypedMsg') - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - log.debug(`actions calling background.signTypedMessage`) - background.signTypedMessage(msgData, (err, newState) => { - log.debug('signTypedMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - log.error(err) - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.completedTx(msgData.metamaskId)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - }) - dispatch(actions.showConfTxPage({})) - } -} - -function setGasLimit (gasLimit) { - return { - type: actions.UPDATE_GAS_LIMIT, - value: gasLimit, - } -} - -function setGasPrice (gasPrice) { - return { - type: actions.UPDATE_GAS_PRICE, - value: gasPrice, - } -} - -function setGasTotal (gasTotal) { - return { - type: actions.UPDATE_GAS_TOTAL, - value: gasTotal, - } -} - -function updateGasData ({ - gasPrice, - blockGasLimit, - recentBlocks, - selectedAddress, - selectedToken, - to, - value, - data, -}) { - return (dispatch) => { - dispatch(actions.gasLoadingStarted()) - return estimateGas({ - estimateGasMethod: background.estimateGas, - blockGasLimit, - selectedAddress, - selectedToken, - to, - value, - estimateGasPrice: gasPrice, - data, - }) - .then(gas => { - dispatch(actions.setGasLimit(gas)) - dispatch(gasDuck.setCustomGasLimit(gas)) - dispatch(updateSendErrors({ gasLoadingError: null })) - dispatch(actions.gasLoadingFinished()) - }) - .catch(err => { - log.error(err) - dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })) - dispatch(actions.gasLoadingFinished()) - }) - } -} - -function gasLoadingStarted () { - return { - type: actions.GAS_LOADING_STARTED, - } -} - -function gasLoadingFinished () { - return { - type: actions.GAS_LOADING_FINISHED, - } -} - -function updateSendTokenBalance ({ - selectedToken, - tokenContract, - address, -}) { - return (dispatch) => { - const tokenBalancePromise = tokenContract - ? tokenContract.balanceOf(address) - : Promise.resolve() - return tokenBalancePromise - .then(usersToken => { - if (usersToken) { - const newTokenBalance = calcTokenBalance({ selectedToken, usersToken }) - dispatch(setSendTokenBalance(newTokenBalance)) - } - }) - .catch(err => { - log.error(err) - updateSendErrors({ tokenBalance: 'tokenBalanceError' }) - }) - } -} - -function updateSendErrors (errorObject) { - return { - type: actions.UPDATE_SEND_ERRORS, - value: errorObject, - } -} - -function updateSendWarnings (warningObject) { - return { - type: actions.UPDATE_SEND_WARNINGS, - value: warningObject, - } -} - -function setSendTokenBalance (tokenBalance) { - return { - type: actions.UPDATE_SEND_TOKEN_BALANCE, - value: tokenBalance, - } -} - -function updateSendHexData (value) { - return { - type: actions.UPDATE_SEND_HEX_DATA, - value, - } -} - -function updateSendTo (to, nickname = '') { - return { - type: actions.UPDATE_SEND_TO, - value: { to, nickname }, - } -} - -function updateSendAmount (amount) { - return { - type: actions.UPDATE_SEND_AMOUNT, - value: amount, - } -} - -function updateSendMemo (memo) { - return { - type: actions.UPDATE_SEND_MEMO, - value: memo, - } -} - -function setMaxModeTo (bool) { - return { - type: actions.UPDATE_MAX_MODE, - value: bool, - } -} - -function updateSend (newSend) { - return { - type: actions.UPDATE_SEND, - value: newSend, - } -} - -function clearSend () { - return { - type: actions.CLEAR_SEND, - } -} - - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch, getState) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - }) - } -} - -function signTokenTx (tokenAddress, toAddress, amount, txData) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - const token = global.eth.contract(abi).at(tokenAddress) - token.transfer(toAddress, ethUtil.addHexPrefix(amount), txData) - .catch(err => { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.displayWarning(err.message)) - }) - dispatch(actions.showConfTxPage({})) - } -} - -function updateTransaction (txData) { - log.info('actions: updateTx: ' + JSON.stringify(txData)) - return dispatch => { - log.debug(`actions calling background.updateTx`) - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - background.updateTransaction(txData, (err) => { - dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) - if (err) { - dispatch(actions.txError(err)) - dispatch(actions.goHome()) - log.error(err.message) - return reject(err) - } - - resolve(txData) - }) - }) - .then(() => updateMetamaskStateFromBackground()) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => { - dispatch(actions.showConfTxPage({ id: txData.id })) - dispatch(actions.hideLoadingIndication()) - return txData - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch, getState) => { - log.debug(`actions calling background.updateAndApproveTx`) - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - background.updateAndApproveTransaction(txData, err => { - dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) - dispatch(actions.clearSend()) - - if (err) { - dispatch(actions.txError(err)) - dispatch(actions.goHome()) - log.error(err.message) - reject(err) - } - - resolve(txData) - }) - }) - .then(() => updateMetamaskStateFromBackground()) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => { - dispatch(actions.clearSend()) - dispatch(actions.completedTx(txData.id)) - dispatch(actions.hideLoadingIndication()) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return txData - }) - .catch((err) => { - dispatch(actions.hideLoadingIndication()) - return Promise.reject(err) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function updateTransactionParams (id, txParams) { - return { - type: actions.UPDATE_TRANSACTION_PARAMS, - id, - value: txParams, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id, (err, newState) => { - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - return reject(err) - } - - dispatch(actions.completedTx(msgData.id)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function cancelPersonalMsg (msgData) { - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - const id = msgData.id - background.cancelPersonalMessage(id, (err, newState) => { - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - return reject(err) - } - - dispatch(actions.completedTx(id)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function cancelTypedMsg (msgData) { - return (dispatch, getState) => { - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - const id = msgData.id - background.cancelTypedMessage(id, (err, newState) => { - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) { - return reject(err) - } - - dispatch(actions.completedTx(id)) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return resolve(msgData) - }) - }) - } -} - -function cancelTx (txData) { - return (dispatch, getState) => { - log.debug(`background.cancelTransaction`) - dispatch(actions.showLoadingIndication()) - - return new Promise((resolve, reject) => { - background.cancelTransaction(txData.id, err => { - if (err) { - return reject(err) - } - - resolve() - }) - }) - .then(() => updateMetamaskStateFromBackground()) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => { - dispatch(actions.clearSend()) - dispatch(actions.completedTx(txData.id)) - dispatch(actions.hideLoadingIndication()) - - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && - !hasUnconfirmedTransactions(getState())) { - return global.platform.closeCurrentWindow() - } - - return txData - }) - } -} - -/** - * Cancels all of the given transactions - * @param {Array} txDataList a list of tx data objects - * @return {function(*): Promise} - */ -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} txsData - * @return {Function} - */ -function cancelAllTx (txsData) { - return (dispatch) => { - txsData.forEach((txData, i) => { - background.cancelTransaction(txData.id, () => { - dispatch(actions.completedTx(txData.id)) - i === txsData.length - 1 ? dispatch(actions.goHome()) : null - }) - }) - } -} -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function markPasswordForgotten () { - return (dispatch) => { - return background.markPasswordForgotten(() => { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.forgotPassword()) - forceUpdateMetamaskState(dispatch) - }) - } -} - -function unMarkPasswordForgotten () { - return dispatch => { - return new Promise(resolve => { - background.unMarkPasswordForgotten(() => { - dispatch(actions.forgotPassword(false)) - resolve() - }) - }) - .then(() => forceUpdateMetamaskState(dispatch)) - } -} - -function forgotPassword (forgotPasswordState = true) { - return { - type: actions.FORGOT_PASSWORD, - value: forgotPasswordState, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function showNewAccountPage (formToSelect) { - return { - type: actions.SHOW_NEW_ACCOUNT_PAGE, - formToSelect, - } -} - -function setNewAccountForm (formToSelect) { - return { - type: actions.SET_NEW_ACCOUNT_FORM, - formToSelect, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function closeWelcomeScreen () { - return { - type: actions.CLOSE_WELCOME_SCREEN, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockSucceeded (message) { - return { - type: actions.UNLOCK_SUCCEEDED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -const backgroundSetLocked = () => { - return new Promise((resolve, reject) => { - background.setLocked(error => { - if (error) { - return reject(error) - } - resolve() - }) - }) -} - -const updateMetamaskStateFromBackground = () => { - log.debug(`background.getState`) - - return new Promise((resolve, reject) => { - background.getState((error, newState) => { - if (error) { - return reject(error) - } - - resolve(newState) - }) - }) -} - -function lockMetamask () { - log.debug(`background.setLocked`) - - return dispatch => { - dispatch(actions.showLoadingIndication()) - - return backgroundSetLocked() - .then(() => updateMetamaskStateFromBackground()) - .catch(error => { - dispatch(actions.displayWarning(error.message)) - return Promise.reject(error) - }) - .then(newState => { - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - dispatch({ type: actions.LOCK_METAMASK }) - }) - .catch(() => { - dispatch(actions.hideLoadingIndication()) - dispatch({ type: actions.LOCK_METAMASK }) - }) - } -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function setSelectedToken (tokenAddress) { - return { - type: actions.SET_SELECTED_TOKEN, - value: tokenAddress || null, - } -} - -function setSelectedAddress (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - }) - } -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err, tokens) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(updateTokens(tokens)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - dispatch(actions.setSelectedToken()) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage ({transForward = true, id}) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward, - id, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function editTx (txId) { - return { - type: actions.EDIT_TX, - value: txId, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function showAddSuggestedTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals, image) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.addToken(address, symbol, decimals, image, (err, tokens) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - dispatch(actions.updateTokens(tokens)) - resolve(tokens) - }) - }) - } -} - -function removeToken (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.removeToken(address, (err, tokens) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - dispatch(actions.updateTokens(tokens)) - resolve(tokens) - }) - }) - } -} - -function addTokens (tokens) { - return dispatch => { - if (Array.isArray(tokens)) { - dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens[0]))) - return Promise.all(tokens.map(({ address, symbol, decimals }) => ( - dispatch(addToken(address, symbol, decimals)) - ))) - } else { - dispatch(actions.setSelectedToken(getTokenAddressFromTokenObject(tokens))) - return Promise.all( - Object - .entries(tokens) - .map(([_, { address, symbol, decimals }]) => ( - dispatch(addToken(address, symbol, decimals)) - )) - ) - } - } -} - -function removeSuggestedTokens () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.removeSuggestedTokens((err, suggestedTokens) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.clearPendingTokens()) - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { - return global.platform.closeCurrentWindow() - } - resolve(suggestedTokens) - }) - }) - .then(() => updateMetamaskStateFromBackground()) - .then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens}))) - } -} - -function addKnownMethodData (fourBytePrefix, methodData) { - return (dispatch) => { - background.addKnownMethodData(fourBytePrefix, methodData) - } -} - -function updateTokens (newTokens) { - return { - type: actions.UPDATE_TOKENS, - newTokens, - } -} - -function clearPendingTokens () { - return { - type: actions.CLEAR_PENDING_TOKENS, - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - return new Promise((resolve, reject) => { - background.markNoticeRead(notice, (err, notice) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - if (notice) { - dispatch(actions.showNotice(notice)) - resolve(true) - } else { - dispatch(actions.clearNotices()) - resolve(false) - } - }) - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -function retryTransaction (txId, gasPrice) { - log.debug(`background.retryTransaction`) - let newTxId - - return dispatch => { - return new Promise((resolve, reject) => { - background.retryTransaction(txId, gasPrice, (err, newState) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - - const { selectedAddressTxList } = newState - const { id } = selectedAddressTxList[selectedAddressTxList.length - 1] - newTxId = id - resolve(newState) - }) - }) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => newTxId) - } -} - -function createCancelTransaction (txId, customGasPrice) { - log.debug('background.cancelTransaction') - let newTxId - - return dispatch => { - return new Promise((resolve, reject) => { - background.createCancelTransaction(txId, customGasPrice, (err, newState) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - - const { selectedAddressTxList } = newState - const { id } = selectedAddressTxList[selectedAddressTxList.length - 1] - newTxId = id - resolve(newState) - }) - }) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => newTxId) - } -} - -function createSpeedUpTransaction (txId, customGasPrice) { - log.debug('background.createSpeedUpTransaction') - let newTx - - return dispatch => { - return new Promise((resolve, reject) => { - background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - - const { selectedAddressTxList } = newState - newTx = selectedAddressTxList[selectedAddressTxList.length - 1] - resolve(newState) - }) - }) - .then(newState => dispatch(actions.updateMetamaskState(newState))) - .then(() => newTx) - } -} - -// -// config -// - -function setProviderType (type) { - return (dispatch, getState) => { - const { type: currentProviderType } = getState().metamask.provider - log.debug(`background.setProviderType`, type) - background.setProviderType(type, (err, result) => { - if (err) { - log.error(err) - return dispatch(actions.displayWarning('Had a problem changing networks!')) - } - dispatch(setPreviousProvider(currentProviderType)) - dispatch(actions.updateProviderType(type)) - dispatch(actions.setSelectedToken()) - }) - - } -} - -function updateProviderType (type) { - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function setPreviousProvider (type) { - return { - type: actions.SET_PREVIOUS_PROVIDER, - value: type, - } -} - -function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { - return (dispatch) => { - log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(actions.displayWarning('Had a problem changing networks!')) - } - dispatch({ - type: actions.SET_RPC_TARGET, - value: newRpc, - }) - }) - } -} - -function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { - return (dispatch) => { - log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(actions.displayWarning('Had a problem changing networks!')) - } - dispatch(actions.setSelectedToken()) - }) - } -} - -function delRpcTarget (oldRpc) { - return (dispatch) => { - log.debug(`background.delRpcTarget: ${oldRpc}`) - background.delCustomRpc(oldRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem removing network!')) - } - dispatch(actions.setSelectedToken()) - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname = '') { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showNetworkDropdown () { - return { - type: actions.NETWORK_DROPDOWN_OPEN, - } -} - -function hideNetworkDropdown () { - return { - type: actions.NETWORK_DROPDOWN_CLOSE, - } -} - - -function showModal (payload) { - return { - type: actions.MODAL_OPEN, - payload, - } -} - -function hideModal (payload) { - return { - type: actions.MODAL_CLOSE, - payload, - } -} - -function showSidebar ({ transitionName, type, props }) { - return { - type: actions.SIDEBAR_OPEN, - value: { - transitionName, - type, - props, - }, - } -} - -function hideSidebar () { - return { - type: actions.SIDEBAR_CLOSE, - } -} - -function showAlert (msg) { - return { - type: actions.ALERT_OPEN, - value: msg, - } -} - -function hideAlert () { - return { - type: actions.ALERT_CLOSE, - } -} - -/** - * This action will receive two types of values via qrCodeData - * an object with the following structure {type, values} - * or null (used to clear the previous value) - */ -function qrCodeDetected (qrCodeData) { - return { - type: actions.QR_CODE_DETECTED, - value: qrCodeData, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function setHardwareWalletDefaultHdPath ({ device, path }) { - return { - type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH, - value: {device, path}, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - return new Promise((resolve, reject) => { - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - dispatch(self.displayWarning('Incorrect Password.')) - return reject(err) - } - log.debug(`background.exportAccount`) - return background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - dispatch(self.displayWarning('Had a problem exporting the account.')) - return reject(err) - } - - // dispatch(self.exportAccountComplete()) - dispatch(self.showPrivateKey(result)) - - return resolve(result) - }) - }) - }) - } -} - -function exportAccountComplete () { - return { - type: actions.EXPORT_ACCOUNT, - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function setAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setAccountLabel`) - - return new Promise((resolve, reject) => { - background.setAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - - if (err) { - dispatch(actions.displayWarning(err.message)) - reject(err) - } - - dispatch({ - type: actions.SET_ACCOUNT_LABEL, - value: { account, label }, - }) - - resolve(account) - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function showSendTokenPage () { - return { - type: actions.SHOW_SEND_TOKEN_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function onboardingBuyEthView (address) { - return { - type: actions.ONBOARDING_BUY_ETH_VIEW, - value: address, - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address below:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function buyWithShapeShift (data) { - return dispatch => new Promise((resolve, reject) => { - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - if (response.error) { - return reject(response.error) - } - background.createShapeShiftTx(response.deposit, response.depositType) - return resolve(response) - }) - }) -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address below:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - // return dispatch(actions.showModal({ - // name: 'SHAPESHIFT_DEPOSIT_TX', - // Qr: { data, message }, - // })) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - try { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } catch (e) { - cb ? cb({error: e}) : null - return e - } - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -function setFeatureFlag (feature, activated, notificationType) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.setFeatureFlag(feature, activated, (err, updatedFeatureFlags) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - dispatch(actions.updateFeatureFlags(updatedFeatureFlags)) - notificationType && dispatch(actions.showModal({ name: notificationType })) - resolve(updatedFeatureFlags) - }) - }) - } -} - -function updateFeatureFlags (updatedFeatureFlags) { - return { - type: actions.UPDATE_FEATURE_FLAGS, - value: updatedFeatureFlags, - } -} - -function setPreference (preference, value) { - return dispatch => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.setPreference(preference, value, (err, updatedPreferences) => { - dispatch(actions.hideLoadingIndication()) - - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - - dispatch(actions.updatePreferences(updatedPreferences)) - resolve(updatedPreferences) - }) - }) - } -} - -function updatePreferences (value) { - return { - type: actions.UPDATE_PREFERENCES, - value, - } -} - -function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) { - return setPreference('useNativeCurrencyAsPrimaryCurrency', value) -} - -function setShowFiatConversionOnTestnetsPreference (value) { - return setPreference('showFiatInTestnets', value) -} - -function setCompletedOnboarding () { - return dispatch => { - dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.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/app.js b/ui/app/app.js deleted file mode 100644 index efec4e49a..000000000 --- a/ui/app/app.js +++ /dev/null @@ -1,441 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { Route, Switch, withRouter, matchPath } from 'react-router-dom' -import { compose } from 'recompose' -import actions from './actions' -import log from 'loglevel' -import { getMetaMaskAccounts, getNetworkIdentifier } from './selectors' - -// init -import FirstTimeFlow from './components/pages/first-time-flow' -// accounts -const SendTransactionScreen = require('./components/send/send.container') -const ConfirmTransaction = require('./components/pages/confirm-transaction') - -// slideout menu -const Sidebar = require('./components/sidebars').default -const { WALLET_VIEW_SIDEBAR } = require('./components/sidebars/sidebar.constants') - -// other views -import Home from './components/pages/home' -import Settings from './components/pages/settings' -import Authenticated from './higher-order-components/authenticated' -import Initialized from './higher-order-components/initialized' -import Lock from './components/pages/lock' -import UiMigrationAnnouncement from './components/ui-migration-annoucement' -const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default -const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') -const MobileSyncPage = require('./components/pages/mobile-sync') -const AddTokenPage = require('./components/pages/add-token') -const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') -const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token') -const CreateAccountPage = require('./components/pages/create-account') -const NoticeScreen = require('./components/pages/notice') - -const Loading = require('./components/loading-screen') -const LoadingNetwork = require('./components/loading-network-screen').default -const NetworkDropdown = require('./components/dropdowns/network-dropdown') -import AccountMenu from './components/account-menu' - -// Global Modals -const Modal = require('./components/modals/index').Modal -// Global Alert -const Alert = require('./components/alert') - -import AppHeader from './components/app-header' -import UnlockPage from './components/pages/unlock-page' - -import { - submittedPendingTransactionsSelector, -} from './selectors/transactions' - -// Routes -import { - DEFAULT_ROUTE, - LOCK_ROUTE, - UNLOCK_ROUTE, - SETTINGS_ROUTE, - REVEAL_SEED_ROUTE, - MOBILE_SYNC_ROUTE, - RESTORE_VAULT_ROUTE, - ADD_TOKEN_ROUTE, - CONFIRM_ADD_TOKEN_ROUTE, - CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, - NEW_ACCOUNT_ROUTE, - SEND_ROUTE, - CONFIRM_TRANSACTION_ROUTE, - INITIALIZE_ROUTE, - INITIALIZE_UNLOCK_ROUTE, - NOTICE_ROUTE, -} from './routes' - -// enums -import { - ENVIRONMENT_TYPE_NOTIFICATION, - ENVIRONMENT_TYPE_POPUP, -} from '../../app/scripts/lib/enums' - -class App extends Component { - componentWillMount () { - const { currentCurrency, setCurrentCurrencyToUSD } = this.props - - if (!currentCurrency) { - setCurrentCurrencyToUSD() - } - - this.props.history.listen((locationObj, action) => { - if (action === 'PUSH') { - const url = `&url=${encodeURIComponent('http://www.metamask.io/metametrics' + locationObj.pathname)}` - this.context.metricsEvent({}, { - currentPath: '', - pathname: locationObj.pathname, - url, - pageOpts: { - hideDimensions: true, - }, - }) - } - }) - } - - renderRoutes () { - return ( - - - - - - - - - - - - - - - - - - ) - } - - 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 ( -
setMouseUserState(true)} - onKeyDown={e => { - if (e.keyCode === 9) { - setMouseUserState(false) - } - }} - > - - - - { - !this.hideAppHeader() && ( - - ) - } - id === sidebarTransaction.id)} - hideSidebar={this.props.hideSidebar} - transitionName={sidebarTransitionName} - type={sidebarType} - sidebarProps={sidebar.props} - onOverlayClose={sidebarOnOverlayClose} - /> - - -
- { isLoading && } - { !isLoading && isLoadingNetwork && } - { 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, providerId } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = this.context.t('connectingToMainnet') - } else if (providerName === 'ropsten') { - name = this.context.t('connectingToRopsten') - } else if (providerName === 'kovan') { - name = this.context.t('connectingToKovan') - } else if (providerName === 'rinkeby') { - name = this.context.t('connectingToRinkeby') - } else { - name = this.context.t('connectingTo', [providerId]) - } - - return name - } - - getNetworkName () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = this.context.t('mainnet') - } else if (providerName === 'ropsten') { - name = this.context.t('ropsten') - } else if (providerName === 'kovan') { - name = this.context.t('kovan') - } else if (providerName === 'rinkeby') { - name = this.context.t('rinkeby') - } else { - name = this.context.t('unknownNetwork') - } - - return name - } -} - -App.propTypes = { - currentCurrency: PropTypes.string, - setCurrentCurrencyToUSD: PropTypes.func, - isLoading: PropTypes.bool, - loadingMessage: PropTypes.string, - alertMessage: PropTypes.string, - network: PropTypes.string, - provider: PropTypes.object, - frequentRpcListDetail: PropTypes.array, - currentView: PropTypes.object, - sidebar: PropTypes.object, - alertOpen: PropTypes.bool, - hideSidebar: PropTypes.func, - isOnboarding: PropTypes.bool, - isUnlocked: PropTypes.bool, - networkDropdownOpen: PropTypes.bool, - showNetworkDropdown: PropTypes.func, - hideNetworkDropdown: PropTypes.func, - history: PropTypes.object, - location: PropTypes.object, - dispatch: PropTypes.func, - toggleAccountMenu: PropTypes.func, - selectedAddress: PropTypes.string, - noActiveNotices: PropTypes.bool, - lostAccounts: PropTypes.array, - isInitialized: PropTypes.bool, - forgottenPassword: PropTypes.bool, - activeAddress: PropTypes.string, - unapprovedTxs: PropTypes.object, - seedWords: PropTypes.string, - submittedPendingTransactions: PropTypes.array, - unapprovedMsgCount: PropTypes.number, - unapprovedPersonalMsgCount: PropTypes.number, - unapprovedTypedMessagesCount: PropTypes.number, - welcomeScreenSeen: PropTypes.bool, - isPopup: PropTypes.bool, - isMouseUser: PropTypes.bool, - setMouseUserState: PropTypes.func, - t: PropTypes.func, - providerId: PropTypes.string, - providerRequests: PropTypes.array, -} - -function mapStateToProps (state) { - const { appState, metamask } = state - const { - networkDropdownOpen, - sidebar, - alertOpen, - alertMessage, - isLoading, - loadingMessage, - } = appState - - const accounts = getMetaMaskAccounts(state) - - const { - identities, - address, - keyrings, - isInitialized, - noActiveNotices, - seedWords, - unapprovedTxs, - nextUnreadNotice, - lostAccounts, - unapprovedMsgCount, - unapprovedPersonalMsgCount, - unapprovedTypedMessagesCount, - providerRequests, - } = metamask - const selected = address || Object.keys(accounts)[0] - - return { - // state from plugin - networkDropdownOpen, - sidebar, - alertOpen, - alertMessage, - isLoading, - loadingMessage, - noActiveNotices, - isInitialized, - isUnlocked: state.metamask.isUnlocked, - selectedAddress: state.metamask.selectedAddress, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), - isPopup: state.metamask.isPopup, - seedWords: state.metamask.seedWords, - submittedPendingTransactions: submittedPendingTransactionsSelector(state), - unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedMsgCount, - unapprovedPersonalMsgCount, - unapprovedTypedMessagesCount, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - nextUnreadNotice, - lostAccounts, - frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], - currentCurrency: state.metamask.currentCurrency, - isMouseUser: state.appState.isMouseUser, - isRevealingSeedWords: state.metamask.isRevealingSeedWords, - Qr: state.appState.Qr, - welcomeScreenSeen: state.metamask.welcomeScreenSeen, - providerId: getNetworkIdentifier(state), - - // state needed to get account dropdown temporarily rendering from app bar - identities, - selected, - keyrings, - providerRequests, - } -} - -function mapDispatchToProps (dispatch, ownProps) { - return { - dispatch, - hideSidebar: () => dispatch(actions.hideSidebar()), - showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), - hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), - toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), - setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), - } -} - -App.contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(App) diff --git a/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js deleted file mode 100644 index 8a171d0c6..000000000 --- a/ui/app/components/account-dropdown-mini/account-dropdown-mini.component.js +++ /dev/null @@ -1,84 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import AccountListItem from '../send/account-list-item/account-list-item.component' - -export default class AccountDropdownMini extends PureComponent { - static propTypes = { - accounts: PropTypes.array.isRequired, - closeDropdown: PropTypes.func, - disabled: PropTypes.bool, - dropdownOpen: PropTypes.bool, - onSelect: PropTypes.func, - openDropdown: PropTypes.func, - selectedAccount: PropTypes.object.isRequired, - } - - static defaultProps = { - closeDropdown: () => {}, - disabled: false, - dropdownOpen: false, - onSelect: () => {}, - openDropdown: () => {}, - } - - getListItemIcon (currentAccount, selectedAccount) { - return currentAccount.address === selectedAccount.address && ( - - ) - } - - renderDropdown () { - const { accounts, selectedAccount, closeDropdown, onSelect } = this.props - - return ( -
-
-
- { - accounts.map(account => ( - { - onSelect(account) - closeDropdown() - }} - icon={this.getListItemIcon(account, selectedAccount)} - /> - )) - } -
-
- ) - } - - render () { - const { disabled, selectedAccount, openDropdown, dropdownOpen } = this.props - - return ( -
- !disabled && openDropdown()} - displayBalance={false} - displayAddress={false} - icon={ - !disabled && - } - /> - { !disabled && dropdownOpen && this.renderDropdown() } -
- ) - } -} diff --git a/ui/app/components/account-dropdown-mini/index.js b/ui/app/components/account-dropdown-mini/index.js deleted file mode 100644 index cb0839e72..000000000 --- a/ui/app/components/account-dropdown-mini/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './account-dropdown-mini.component' diff --git a/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js deleted file mode 100644 index abd2f7c75..000000000 --- a/ui/app/components/account-dropdown-mini/tests/account-dropdown-mini.component.test.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import AccountDropdownMini from '../account-dropdown-mini.component' -import AccountListItem from '../../send/account-list-item/account-list-item.component' - -describe('AccountDropdownMini', () => { - it('should render an account with an icon', () => { - const accounts = [ - { - address: '0x1', - name: 'account1', - balance: '0x1', - }, - { - address: '0x2', - name: 'account2', - balance: '0x2', - }, - { - address: '0x3', - name: 'account3', - balance: '0x3', - }, - ] - - const wrapper = shallow( - - ) - - 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( - - ) - - 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( - - ) - - assert.ok(wrapper) - assert.equal(wrapper.find(AccountListItem).length, 1) - const accountListItemProps = wrapper.find(AccountListItem).at(0).props() - assert.equal(accountListItemProps.account.address, '0x1') - assert.equal(accountListItemProps.icon, false) - }) -}) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js deleted file mode 100644 index b05ba219c..000000000 --- a/ui/app/components/account-dropdowns.js +++ /dev/null @@ -1,338 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const actions = require('../actions') -const genAccountLink = require('etherscan-link').createAccountLink -const connect = require('react-redux').connect -const Dropdown = require('./dropdown').Dropdown -const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const copyToClipboard = require('copy-to-clipboard') -const { checksumAddress } = require('../util') - -import Identicon from './identicon' - -class AccountDropdowns extends Component { - constructor (props) { - super(props) - this.state = { - accountSelectorActive: false, - optionsMenuActive: false, - } - this.accountSelectorToggleClassName = 'accounts-selector' - this.optionsMenuToggleClassName = 'fa-ellipsis-h' - } - - renderAccounts () { - const { identities, selected, keyrings } = this.props - - return Object.keys(identities).map((key, index) => { - const identity = identities[key] - const isSelected = identity.address === selected - - const simpleAddress = identity.address.substring(2).toLowerCase() - - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - this.props.actions.showAccountDetail(identity.address) - }, - style: { - marginTop: index === 0 ? '5px' : '', - fontSize: '24px', - }, - }, - [ - h( - Identicon, - { - address: identity.address, - diameter: 32, - style: { - marginLeft: '10px', - }, - }, - ), - this.indicateIfLoose(keyring), - h('span', { - style: { - marginLeft: '20px', - fontSize: '24px', - maxWidth: '145px', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, identity.name || ''), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), - ] - ) - }) - } - - indicateIfLoose (keyring) { - try { // Sometimes keyrings aren't loaded yet: - const type = keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null - } catch (e) { return } - } - - renderAccountSelector () { - const { actions } = this.props - const { accountSelectorActive } = this.state - - return h( - Dropdown, - { - useCssTransition: true, // Hardcoded because account selector is temporarily in app-header - style: { - marginLeft: '-238px', - marginTop: '38px', - minWidth: '180px', - overflowY: 'auto', - maxHeight: '300px', - width: '300px', - }, - innerStyle: { - padding: '8px 25px', - }, - isOpen: accountSelectorActive, - onClickOutside: (event) => { - const { classList } = event.target - const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName) - if (accountSelectorActive && isNotToggleElement) { - this.setState({ accountSelectorActive: false }) - } - }, - }, - [ - ...this.renderAccounts(), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.addNewAccount(), - }, - [ - h( - Identicon, - { - style: { - marginLeft: '10px', - }, - diameter: 32, - }, - ), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, this.context.t('createAccount')), - ], - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showImportPage(), - }, - [ - h( - Identicon, - { - style: { - marginLeft: '10px', - }, - diameter: 32, - }, - ), - h('span', { - style: { - marginLeft: '20px', - fontSize: '24px', - marginBottom: '5px', - }, - }, this.context.t('importAccount')), - ] - ), - ] - ) - } - - renderAccountOptions () { - const { actions } = this.props - const { optionsMenuActive } = this.state - - return h( - Dropdown, - { - style: { - marginLeft: '-215px', - minWidth: '180px', - }, - isOpen: optionsMenuActive, - onClickOutside: (event) => { - const { classList } = event.target - const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName) - if (optionsMenuActive && isNotToggleElement) { - this.setState({ optionsMenuActive: false }) - } - }, - }, - [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected, network } = this.props - const url = genAccountLink(selected, network) - global.platform.openWindow({ url }) - }, - }, - this.context.t('etherscanView'), - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected, identities } = this.props - var identity = identities[selected] - actions.showQrView(selected, identity ? identity.name : '') - }, - }, - this.context.t('showQRCode'), - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected } = this.props - copyToClipboard(checksumAddress(selected)) - }, - }, - this.context.t('copyAddress'), - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - actions.requestAccountExport() - }, - }, - this.context.t('exportPrivateKey'), - ), - ] - ) - } - - render () { - const { metricsEvent } = this.context - const { style, enableAccountsSelector, enableAccountOptions } = this.props - const { optionsMenuActive, accountSelectorActive } = this.state - - return h( - 'span', - { - style: style, - }, - [ - enableAccountsSelector && h( - // 'i.fa.fa-angle-down', - 'div.cursor-pointer.color-orange.accounts-selector', - { - style: { - // fontSize: '1.8em', - background: 'url(images/switch_acc.svg) white center center no-repeat', - height: '25px', - width: '25px', - transform: 'scale(0.75)', - marginRight: '3px', - }, - onClick: (event) => { - event.stopPropagation() - this.setState({ - accountSelectorActive: !accountSelectorActive, - optionsMenuActive: false, - }) - }, - }, - this.renderAccountSelector(), - ), - enableAccountOptions && h( - 'i.fa.fa-ellipsis-h', - { - style: { - marginRight: '0.5em', - fontSize: '1.8em', - }, - onClick: (event) => { - metricsEvent({ - eventOpts: { - category: 'Accounts', - action: 'userClick', - name: 'accountsOpenedMenu', - }, - pageOpts: { - section: 'header', - component: 'accountDropdownIcon', - }, - }) - event.stopPropagation() - this.setState({ - accountSelectorActive: false, - optionsMenuActive: !optionsMenuActive, - }) - }, - }, - this.renderAccountOptions() - ), - ] - ) - } -} - -AccountDropdowns.defaultProps = { - enableAccountsSelector: false, - enableAccountOptions: false, -} - -AccountDropdowns.propTypes = { - identities: PropTypes.objectOf(PropTypes.object), - selected: PropTypes.string, - keyrings: PropTypes.array, - actions: PropTypes.objectOf(PropTypes.func), - network: PropTypes.string, - style: PropTypes.object, - enableAccountOptions: PropTypes.bool, - enableAccountsSelector: PropTypes.bool, - t: PropTypes.func, -} - -const mapDispatchToProps = (dispatch) => { - return { - actions: { - showConfigPage: () => dispatch(actions.showConfigPage()), - requestAccountExport: () => dispatch(actions.requestExportAccount()), - showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), - addNewAccount: () => dispatch(actions.addNewAccount()), - showImportPage: () => dispatch(actions.showImportPage()), - showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), - }, - } -} - -AccountDropdowns.contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, -} - -module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), -} diff --git a/ui/app/components/account-menu/account-menu.component.js b/ui/app/components/account-menu/account-menu.component.js deleted file mode 100644 index f7c962874..000000000 --- a/ui/app/components/account-menu/account-menu.component.js +++ /dev/null @@ -1,340 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import debounce from 'lodash.debounce' -import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu' -import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' -import { getEnvironmentType } from '../../../../app/scripts/lib/util' -import Tooltip from '../tooltip' -import Identicon from '../identicon' -import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' -import { PRIMARY } from '../../constants/common' -import { - SETTINGS_ROUTE, - INFO_ROUTE, - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, - DEFAULT_ROUTE, -} from '../../routes' - -export default class AccountMenu extends PureComponent { - static contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, - } - - static propTypes = { - accounts: PropTypes.object, - history: PropTypes.object, - identities: PropTypes.object, - isAccountMenuOpen: PropTypes.bool, - prevIsAccountMenuOpen: PropTypes.bool, - keyrings: PropTypes.array, - lockMetamask: PropTypes.func, - selectedAddress: PropTypes.string, - showAccountDetail: PropTypes.func, - showRemoveAccountConfirmationModal: PropTypes.func, - toggleAccountMenu: PropTypes.func, - } - - state = { - atAccountListBottom: false, - } - - componentDidUpdate (prevProps) { - const { prevIsAccountMenuOpen } = prevProps - const { isAccountMenuOpen } = this.props - - if (!prevIsAccountMenuOpen && isAccountMenuOpen) { - this.setAtAccountListBottom() - } - } - - renderAccounts () { - const { - identities, - accounts, - selectedAddress, - keyrings, - showAccountDetail, - } = this.props - - const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - - return accountOrder.filter(address => !!identities[address]).map(address => { - const identity = identities[address] - const isSelected = identity.address === selectedAddress - - const balanceValue = accounts[address] ? accounts[address].balance : '' - const simpleAddress = identity.address.substring(2).toLowerCase() - - const keyring = keyrings.find(kr => { - return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address) - }) - - return ( -
{ - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Main Menu', - name: 'Switched Account', - }, - }) - showAccountDetail(identity.address) - }} - key={identity.address} - > -
- { isSelected &&
} -
- -
-
- { identity.name || '' } -
- -
- { this.renderKeyringType(keyring) } - { this.renderRemoveAccount(keyring, identity) } -
- ) - }) - } - - 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 && ( - - this.removeAccount(e, identity)} - /> - - ) - } - - 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 && ( -
- { label } -
- ) - } - - 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 && ( -
- -
- ) - } - - render () { - const { t } = this.context - const { - isAccountMenuOpen, - toggleAccountMenu, - lockMetamask, - history, - } = this.props - const { metricsEvent } = this.context - - return ( - - - - { t('myAccounts') } - - - -
-
- { this.renderAccounts() } -
- { this.renderScrollButton() } -
- - { - toggleAccountMenu() - metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Main Menu', - name: 'Clicked Create Account', - }, - }) - history.push(NEW_ACCOUNT_ROUTE) - }} - icon={ - - } - text={t('createAccount')} - /> - { - toggleAccountMenu() - metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Main Menu', - name: 'Clicked Import Account', - }, - }) - history.push(IMPORT_ACCOUNT_ROUTE) - }} - icon={ - - } - text={t('importAccount')} - /> - { - 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={ - - } - text={t('connectHardwareWallet')} - /> - - { - toggleAccountMenu() - history.push(INFO_ROUTE) - }} - icon={ - - } - text={t('infoHelp')} - /> - { - toggleAccountMenu() - history.push(SETTINGS_ROUTE) - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Main Menu', - name: 'Opened Settings', - }, - }) - }} - icon={ - - } - text={t('settings')} - /> -
- ) - } -} diff --git a/ui/app/components/account-menu/account-menu.container.js b/ui/app/components/account-menu/account-menu.container.js deleted file mode 100644 index 93246ec72..000000000 --- a/ui/app/components/account-menu/account-menu.container.js +++ /dev/null @@ -1,62 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import { withRouter } from 'react-router-dom' -import { - toggleAccountMenu, - showAccountDetail, - hideSidebar, - lockMetamask, - hideWarning, - showConfigPage, - showInfoPage, - showModal, -} from '../../actions' -import { getMetaMaskAccounts } from '../../selectors' -import AccountMenu from './account-menu.component' - -function mapStateToProps (state) { - const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state - - return { - selectedAddress, - isAccountMenuOpen, - keyrings, - identities, - accounts: getMetaMaskAccounts(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - toggleAccountMenu: () => dispatch(toggleAccountMenu()), - showAccountDetail: address => { - dispatch(showAccountDetail(address)) - dispatch(hideSidebar()) - dispatch(toggleAccountMenu()) - }, - lockMetamask: () => { - dispatch(lockMetamask()) - dispatch(hideWarning()) - dispatch(hideSidebar()) - dispatch(toggleAccountMenu()) - }, - showConfigPage: () => { - dispatch(showConfigPage()) - dispatch(hideSidebar()) - dispatch(toggleAccountMenu()) - }, - showInfoPage: () => { - dispatch(showInfoPage()) - dispatch(hideSidebar()) - dispatch(toggleAccountMenu()) - }, - showRemoveAccountConfirmationModal: identity => { - return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) - }, - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(AccountMenu) diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js deleted file mode 100644 index b2b4e4c6f..000000000 --- a/ui/app/components/account-menu/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './account-menu.container' diff --git a/ui/app/components/account-menu/index.scss b/ui/app/components/account-menu/index.scss deleted file mode 100644 index 9a61bf887..000000000 --- a/ui/app/components/account-menu/index.scss +++ /dev/null @@ -1,177 +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: 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/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/add-token-button/add-token-button.component.js b/ui/app/components/add-token-button/add-token-button.component.js deleted file mode 100644 index 10887aed8..000000000 --- a/ui/app/components/add-token-button/add-token-button.component.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types' -import React, {PureComponent} from 'react' - -export default class AddTokenButton extends PureComponent { - static contextTypes = { - t: PropTypes.func.isRequired, - } - - static defaultProps = { - onClick: () => {}, - } - - static propTypes = { - onClick: PropTypes.func, - } - - render () { - const { t } = this.context - const { onClick } = this.props - - return ( -
-

{t('missingYourTokens')}

-

{t('clickToAdd', [t('addToken')])}

-
- {t('addToken')} -
-
- ) - } -} diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/add-token-button/index.js deleted file mode 100644 index 15c4fe6ca..000000000 --- a/ui/app/components/add-token-button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './add-token-button.component' diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/add-token-button/index.scss deleted file mode 100644 index 39f404716..000000000 --- a/ui/app/components/add-token-button/index.scss +++ /dev/null @@ -1,26 +0,0 @@ -.add-token-button { - display: flex; - flex-direction: column; - color: lighten($scorpion, 25%); - width: 185px; - margin: 36px auto; - text-align: center; - - &__help-header { - font-weight: bold; - font-size: 1rem; - } - - &__help-desc { - font-size: 0.75rem; - margin-top: 1rem; - } - - &__button { - font-size: 0.75rem; - margin: 1rem; - text-transform: uppercase; - color: $curious-blue; - cursor: pointer; - } -} diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js deleted file mode 100644 index 5620d847a..000000000 --- a/ui/app/components/alert/index.js +++ /dev/null @@ -1,62 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') - -class Alert extends Component { - - constructor (props) { - super(props) - - this.state = { - visble: false, - msg: false, - className: '', - } - } - - componentWillReceiveProps (nextProps) { - if (!this.props.visible && nextProps.visible) { - this.animateIn(nextProps) - } else if (this.props.visible && !nextProps.visible) { - this.animateOut(nextProps) - } - } - - animateIn (props) { - this.setState({ - msg: props.msg, - visible: true, - className: '.visible', - }) - } - - animateOut (props) { - this.setState({ - msg: null, - className: '.hidden', - }) - - setTimeout(_ => { - this.setState({visible: false}) - }, 500) - - } - - render () { - if (this.state.visible) { - return ( - h(`div.global-alert${this.state.className}`, {}, - h('a.msg', {}, this.state.msg) - ) - ) - } - return null - } -} - -Alert.propTypes = { - visible: PropTypes.bool.isRequired, - msg: PropTypes.string, -} -module.exports = Alert - diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js deleted file mode 100644 index 14f8b9f30..000000000 --- a/ui/app/components/app-header/app-header.component.js +++ /dev/null @@ -1,127 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import Identicon from '../identicon' -import { DEFAULT_ROUTE } from '../../routes' -const NetworkIndicator = require('../network') - -export default class AppHeader extends PureComponent { - static propTypes = { - history: PropTypes.object, - network: PropTypes.string, - provider: PropTypes.object, - networkDropdownOpen: PropTypes.bool, - showNetworkDropdown: PropTypes.func, - hideNetworkDropdown: PropTypes.func, - toggleAccountMenu: PropTypes.func, - selectedAddress: PropTypes.string, - isUnlocked: PropTypes.bool, - hideNetworkIndicator: PropTypes.bool, - disabled: PropTypes.bool, - isAccountMenuOpen: PropTypes.bool, - } - - static contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, - } - - handleNetworkIndicatorClick (event) { - event.preventDefault() - event.stopPropagation() - - const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props - - if (networkDropdownOpen === false) { - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Home', - name: 'Opened Network Menu', - }, - }) - showNetworkDropdown() - } else { - hideNetworkDropdown() - } - } - - renderAccountMenu () { - const { isUnlocked, toggleAccountMenu, selectedAddress, disabled, isAccountMenuOpen } = this.props - - return isUnlocked && ( -
{ - if (!disabled) { - !isAccountMenuOpen && this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Home', - name: 'Opened Main Menu', - }, - }) - toggleAccountMenu() - } - }} - > - -
- ) - } - - render () { - const { - history, - network, - provider, - isUnlocked, - hideNetworkIndicator, - disabled, - } = this.props - - return ( -
-
-
history.push(DEFAULT_ROUTE)} - > - - -
-
- { - !hideNetworkIndicator && ( -
- this.handleNetworkIndicatorClick(event)} - disabled={disabled} - /> -
- ) - } - { this.renderAccountMenu() } -
-
-
- ) - } -} diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js deleted file mode 100644 index 1abc2afeb..000000000 --- a/ui/app/components/app-header/app-header.container.js +++ /dev/null @@ -1,40 +0,0 @@ -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { compose } from 'recompose' - -import AppHeader from './app-header.component' -const actions = require('../../actions') - -const mapStateToProps = state => { - const { appState, metamask } = state - const { networkDropdownOpen } = appState - const { - network, - provider, - selectedAddress, - isUnlocked, - isAccountMenuOpen, - } = metamask - - return { - networkDropdownOpen, - network, - provider, - selectedAddress, - isUnlocked, - isAccountMenuOpen, - } -} - -const mapDispatchToProps = dispatch => { - return { - showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), - hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), - toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(AppHeader) diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js deleted file mode 100644 index 6de2f9c78..000000000 --- a/ui/app/components/app-header/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './app-header.container' diff --git a/ui/app/components/app-header/index.scss b/ui/app/components/app-header/index.scss deleted file mode 100644 index 325844af5..000000000 --- a/ui/app/components/app-header/index.scss +++ /dev/null @@ -1,90 +0,0 @@ -.app-header { - align-items: center; - background: $gallery; - position: relative; - z-index: $header-z-index; - display: flex; - flex-flow: column nowrap; - width: 100%; - flex: 0 0 auto; - - @media screen and (max-width: 575px) { - padding: 12px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); - z-index: $mobile-header-z-index; - } - - @media screen and (min-width: 576px) { - height: 75px; - justify-content: center; - - &--back-drop { - &::after { - content: ''; - position: absolute; - width: 100%; - height: 32px; - background: $gallery; - bottom: -32px; - } - } - } - - &__metafox-logo { - cursor: pointer; - - &--icon { - @media screen and (min-width: $break-large) { - display: none; - } - } - - &--horizontal { - @media screen and (max-width: $break-small) { - display: none; - } - } - } - - &__contents { - display: flex; - justify-content: space-between; - flex-flow: row nowrap; - width: 100%; - - @media screen and (max-width: 575px) { - height: 100%; - } - - @media screen and (min-width: 576px) { - width: 85vw; - } - - @media screen and (min-width: 769px) { - width: 80vw; - } - - @media screen and (min-width: 1281px) { - width: 62vw; - } - } - - &__logo-container { - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - } - - &__account-menu-container { - display: flex; - flex-flow: row nowrap; - align-items: center; - } - - &__network-component-wrapper { - display: flex; - flex-direction: row; - align-items: center; - } -} 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 ( +
{ + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Switched Account', + }, + }) + showAccountDetail(identity.address) + }} + key={identity.address} + > +
+ { isSelected &&
} +
+ +
+
+ { identity.name || '' } +
+ +
+ { this.renderKeyringType(keyring) } + { this.renderRemoveAccount(keyring, identity) } +
+ ) + }) + } + + 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 && ( + +
this.removeAccount(e, identity)} + /> + + ) + } + + 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 && ( +
+ { label } +
+ ) + } + + 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 && ( +
+ +
+ ) + } + + render () { + const { t } = this.context + const { + isAccountMenuOpen, + toggleAccountMenu, + lockMetamask, + history, + } = this.props + const { metricsEvent } = this.context + + return ( + + + + { t('myAccounts') } + + + +
+
+ { this.renderAccounts() } +
+ { this.renderScrollButton() } +
+ + { + toggleAccountMenu() + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Clicked Create Account', + }, + }) + history.push(NEW_ACCOUNT_ROUTE) + }} + icon={ + + } + text={t('createAccount')} + /> + { + toggleAccountMenu() + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Clicked Import Account', + }, + }) + history.push(IMPORT_ACCOUNT_ROUTE) + }} + icon={ + + } + text={t('importAccount')} + /> + { + 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={ + + } + text={t('connectHardwareWallet')} + /> + + { + toggleAccountMenu() + history.push(INFO_ROUTE) + }} + icon={ + + } + text={t('infoHelp')} + /> + { + toggleAccountMenu() + history.push(SETTINGS_ROUTE) + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Opened Settings', + }, + }) + }} + icon={ + + } + text={t('settings')} + /> +
+ ) + } +} 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/app/add-token-button/add-token-button.component.js b/ui/app/components/app/add-token-button/add-token-button.component.js new file mode 100644 index 000000000..10887aed8 --- /dev/null +++ b/ui/app/components/app/add-token-button/add-token-button.component.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +export default class AddTokenButton extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static defaultProps = { + onClick: () => {}, + } + + static propTypes = { + onClick: PropTypes.func, + } + + render () { + const { t } = this.context + const { onClick } = this.props + + return ( +
+

{t('missingYourTokens')}

+

{t('clickToAdd', [t('addToken')])}

+
+ {t('addToken')} +
+
+ ) + } +} diff --git a/ui/app/components/app/add-token-button/index.js b/ui/app/components/app/add-token-button/index.js new file mode 100644 index 000000000..15c4fe6ca --- /dev/null +++ b/ui/app/components/app/add-token-button/index.js @@ -0,0 +1 @@ +export { default } from './add-token-button.component' diff --git a/ui/app/components/app/add-token-button/index.scss b/ui/app/components/app/add-token-button/index.scss new file mode 100644 index 000000000..39f404716 --- /dev/null +++ b/ui/app/components/app/add-token-button/index.scss @@ -0,0 +1,26 @@ +.add-token-button { + display: flex; + flex-direction: column; + color: lighten($scorpion, 25%); + width: 185px; + margin: 36px auto; + text-align: center; + + &__help-header { + font-weight: bold; + font-size: 1rem; + } + + &__help-desc { + font-size: 0.75rem; + margin-top: 1rem; + } + + &__button { + font-size: 0.75rem; + margin: 1rem; + text-transform: uppercase; + color: $curious-blue; + cursor: pointer; + } +} 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 && ( +
{ + if (!disabled) { + !isAccountMenuOpen && this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Main Menu', + }, + }) + toggleAccountMenu() + } + }} + > + +
+ ) + } + + render () { + const { + history, + network, + provider, + isUnlocked, + hideNetworkIndicator, + disabled, + } = this.props + + return ( +
+
+
history.push(DEFAULT_ROUTE)} + > + + +
+
+ { + !hideNetworkIndicator && ( +
+ this.handleNetworkIndicatorClick(event)} + disabled={disabled} + /> +
+ ) + } + { this.renderAccountMenu() } +
+
+
+ ) + } +} 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/app-header/index.js b/ui/app/components/app/app-header/index.js new file mode 100644 index 000000000..6de2f9c78 --- /dev/null +++ b/ui/app/components/app/app-header/index.js @@ -0,0 +1 @@ +export { default } from './app-header.container' diff --git a/ui/app/components/app/app-header/index.scss b/ui/app/components/app/app-header/index.scss new file mode 100644 index 000000000..325844af5 --- /dev/null +++ b/ui/app/components/app/app-header/index.scss @@ -0,0 +1,90 @@ +.app-header { + align-items: center; + background: $gallery; + position: relative; + z-index: $header-z-index; + display: flex; + flex-flow: column nowrap; + width: 100%; + flex: 0 0 auto; + + @media screen and (max-width: 575px) { + padding: 12px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); + z-index: $mobile-header-z-index; + } + + @media screen and (min-width: 576px) { + height: 75px; + justify-content: center; + + &--back-drop { + &::after { + content: ''; + position: absolute; + width: 100%; + height: 32px; + background: $gallery; + bottom: -32px; + } + } + } + + &__metafox-logo { + cursor: pointer; + + &--icon { + @media screen and (min-width: $break-large) { + display: none; + } + } + + &--horizontal { + @media screen and (max-width: $break-small) { + display: none; + } + } + } + + &__contents { + display: flex; + justify-content: space-between; + flex-flow: row nowrap; + width: 100%; + + @media screen and (max-width: 575px) { + height: 100%; + } + + @media screen and (min-width: 576px) { + width: 85vw; + } + + @media screen and (min-width: 769px) { + width: 80vw; + } + + @media screen and (min-width: 1281px) { + width: 62vw; + } + } + + &__logo-container { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + } + + &__account-menu-container { + display: flex; + flex-flow: row nowrap; + align-items: center; + } + + &__network-component-wrapper { + display: flex; + flex-direction: row; + align-items: center; + } +} diff --git a/ui/app/components/app/bn-as-decimal-input.js b/ui/app/components/app/bn-as-decimal-input.js new file mode 100644 index 000000000..9a033f893 --- /dev/null +++ b/ui/app/components/app/bn-as-decimal-input.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 ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') +const connect = require('react-redux').connect + +BnAsDecimalInput.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect()(BnAsDecimalInput) + + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) + const newValue = this.downsize(valueString, scale) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min: newMin, + max: newMax, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max, scale, suffix } = this.props + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) + let message = name ? name + ' ' : '' + + if (min && max) { + message += this.context.t('betweenMinAndMax', [`${newMin} ${suffix}`, `${newMax} ${suffix}`]) + } else if (min) { + message += this.context.t('greaterThanMin', [`${newMin} ${suffix}`]) + } else if (max) { + message += this.context.t('lessThanMax', [`${newMax} ${suffix}`]) + } else { + message += this.context.t('invalidInput') + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var adjustedNumber = number + while (adjustedNumber.length < scale) { + adjustedNumber = '0' + adjustedNumber + } + return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} 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 ( +
+
+ { label } +
+
+
onHeaderClick && onHeaderClick()} + > + { headerText } +
+ { + primaryText + ? ( +
+ { primaryText } +
+ ) : ( + + ) + } + { + secondaryText + ? ( +
+ { secondaryText } +
+ ) : ( + + ) + } +
+
+ ) +} + +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/app/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js new file mode 100644 index 000000000..056afff04 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js @@ -0,0 +1 @@ +export { default } from './confirm-detail-row.component' 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/app/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 new file mode 100644 index 000000000..c8507985d --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -0,0 +1,64 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import ConfirmDetailRow from '../confirm-detail-row.component.js' +import sinon from 'sinon' + +const propsMethodSpies = { + onHeaderClick: sinon.spy(), +} + +describe('Confirm Detail Row Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow( + + ) + }) + + describe('render', () => { + it('should render a div with a confirm-detail-row class', () => { + assert.equal(wrapper.find('div.confirm-detail-row').length, 1) + }) + + it('should render the label as a child of the confirm-detail-row__label', () => { + assert.equal(wrapper.find('.confirm-detail-row > .confirm-detail-row__label').childAt(0).text(), 'mockLabel') + }) + + it('should render the headerText as a child of the confirm-detail-row__header-text', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText') + }) + + it('should render the primaryText as a child of the confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText') + }) + + it('should render the ethText as a child of the confirm-detail-row__secondary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText') + }) + + it('should set the fiatTextColor on confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor') + }) + + it('should assure the confirm-detail-row__header-text classname is correct', () => { + assert.equal(wrapper.find('.confirm-detail-row__header-text').props().className, 'confirm-detail-row__header-text mockHeaderClass') + }) + + it('should call onHeaderClick when headerText div gets clicked', () => { + wrapper.find('.confirm-detail-row__header-text').props().onClick() + assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) + }) + }) +}) 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 ( + + + { detailsComponent } + + + { dataComponent } + + + ) + } + + render () { + const { + action, + errorKey, + errorMessage, + title, + titleComponent, + subtitle, + subtitleComponent, + hideSubtitle, + identiconAddress, + nonce, + assetImage, + summaryComponent, + detailsComponent, + dataComponent, + warning, + } = this.props + + return ( +
+ { + warning && ( + + ) + } + { + summaryComponent || ( + + ) + } + { this.renderContent() } + { + (errorKey || errorMessage) && ( +
+ +
+ ) + } +
+ ) + } +} 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 ( +
+
+
+ { action } +
+ { + nonce && ( +
+ { `#${nonce}` } +
+ ) + } +
+
+ { + identiconAddress && ( + + ) + } +
+ { titleComponent || title } +
+
+ { + hideSubtitle ||
+ { subtitleComponent || subtitle } +
+ } +
+ ) +} + +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/app/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 new file mode 100644 index 000000000..ed1b28cf2 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-summary.component' diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..7f0f5d37a --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -0,0 +1,54 @@ +.confirm-page-container-summary { + padding: 16px 24px 0; + background-color: #f9fafa; + height: 133px; + box-sizing: border-box; + + &__action-row { + display: flex; + justify-content: space-between; + } + + &__action { + text-transform: uppercase; + color: $oslo-gray; + font-size: .75rem; + padding: 3px 8px; + border: 1px solid $oslo-gray; + border-radius: 4px; + display: inline-block; + } + + &__nonce { + color: $oslo-gray; + } + + &__title { + padding: 4px 0; + display: flex; + align-items: center; + } + + &__identicon { + flex: 0 0 auto; + margin-right: 8px; + } + + &__title-text { + font-size: 2.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__subtitle { + color: $oslo-gray; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &--border { + border-bottom: 1px solid $geyser; + } +} diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..79901c8fc --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerWarning = props => { + return ( +
+ +
+ { props.warning } +
+
+ ) +} + +ConfirmPageContainerWarning.propTypes = { + warning: PropTypes.string, +} + +export default ConfirmPageContainerWarning diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..6e48bd144 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-warning.component' diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..50545a1a2 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss @@ -0,0 +1,18 @@ +.confirm-page-container-warning { + background-color: #fffcdb; + display: flex; + justify-content: center; + align-items: center; + border-bottom: 1px solid $geyser; + padding: 12px 24px; + + &__icon { + flex: 0 0 auto; + margin-right: 16px; + } + + &__warning { + font-size: .75rem; + color: #5f5922; + } +} diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js new file mode 100644 index 000000000..4dfd89d92 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js @@ -0,0 +1,3 @@ +export { default } from './confirm-page-container-content.component' +export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary' +export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning' 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 ( +
+
+ + onEdit()} + > + { this.context.t('edit') } + +
+ { !isFullScreen && } +
+ ) + } + + render () { + const { children } = this.props + + return ( +
+ { this.renderTop() } + { children } +
+ ) + } +} diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js new file mode 100644 index 000000000..71feb6931 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-header.component' 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 ( +
+
+
onNextTx(firstTx)}> + +
+
onNextTx(prevTxId)}> + +
+
+
+
+ {positionOfCurrentTx} {ofText} {totalTx} +
+
+ {requestsWaitingText} +
+
+
+
onNextTx(nextTxId)}> + +
+
onNextTx(lastTx)}> + +
+
+
+ ) +} + +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 ( +
+ ) + } +} 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/app/customize-gas-modal/gas-modal-card.js b/ui/app/components/app/customize-gas-modal/gas-modal-card.js new file mode 100644 index 000000000..23754d819 --- /dev/null +++ b/ui/app/components/app/customize-gas-modal/gas-modal-card.js @@ -0,0 +1,54 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const InputNumber = require('../input-number.js') +// const GasSlider = require('./gas-slider.js') + +module.exports = GasModalCard + +inherits(GasModalCard, Component) +function GasModalCard () { + Component.call(this) +} + +GasModalCard.prototype.render = function () { + const { + // memo, + onChange, + unitLabel, + value, + min, + // max, + step, + title, + copy, + } = this.props + + return h('div.send-v2__gas-modal-card', [ + + h('div.send-v2__gas-modal-card__title', {}, title), + + h('div.send-v2__gas-modal-card__copy', {}, copy), + + h(InputNumber, { + unitLabel, + step, + // max, + min, + placeholder: '0', + value, + onChange, + }), + + // h(GasSlider, { + // value, + // step, + // max, + // min, + // onChange, + // }), + + ]) + +} + diff --git a/ui/app/components/app/customize-gas-modal/gas-slider.js b/ui/app/components/app/customize-gas-modal/gas-slider.js new file mode 100644 index 000000000..69fd6f985 --- /dev/null +++ b/ui/app/components/app/customize-gas-modal/gas-slider.js @@ -0,0 +1,50 @@ +// const Component = require('react').Component +// const h = require('react-hyperscript') +// const inherits = require('util').inherits + +// module.exports = GasSlider + +// inherits(GasSlider, Component) +// function GasSlider () { +// Component.call(this) +// } + +// GasSlider.prototype.render = function () { +// const { +// memo, +// identities, +// onChange, +// unitLabel, +// value, +// id, +// step, +// max, +// min, +// } = this.props + +// return h('div.gas-slider', [ + +// h('input.gas-slider__input', { +// type: 'range', +// step, +// max, +// min, +// value, +// id: 'gasSlider', +// onChange: event => onChange(event.target.value), +// }, []), + +// h('div.gas-slider__bar', [ + +// h('div.gas-slider__low'), + +// h('div.gas-slider__mid'), + +// h('div.gas-slider__high'), + +// ]), + +// ]) + +// } + 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/app/dropdowns/components/dropdown.js b/ui/app/components/app/dropdowns/components/dropdown.js new file mode 100644 index 000000000..149f063a7 --- /dev/null +++ b/ui/app/components/app/dropdowns/components/dropdown.js @@ -0,0 +1,112 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const MenuDroppo = require('../../menu-droppo') +const extend = require('xtend') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { + containerClassName, + isOpen, + onClickOutside, + style, + innerStyle, + children, + useCssTransition, + } = this.props + + const innerStyleDefaults = extend({ + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, innerStyle) + + return h( + MenuDroppo, + { + containerClassName, + useCssTransition, + isOpen, + zIndex: 55, + onClickOutside, + style, + innerStyle: innerStyleDefaults, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { + color:rgb(225, 225, 225); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 4px; + } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, + useCssTransition: false, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, + onClickOutside: PropTypes.func, + innerStyle: PropTypes.object, + useCssTransition: PropTypes.bool, + containerClassName: PropTypes.string, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children, style } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: Object.assign({ + listStyle: 'none', + padding: '8px 0px', + fontSize: '18px', + fontStyle: 'normal', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + color: 'white', + }, style), + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/ui/app/components/app/dropdowns/components/menu.js b/ui/app/components/app/dropdowns/components/menu.js new file mode 100644 index 000000000..f6d8a139e --- /dev/null +++ b/ui/app/components/app/dropdowns/components/menu.js @@ -0,0 +1,51 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') + +inherits(Menu, Component) +function Menu () { Component.call(this) } + +Menu.prototype.render = function () { + const { className = '', children, isShowing } = this.props + return isShowing + ? h('div', { className: `menu ${className}` }, children) + : h('noscript') +} + +inherits(Item, Component) +function Item () { Component.call(this) } + +Item.prototype.render = function () { + const { + icon, + children, + text, + className = '', + onClick, + } = this.props + const itemClassName = `menu__item ${className} ${onClick ? 'menu__item--clickable' : ''}` + const iconComponent = icon ? h('div.menu__item__icon', [icon]) : null + const textComponent = text ? h('div.menu__item__text', text) : null + + return children + ? h('div', { className: itemClassName, onClick }, children) + : h('div.menu__item', { className: itemClassName, onClick }, [ iconComponent, textComponent ] + .filter(d => Boolean(d)) + ) +} + +inherits(Divider, Component) +function Divider () { Component.call(this) } + +Divider.prototype.render = function () { + return h('div.menu__divider') +} + +inherits(CloseArea, Component) +function CloseArea () { Component.call(this) } + +CloseArea.prototype.render = function () { + return h('div.menu__close-area', { onClick: this.props.onClick }) +} + +module.exports = { Menu, Item, Divider, CloseArea } 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/app/dropdowns/index.js b/ui/app/components/app/dropdowns/index.js new file mode 100644 index 000000000..605507058 --- /dev/null +++ b/ui/app/components/app/dropdowns/index.js @@ -0,0 +1,11 @@ +// Reusable Dropdown Components +// TODO: Refactor into separate components +const Dropdown = require('./components/dropdown').Dropdown + +// App-Specific Instances +const NetworkDropdown = require('./network-dropdown').default + +module.exports = { + NetworkDropdown, + Dropdown, +} 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..fcc7a8681 --- /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 { SETTINGS_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(SETTINGS_ROUTE), + style: dropdownMenuItemStyle, + }, + [ + activeNetwork === 'custom' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + isSelected: activeNetwork === 'custom', + innerBorder: '1px solid #9b9b9b', + }), + h('span.network-name-item', { + style: { + color: activeNetwork === 'custom' ? '#ffffff' : '#9b9b9b', + }, + }, this.context.t('customRPC')), + ] + ), + + ]) +} + +NetworkDropdown.prototype.handleClick = function (newProviderType) { + const { provider: { type: providerType }, setProviderType } = this.props + const { metricsEvent } = this.context + + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Switched Networks', + }, + customVariables: { + fromNetwork: providerType, + toNetwork: newProviderType, + }, + }) + setProviderType(newProviderType) +} + +NetworkDropdown.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = this.context.t('mainnet') + } else if (providerName === 'ropsten') { + name = this.context.t('ropsten') + } else if (providerName === 'kovan') { + name = this.context.t('kovan') + } else if (providerName === 'rinkeby') { + name = this.context.t('rinkeby') + } else { + name = provider.nickname || this.context.t('unknownNetwork') + } + + return name +} + +NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { + const props = this.props + const reversedRpcListDetail = rpcListDetail.slice().reverse() + + return reversedRpcListDetail.map((entry) => { + const rpc = entry.rpcUrl + const ticker = entry.ticker || 'ETH' + const nickname = entry.nickname || '' + const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget + + if ((rpc === 'http://localhost:8545') || currentRpcTarget) { + return null + } else { + const chainId = entry.chainId + return h( + DropdownMenuItem, + { + key: `common${rpc}`, + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname), + style: { + fontSize: '16px', + lineHeight: '20px', + padding: '12px 0', + }, + }, + [ + currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h('i.fa.fa-question-circle.fa-med.menu-icon-circle'), + h('span.network-name-item', { + style: { + color: currentRpcTarget ? '#ffffff' : '#9b9b9b', + }, + }, nickname || rpc), + h('i.fa.fa-times.delete', + { + onClick: (e) => { + e.stopPropagation() + props.delRpcTarget(rpc) + }, + }), + ] + ) + } + }) +} + +NetworkDropdown.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type, ticker, nickname } = provider + const props = this.props + const network = props.network + + if (type !== 'rpc') return null + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h( + DropdownMenuItem, + { + key: rpcTarget, + onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname), + closeMenu: () => this.props.hideNetworkDropdown(), + style: { + fontSize: '16px', + lineHeight: '20px', + padding: '12px 0', + }, + }, + [ + h('i.fa.fa-check'), + h('i.fa.fa-question-circle.fa-med.menu-icon-circle'), + h('span.network-name-item', { + style: { + color: '#ffffff', + }, + }, nickname || rpcTarget), + ] + ) + } +} diff --git a/ui/app/components/app/dropdowns/simple-dropdown.js b/ui/app/components/app/dropdowns/simple-dropdown.js new file mode 100644 index 000000000..bba088ed1 --- /dev/null +++ b/ui/app/components/app/dropdowns/simple-dropdown.js @@ -0,0 +1,92 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const classnames = require('classnames') +const R = require('ramda') + +class SimpleDropdown extends Component { + constructor (props) { + super(props) + this.state = { + isOpen: false, + } + } + + getDisplayValue () { + const { selectedOption, options } = this.props + const matchesOption = option => option.value === selectedOption + const matchingOption = R.find(matchesOption)(options) + return matchingOption + ? matchingOption.displayValue || matchingOption.value + : selectedOption + } + + handleClose () { + this.setState({ isOpen: false }) + } + + toggleOpen () { + const { isOpen } = this.state + this.setState({ isOpen: !isOpen }) + } + + renderOptions () { + const { options, onSelect, selectedOption } = this.props + + return h('div', [ + h('div.simple-dropdown__close-area', { + onClick: event => { + event.stopPropagation() + this.handleClose() + }, + }), + h('div.simple-dropdown__options', [ + ...options.map(option => { + return h( + 'div.simple-dropdown__option', + { + className: classnames({ + 'simple-dropdown__option--selected': option.value === selectedOption, + }), + key: option.value, + onClick: () => { + if (option.value !== selectedOption) { + onSelect(option.value) + } + + this.handleClose() + }, + }, + option.displayValue || option.value, + ) + }), + ]), + ]) + } + + render () { + const { placeholder } = this.props + const { isOpen } = this.state + + return h( + 'div.simple-dropdown', + { + onClick: () => this.toggleOpen(), + }, + [ + h('div.simple-dropdown__selected', this.getDisplayValue() || placeholder || 'Select'), + h('i.fa.fa-caret-down.fa-lg.simple-dropdown__caret'), + isOpen && this.renderOptions(), + ] + ) + } +} + +SimpleDropdown.propTypes = { + options: PropTypes.array.isRequired, + placeholder: PropTypes.string, + onSelect: PropTypes.func, + selectedOption: PropTypes.string, +} + +module.exports = SimpleDropdown diff --git a/ui/app/components/app/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/tests/dropdown.test.js new file mode 100644 index 000000000..2b026589a --- /dev/null +++ b/ui/app/components/app/dropdowns/tests/dropdown.test.js @@ -0,0 +1,37 @@ +import React from 'react' +import assert from 'assert' +import sinon from 'sinon' +import { shallow } from 'enzyme' +import { DropdownMenuItem } from '../components/dropdown.js' + +describe('', () => { + let wrapper + const onClickSpy = sinon.spy() + const closeMenuSpy = sinon.spy() + + beforeEach(() => { + wrapper = shallow( + + + ) + }) + + it('renders li with dropdown-menu-item class', () => { + assert.equal(wrapper.find('li.dropdown-menu-item').length, 1) + }) + + it('adds style based on props passed', () => { + assert.equal(wrapper.prop('style').test, 'style') + }) + + it('simulates click event and calls onClick and closeMenu', () => { + wrapper.prop('onClick')() + assert.equal(onClickSpy.callCount, 1) + assert.equal(closeMenuSpy.callCount, 1) + }) + +}) diff --git a/ui/app/components/app/dropdowns/tests/menu.test.js b/ui/app/components/app/dropdowns/tests/menu.test.js new file mode 100644 index 000000000..9f5f13f00 --- /dev/null +++ b/ui/app/components/app/dropdowns/tests/menu.test.js @@ -0,0 +1,87 @@ +import React from 'react' +import assert from 'assert' +import sinon from 'sinon' +import { shallow } from 'enzyme' +import { Menu, Item, Divider, CloseArea } from '../components/menu' + +describe('Dropdown Menu Components', () => { + + describe('Menu', () => { + let wrapper + + beforeEach(() => { + wrapper = shallow( + + ) + }) + + it('adds prop className to menu', () => { + assert.equal(wrapper.find('.menu').prop('className'), 'menu Test Class') + }) + + }) + + describe('Item', () => { + let wrapper + + const onClickSpy = sinon.spy() + + beforeEach(() => { + wrapper = shallow( + + ) + }) + + it('add className based on props', () => { + assert.equal(wrapper.find('.menu__item').prop('className'), 'menu__item menu__item test className menu__item--clickable') + }) + + it('simulates onClick called', () => { + wrapper.find('.menu__item').prop('onClick')() + assert.equal(onClickSpy.callCount, 1) + }) + + it('adds icon based on icon props', () => { + assert.equal(wrapper.find('.menu__item__icon').text(), 'test icon') + }) + + it('adds html text based on text props', () => { + assert.equal(wrapper.find('.menu__item__text').text(), 'test text') + }) + }) + + describe('Divider', () => { + let wrapper + + before(() => { + wrapper = shallow() + }) + + it('renders menu divider', () => { + assert.equal(wrapper.find('.menu__divider').length, 1) + }) + }) + + describe('CloseArea', () => { + let wrapper + + const onClickSpy = sinon.spy() + + beforeEach(() => { + wrapper = shallow() + }) + + it('simulates click', () => { + wrapper.prop('onClick')() + assert.equal(onClickSpy.callCount, 1) + }) + }) + +}) diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js new file mode 100644 index 000000000..67b192c11 --- /dev/null +++ b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import NetworkDropdownIcon from '../components/network-dropdown-icon' + +describe('Network Dropdown Icon', () => { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + it('adds style props based on props', () => { + const styleProp = wrapper.find('.menu-icon-circle').children().prop('style') + assert.equal(styleProp.background, 'red') + assert.equal(styleProp.border, 'none') + assert.equal(styleProp.height, '12px') + assert.equal(styleProp.width, '12px') + }) +}) diff --git a/ui/app/components/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( + + ) + }) + + 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( + , + ) + }) + + 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 ( +
+ onChange(Number(event.target.value))} + /> +
+
onChange(value + 1)} + > + +
+
onChange(Math.max(value - 1, 0))} + > + +
+
+ { isInError + ?
+ { errorText } +
+ : null } +
+ ) + } + + infoButton (onClick) { + return + } + + renderGasEditRow (gasInputArgs) { + return ( +
+
+ { this.context.t(gasInputArgs.labelKey) } + { this.infoButton(() => gasInputArgs.infoOnClick()) } +
+ { this.gasInput(gasInputArgs) } +
+ ) + } + + render () { + const { + customGasPrice, + updateCustomGasPrice, + customGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + showGasPriceInfoModal, + showGasLimitInfoModal, + } = this.props + + return ( +
+ { 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, + }) } +
+ ) + } +} 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..7fbf8f0bd --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -0,0 +1,219 @@ +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, + } + + 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 ( +
+ onChange(Number(event.target.value))} + /> +
+
onChange(value + 1)} + > + +
+
onChange(Math.max(value - 1, 0))} + > + +
+
+ { isInError + ?
+ { errorText } +
+ : null } +
+ ) + } + + infoButton (onClick) { + return + } + + renderDataSummary (transactionFee, timeRemaining) { + return ( +
+
+ { this.context.t('newTransactionFee') } + ~{ this.context.t('transactionTime') } +
+
+
+ {transactionFee} +
+
{timeRemaining}
+
+
+ ) + } + + renderGasEditRow (gasInputArgs) { + return ( +
+
+ { this.context.t(gasInputArgs.labelKey) } + { this.infoButton(() => {}) } +
+ { this.gasInput(gasInputArgs) } +
+ ) + } + + renderGasEditRows ({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) { + return ( +
+ { this.renderGasEditRow({ + labelKey: 'gasPrice', + value: customGasPrice, + onChange: updateCustomGasPrice, + insufficientBalance, + customPriceIsSafe, + showGWEI: true, + isSpeedUp, + }) } + { this.renderGasEditRow({ + labelKey: 'gasLimit', + value: customGasLimit, + onChange: this.onChangeGasLimit, + insufficientBalance, + customPriceIsSafe, + }) } +
+ ) + } + + render () { + const { t } = this.context + const { + updateCustomGasPrice, + updateCustomGasLimit, + timeRemaining, + customGasPrice, + customGasLimit, + insufficientBalance, + gasChartProps, + gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, + transactionFee, + } = this.props + + return ( +
+ { this.renderDataSummary(transactionFee, timeRemaining) } +
+ { this.renderGasEditRows({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) } +
{ t('liveGasPricePredictions') }
+ {!gasEstimatesLoading + ? + : + } +
+ { t('slower') } + { t('faster') } +
+
+
+ ) + } +} 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..53cb84791 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -0,0 +1,203 @@ +@import './time-remaining/index'; + +.advanced-tab { + display: flex; + flex-flow: column; + + &__transaction-data-summary, + &__fee-chart-title { + padding-left: 24px; + padding-right: 24px; + } + + &__transaction-data-summary { + display: flex; + flex-flow: column; + color: $mid-gray; + margin-top: 12px; + padding-left: 18px; + padding-right: 18px; + + &__titles, + &__container { + display: flex; + flex-flow: row; + justify-content: space-between; + font-size: 12px; + color: #888EA3; + } + + &__container { + font-size: 16px; + margin-top: 0px; + } + + &__fee { + font-size: 16px; + color: #313A5E; + } + } + + &__fee-chart { + margin-top: 8px; + height: 265px; + background: #F8F9FB; + border-bottom: 1px solid #d2d8dd; + border-top: 1px solid #d2d8dd; + position: relative; + + &__title { + font-size: 12px; + color: #313A5E; + margin-left: 22px; + } + + &__speed-buttons { + position: absolute; + bottom: 13px; + display: flex; + justify-content: space-between; + padding-left: 20px; + padding-right: 19px; + width: 100%; + font-size: 10px; + color: #888EA3; + } + } + + &__slider-container { + padding-left: 27px; + padding-right: 27px; + } + + &__gas-edit-rows { + height: 73px; + display: flex; + flex-flow: row; + justify-content: space-between; + margin-left: 20px; + margin-right: 10px; + margin-top: 9px; + } + + &__gas-edit-row { + display: flex; + flex-flow: column; + + &__label { + color: #313B5E; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; + + .fa-info-circle { + color: $silver; + margin-left: 10px; + cursor: pointer; + } + + .fa-info-circle:hover { + color: $mid-gray; + } + } + + &__error-text { + font-size: 12px; + color: red; + } + + &__warning-text { + font-size: 12px; + color: orange; + } + + &__input-wrapper { + position: relative; + } + + &__input { + border: 1px solid $dusty-gray; + border-radius: 4px; + color: $mid-gray; + font-size: 16px; + height: 24px; + width: 155px; + padding-left: 8px; + padding-top: 2px; + margin-top: 7px; + } + + &__input--error { + border: 1px solid $red; + } + + &__input--warning { + border: 1px solid $orange; + } + + &__input-arrows { + position: absolute; + top: 7px; + right: 0px; + width: 17px; + height: 24px; + border: 1px solid #dadada; + border-top-right-radius: 4px; + display: flex; + flex-direction: column; + color: #9b9b9b; + font-size: .8em; + border-bottom-right-radius: 4px; + cursor: pointer; + + &__i-wrap { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + cursor: pointer; + } + + &__i-wrap:hover { + background: #4EADE7; + color: $white; + } + + i:hover { + background: #4EADE7; + } + + i { + font-size: 10px; + } + } + + &__input-arrows--error { + border: 1px solid $red; + } + + &__input-arrows--warning { + border: 1px solid $orange; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__gwei-symbol { + position: absolute; + top: 8px; + right: 10px; + color: $dusty-gray; + } + } +} \ No newline at end of file diff --git a/ui/app/components/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..a6b81d2ce --- /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,364 @@ +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(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.updateCustomGasPrice.resetHistory() + propsMethodSpies.updateCustomGasLimit.resetHistory() + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + AdvancedTabContent.prototype.gasInput.resetHistory() + AdvancedTabContent.prototype.renderGasEditRows.resetHistory() + AdvancedTabContent.prototype.renderDataSummary.resetHistory() + }) + + describe('render()', () => { + it('should render the advanced-tab root node', () => { + assert(wrapper.hasClass('advanced-tab')) + }) + + it('should render the expected four children of the advanced-tab div', () => { + const advancedTabChildren = wrapper.children() + assert.equal(advancedTabChildren.length, 2) + + assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) + assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) + + const feeChartDiv = advancedTabChildren.at(1) + + assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) + assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title')) + assert(feeChartDiv.childAt(2).is(GasPriceChart)) + assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons')) + }) + + it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => { + wrapper.setProps({ gasEstimatesLoading: true }) + const advancedTabChildren = wrapper.children() + assert.equal(advancedTabChildren.length, 2) + + assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) + assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) + + const feeChartDiv = advancedTabChildren.at(1) + + assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) + assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title')) + assert(feeChartDiv.childAt(2).is(Loading)) + assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons')) + }) + + it('should call renderDataSummary with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) + const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args + assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) + }) + + it('should call renderGasEditRows with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) + const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args + assert.deepEqual(renderGasEditRowArgs, [{ + customGasPrice: 11, + customGasLimit: 23456, + insufficientBalance: false, + customPriceIsSafe: true, + updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, + updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit, + isSpeedUp: false, + }]) + }) + }) + + describe('renderDataSummary()', () => { + let dataSummary + + beforeEach(() => { + dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining')) + }) + + it('should render the transaction-data-summary root node', () => { + assert(dataSummary.hasClass('advanced-tab__transaction-data-summary')) + }) + + it('should render titles of the data', () => { + const titlesNode = dataSummary.children().at(0) + assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles')) + assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee') + assert.equal(titlesNode.children().at(1).text(), '~transactionTime') + }) + + it('should render the data', () => { + const dataNode = dataSummary.children().at(1) + assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container')) + assert.equal(dataNode.children().at(0).text(), 'mockTotalFee') + assert(dataNode.children().at(1).hasClass('time-remaining')) + assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining') + }) + }) + + describe('renderGasEditRow()', () => { + let gasEditRow + + beforeEach(() => { + AdvancedTabContent.prototype.gasInput.resetHistory() + gasEditRow = shallow(wrapper.instance().renderGasEditRow({ + labelKey: 'mockLabelKey', + someArg: 'argA', + })) + }) + + it('should render the gas-edit-row root node', () => { + assert(gasEditRow.hasClass('advanced-tab__gas-edit-row')) + }) + + it('should render a label and an input', () => { + const gasEditRowChildren = gasEditRow.children() + assert.equal(gasEditRowChildren.length, 2) + assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label')) + assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper')) + }) + + it('should render the label key and info button', () => { + const gasRowLabelChildren = gasEditRow.children().at(0).children() + assert.equal(gasRowLabelChildren.length, 2) + assert(gasRowLabelChildren.at(0), 'mockLabelKey') + assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle')) + }) + + it('should call this.gasInput with the correct args', () => { + const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args + assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ]) + }) + }) + + describe('renderGasEditRows()', () => { + let gasEditRows + let tempOnChangeGasLimit + + beforeEach(() => { + tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit + wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit' + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasEditRows = shallow(wrapper.instance().renderGasEditRows( + 'mockGasPrice', + () => 'mockUpdateCustomGasPriceReturn', + 'mockGasLimit', + () => 'mockUpdateCustomGasLimitReturn', + false + )) + }) + + afterEach(() => { + wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit + }) + + it('should render the gas-edit-rows root node', () => { + assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows')) + }) + + it('should render two rows', () => { + const gasEditRowsChildren = gasEditRows.children() + assert.equal(gasEditRowsChildren.length, 2) + assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row')) + assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row')) + }) + + it('should call this.renderGasEditRow twice, with the expected args', () => { + const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args + assert.equal(renderGasEditRowSpyArgs.length, 2) + assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasLimit', + onChange: () => 'mockOnChangeGasLimit', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) + assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasPrice', + onChange: () => 'mockUpdateCustomGasPriceReturn', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) + }) + }) + + describe('infoButton()', () => { + let infoButton + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn')) + }) + + it('should render the i element', () => { + assert(infoButton.hasClass('fa-info-circle')) + }) + + it('should pass the onClick argument to the i tag onClick prop', () => { + assert(infoButton.props().onClick(), 'mockOnClickReturn') + }) + }) + + describe('gasInput()', () => { + let gasInput + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + AdvancedTabContent.prototype.gasInputError.resetHistory() + gasInput = shallow(wrapper.instance().gasInput({ + labelKey: 'gasPrice', + value: 321, + onChange: value => value + 7, + insufficientBalance: false, + showGWEI: true, + customPriceIsSafe: true, + isSpeedUp: false, + })) + }) + + it('should render the input-wrapper root node', () => { + assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) + }) + + it('should render two children, including an input', () => { + assert.equal(gasInput.children().length, 2) + assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) + }) + + it('should call the passed onChange method with the value of the input onChange event', () => { + const inputOnChange = gasInput.find('input').props().onChange + assert.equal(inputOnChange({ target: { value: 8} }), 15) + }) + + it('should have two input arrows', () => { + const upArrow = gasInput.find('.fa-angle-up') + assert.equal(upArrow.length, 1) + const downArrow = gasInput.find('.fa-angle-down') + assert.equal(downArrow.length, 1) + }) + + it('should call onChange with the value incremented decremented when its onchange method is called', () => { + const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0) + assert.equal(upArrow.props().onClick(), 329) + const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) + assert.equal(downArrow.props().onClick(), 327) + }) + + it('should call gasInputError with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1) + const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args + assert.deepEqual(gasInputErrorArgs, [{ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 321, + isSpeedUp: false, + }]) + }) + }) + + describe('gasInputError()', () => { + let gasInputError + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasInputError = wrapper.instance().gasInputError({ + labelKey: '', + insufficientBalance: false, + customPriceIsSafe: true, + isSpeedUp: false, + }) + }) + + it('should return an insufficientBalance error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: true, + customPriceIsSafe: true, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'insufficientBalance', + errorType: 'error', + }) + }) + + it('should return a zero gas on retry error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: true, + value: 0, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'zeroGasPriceOnSpeedUpError', + errorType: 'error', + }) + }) + + it('should return a low gas warning', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'gasPriceExtremelyLow', + errorType: 'warning', + }) + }) + + it('should return isInError false if there is no error', () => { + gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 1, + }) + assert.equal(gasInputError.isInError, false) + }) + }) + +}) diff --git a/ui/app/components/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() + }) + + 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 ( +
+ {minutes} + {this.context.t('minutesShorthand')} + {seconds} + {this.context.t('secondsShorthand')} +
+ ) + } +} 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 ( +
+
{ t('estimatedProcessingTimes') }
+
{ t('selectAHigherGasFee') }
+ {!gasPriceButtonGroupProps.loading + ? + : + } +
{ t('acceleratingATransaction') }
+
+ ) + } +} 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() + }) + + 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..76edbc334 --- /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 ( + + ) + } + + renderAdvancedTabContent ({ + convertThenUpdateCustomGasPrice, + convertThenUpdateCustomGasLimit, + customGasPrice, + customGasLimit, + newTotalFiat, + gasChartProps, + currentTimeEstimate, + insufficientBalance, + gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, + transactionFee, + }) { + return ( + + ) + } + + renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) { + return ( +
+
+
+ {this.context.t('sendAmount')} + {sendAmount} +
+
+ {this.context.t('transactionFee')} + {transactionFee} +
+
+ {this.context.t('newTotal')} + {newTotalEth} +
+
+ {newTotalFiat} +
+
+
+ ) + } + + 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 ( + + {tabsToRender.map(({ name, content }, i) => +
+ { content } + { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } +
+
+ )} +
+ ) + } + + render () { + const { + cancelAndClose, + infoRowProps, + onSubmit, + customModalGasPriceInHex, + customModalGasLimitInHex, + disableSave, + ...tabProps + } = this.props + + return ( +
+ cancelAndClose()} + onClose={() => cancelAndClose()} + onSubmit={() => { + onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) + }} + submitText={this.context.t('save')} + headerCloseText={'Close'} + hideCancel={true} + /> +
+ ) + } +} 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..cbc1e3e96 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -0,0 +1,291 @@ +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 { + getCurrentCurrency, + conversionRateSelector as getConversionRate, + getSelectedToken, + getCurrentEthBalance, +} 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' +import {getIsMainnet, preferencesSelector} from '../../../../selectors/selectors' + +const mapStateToProps = (state, ownProps) => { + const { transaction = {} } = ownProps + const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) + const gasEstimatesLoading = getGasEstimatesLoadingStatus(state) + + const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id) + const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice + const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit + const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) + + const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) + + const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex) + + const currentCurrency = getCurrentCurrency(state) + const conversionRate = getConversionRate(state) + + const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate) + + const hideBasic = state.appState.modal.modalState.props.hideBasic + + const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) + + const gasPrices = getEstimatedGasPrices(state) + const estimatedTimes = getEstimatedGasTimes(state) + const balance = getCurrentEthBalance(state) + + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + const showFiat = Boolean(isMainnet || showFiatInTestnets) + + const insufficientBalance = !isBalanceSufficient({ + amount: value, + gasTotal, + balance, + conversionRate, + }) + + return { + hideBasic, + isConfirm: isConfirm(state), + customModalGasPriceInHex, + customModalGasLimitInHex, + customGasPrice, + customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), + newTotalFiat, + currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), + blockTime: getBasicGasEstimateBlockTime(state), + customPriceIsSafe: isCustomPriceSafe(state), + gasPriceButtonGroupProps: { + buttonDataLoading, + defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), + gasButtonInfo, + }, + gasChartProps: { + currentPrice: customGasPrice, + gasPrices, + estimatedTimes, + gasPricesMax: gasPrices[gasPrices.length - 1], + estimatedTimesMax: estimatedTimes[0], + }, + infoRowProps: { + originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), + originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), + newTotalFiat: showFiat ? newTotalFiat : '', + newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), + transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), + sendAmount: addHexWEIsToRenderableEth(value, '0x0'), + }, + isSpeedUp: transaction.status === 'submitted', + txId: transaction.id, + insufficientBalance, + gasEstimatesLoading, + } +} + +const mapDispatchToProps = dispatch => { + const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice))) + + return { + cancelAndClose: () => { + dispatch(resetCustomData()) + dispatch(hideModal()) + }, + hideModal: () => dispatch(hideModal()), + updateCustomGasPrice, + convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), + convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), + setGasData: (newLimit, newPrice) => { + dispatch(setGasLimit(newLimit)) + dispatch(setGasPrice(newPrice)) + }, + updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { + updateCustomGasPrice(gasPrice) + dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16)))) + return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) + }, + createSpeedUpTransaction: (txId, gasPrice) => { + return dispatch(createSpeedUpTransaction(txId, gasPrice)) + }, + hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), + setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), + hideSidebar: () => dispatch(hideSidebar()), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps + const { + updateCustomGasPrice: dispatchUpdateCustomGasPrice, + hideGasButtonGroup: dispatchHideGasButtonGroup, + setGasData: dispatchSetGasData, + updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, + createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, + hideSidebar: dispatchHideSidebar, + cancelAndClose: dispatchCancelAndClose, + hideModal: dispatchHideModal, + ...otherDispatchProps + } = dispatchProps + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + onSubmit: (gasLimit, gasPrice) => { + if (isConfirm) { + dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice) + dispatchHideModal() + } else if (isSpeedUp) { + dispatchCreateSpeedUpTransaction(txId, gasPrice) + dispatchHideSidebar() + dispatchCancelAndClose() + } else { + dispatchSetGasData(gasLimit, gasPrice) + dispatchHideGasButtonGroup() + dispatchCancelAndClose() + } + }, + gasPriceButtonGroupProps: { + ...gasPriceButtonGroupProps, + handleGasPriceSelection: dispatchUpdateCustomGasPrice, + }, + cancelAndClose: () => { + dispatchCancelAndClose() + if (isSpeedUp) { + dispatchHideSidebar() + } + }, + disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0), + } +} + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer) + +function isConfirm (state) { + return Boolean(Object.keys(state.confirmTransaction.txData).length) +} + +function calcCustomGasPrice (customGasPriceInHex) { + return Number(hexWEIToDecGWEI(customGasPriceInHex)) +} + +function calcCustomGasLimit (customGasLimitInHex) { + return parseInt(customGasLimitInHex, 16) +} + +function getTxParams (state, transactionId) { + const { confirmTransaction: { txData }, metamask: { send } } = state + const pendingTransactions = submittedPendingTransactionsSelector(state) + const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId) + const { txParams: pendingTxParams } = pendingTransaction || {} + return txData.txParams || pendingTxParams || { + from: send.from, + gas: send.gasLimit || '0x5208', + gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), + to: send.to, + value: getSelectedToken(state) ? '0x0' : send.amount, + } +} + +function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { + return pipe( + addHexWEIsToDec, + formatETHFee + )(aHexWEI, bHexWEI) +} + +function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) { + return pipe( + addHexWEIsToDec, + partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), + partialRight(formatCurrency, [convertedCurrency]), + )(aHexWEI, bHexWEI) +} + +function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { + const minGasPrice = gasPrices[0] + const maxGasPrice = gasPrices[gasPrices.length - 1] + let priceForEstimation = currentGasPrice + if (currentGasPrice < minGasPrice) { + priceForEstimation = minGasPrice + } else if (currentGasPrice > maxGasPrice) { + priceForEstimation = maxGasPrice + } + + const { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue, + closestLowerValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) + + const newTimeEstimate = extrapolateY({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: priceForEstimation, + }) + + return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) +} diff --git a/ui/app/components/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( '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(, { 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..aaa4f1c41 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -0,0 +1,425 @@ +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, + } + const baseMockOwnProps = { transaction: { id: 34 } } + const tests = [ + { mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' }, + }), + expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }), + mockOwnProps: baseMockOwnProps, + }, + { + mockState: baseMockState, + mockOwnProps: Object.assign({}, baseMockOwnProps, { + transaction: { id: 34, status: 'submitted' }, + }), + expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }), + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: false, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'rinkeby', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: { + ...baseExpectedResult, + infoRowProps: { + ...baseExpectedResult.infoRowProps, + newTotalFiat: '', + }, + }, + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: true, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'rinkeby', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: baseExpectedResult, + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: true, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'mainnet', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: baseExpectedResult, + }, + ] + + let result + tests.forEach(({ mockState, mockOwnProps, expectedResult}) => { + result = mapStateToProps(mockState, mockOwnProps) + assert.deepEqual(result, expectedResult) + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + afterEach(() => { + actionSpies.hideModal.resetHistory() + gasActionSpies.setCustomGasPrice.resetHistory() + gasActionSpies.setCustomGasLimit.resetHistory() + }) + + describe('hideGasButtonGroup()', () => { + it('should dispatch a hideGasButtonGroup action', () => { + mapDispatchToPropsObject.hideGasButtonGroup() + assert(dispatchSpy.calledOnce) + assert(sendActionSpies.hideGasButtonGroup.calledOnce) + }) + }) + + describe('cancelAndClose()', () => { + it('should dispatch a hideModal action', () => { + mapDispatchToPropsObject.cancelAndClose() + assert(dispatchSpy.calledTwice) + assert(actionSpies.hideModal.calledOnce) + assert(gasActionSpies.resetCustomData.calledOnce) + }) + }) + + describe('updateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => { + mapDispatchToPropsObject.updateCustomGasPrice('ffff') + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasPrice.calledOnce) + assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff') + }) + }) + + describe('convertThenUpdateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => { + mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff') + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasPrice.calledOnce) + assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600') + }) + }) + + + describe('convertThenUpdateCustomGasLimit()', () => { + it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => { + mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16) + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasLimit.calledOnce) + assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10') + }) + }) + + describe('setGasData()', () => { + it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => { + mapDispatchToPropsObject.setGasData('ffff', 'aaaa') + assert(dispatchSpy.calledTwice) + assert(actionSpies.setGasPrice.calledOnce) + assert(actionSpies.setGasLimit.calledOnce) + assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff') + assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa') + }) + }) + + describe('updateConfirmTxGasAndCalculate()', () => { + it('should dispatch a updateGasAndCalculate action with the correct props', () => { + mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa') + assert.equal(dispatchSpy.callCount, 3) + assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce) + assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' }) + }) + }) + + }) + + describe('mergeProps', () => { + let stateProps + let dispatchProps + let ownProps + + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + isConfirm: true, + someOtherStateProp: 'baz', + } + dispatchProps = { + updateCustomGasPrice: sinon.spy(), + hideGasButtonGroup: sinon.spy(), + setGasData: sinon.spy(), + updateConfirmTxGasAndCalculate: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + createSpeedUpTransaction: sinon.spy(), + hideSidebar: sinon.spy(), + hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), + } + ownProps = { someOwnProp: 123 } + }) + + afterEach(() => { + dispatchProps.updateCustomGasPrice.resetHistory() + dispatchProps.hideGasButtonGroup.resetHistory() + dispatchProps.setGasData.resetHistory() + dispatchProps.updateConfirmTxGasAndCalculate.resetHistory() + dispatchProps.someOtherDispatchProp.resetHistory() + dispatchProps.createSpeedUpTransaction.resetHistory() + dispatchProps.hideSidebar.resetHistory() + dispatchProps.hideModal.resetHistory() + }) + it('should return the expected props when isConfirm is true', () => { + const result = mergeProps(stateProps, dispatchProps, ownProps) + + assert.equal(result.isConfirm, true) + assert.equal(result.someOtherStateProp, 'baz') + assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo') + assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar') + assert.equal(result.someOwnProp, 123) + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.hideModal.callCount, 0) + + result.onSubmit() + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.hideModal.callCount, 1) + + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) + + it('should return the expected props when isConfirm is false', () => { + const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps) + + assert.equal(result.isConfirm, false) + assert.equal(result.someOtherStateProp, 'baz') + assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo') + assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar') + assert.equal(result.someOwnProp, 123) + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.cancelAndClose.callCount, 0) + + result.onSubmit('mockNewLimit', 'mockNewPrice') + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 1) + assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice']) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1) + assert.equal(dispatchProps.cancelAndClose.callCount, 1) + + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) + + it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => { + const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps) + + result.onSubmit() + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.cancelAndClose.callCount, 1) + + assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1) + assert.equal(dispatchProps.hideSidebar.callCount, 1) + }) + }) + +}) diff --git a/ui/app/components/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 (
+ { labelKey &&
{ this.context.t(labelKey) }
} + { timeEstimate &&
{ timeEstimate }
} + { feeInPrimaryCurrency &&
{ feeInPrimaryCurrency }
} + { feeInSecondaryCurrency &&
{ feeInSecondaryCurrency }
} + { showCheck &&
} +
) + } + + renderButton ({ + priceInHexWei, + ...renderableGasInfo + }, { + buttonDataLoading, + handleGasPriceSelection, + ...buttonContentPropsAndFlags + }, index) { + return ( + + ) + } + + render () { + const { + gasButtonInfo, + defaultActiveButtonIndex = 1, + newActiveButtonIndex, + noButtonActiveByDefault = false, + buttonDataLoading, + ...buttonPropsAndFlags + } = this.props + + return ( + !buttonDataLoading + ? + { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } + + :
{ this.context.t('loading') }
+ ) + } +} 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() + }) + + 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 ( +
+
+
+ ) + } +} 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 = "" + "' + } + }) + return text + '
" + titleFormat(el.x) + '
' + "
" + }, + 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() + }) + + 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 ( +
+ onChange(event.target.value)} + /> +
+
+
+
+ {lowLabel} + {highLabel} +
+
+ ) + } +} 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/app/info-box/index.js b/ui/app/components/app/info-box/index.js new file mode 100644 index 000000000..6110422ed --- /dev/null +++ b/ui/app/components/app/info-box/index.js @@ -0,0 +1,2 @@ +import InfoBox from './info-box.component' +module.exports = InfoBox diff --git a/ui/app/components/app/info-box/index.scss b/ui/app/components/app/info-box/index.scss new file mode 100644 index 000000000..8b5626d79 --- /dev/null +++ b/ui/app/components/app/info-box/index.scss @@ -0,0 +1,24 @@ +.info-box { + border-radius: 4px; + background-color: $alabaster; + position: relative; + padding: 16px; + display: flex; + flex-flow: column; + color: $mid-gray; + + &__close::after { + content: '\00D7'; + font-size: 29px; + font-weight: 200; + color: $dusty-gray; + position: absolute; + right: 12px; + top: 0; + cursor: pointer; + } + + &__description { + font-size: .75rem; + } +} diff --git a/ui/app/components/app/info-box/info-box.component.js b/ui/app/components/app/info-box/info-box.component.js new file mode 100644 index 000000000..8688b8e8f --- /dev/null +++ b/ui/app/components/app/info-box/info-box.component.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class InfoBox extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + onClose: PropTypes.func, + title: PropTypes.string, + description: PropTypes.string, + } + + constructor (props) { + super(props) + + this.state = { + isShowing: true, + } + } + + handleClose () { + const { onClose } = this.props + + if (onClose) { + onClose() + } else { + this.setState({ isShowing: false }) + } + } + + render () { + const { title, description } = this.props + + return !this.state.isShowing + ? null + : ( +
+
this.handleClose()} + /> +
{ title }
+
{ description }
+
+ ) + } +} 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 { this.getConnectingLabel(this.props.loadingMessage) } + } + + renderLoadingScreenContent = () => { + return
+ + {this.renderMessage()} +
+ } + + renderErrorScreenContent = () => { + const { showNetworkDropdown, setProviderArgs, setProviderType } = this.props + + return
+ 😞 + { this.context.t('somethingWentWrong') } +
+ + + +
+
+ } + + 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 ( +
+
setProviderType(lastSelectedProvider || 'ropsten')} + /> +
+ { this.state.showErrorScreen + ? this.renderErrorScreenContent() + : this.renderLoadingScreenContent() + } +
+
+ ) + } +} 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/app/menu-bar/index.js b/ui/app/components/app/menu-bar/index.js new file mode 100644 index 000000000..c5760847f --- /dev/null +++ b/ui/app/components/app/menu-bar/index.js @@ -0,0 +1 @@ +export { default } from './menu-bar.container' diff --git a/ui/app/components/app/menu-bar/index.scss b/ui/app/components/app/menu-bar/index.scss new file mode 100644 index 000000000..f699f4090 --- /dev/null +++ b/ui/app/components/app/menu-bar/index.scss @@ -0,0 +1,23 @@ +.menu-bar { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex: 0 0 auto; + margin-bottom: 16px; + padding: 5px; + border-bottom: 1px solid #e5e5e5; + + &__sidebar-button { + font-size: 1.25rem; + cursor: pointer; + padding: 10px; + } + + &__open-in-browser { + cursor: pointer; + display: flex; + justify-content: center; + padding: 10px; + } +} diff --git a/ui/app/components/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 ( +
+ +
{ + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Hamburger', + }, + }) + sidebarOpen ? hideSidebar() : showSidebar() + }} + /> + + + + +
{ + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Account Options', + }, + }) + this.setState({ accountDetailsMenuOpen: true }) + }} + > +
+
+ + { + accountDetailsMenuOpen && ( + this.setState({ accountDetailsMenuOpen: false })} + /> + ) + } +
+ ) + } +} 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/app/menu-droppo.js b/ui/app/components/app/menu-droppo.js new file mode 100644 index 000000000..c80bee2be --- /dev/null +++ b/ui/app/components/app/menu-droppo.js @@ -0,0 +1,134 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + +module.exports = MenuDroppoComponent + + +inherits(MenuDroppoComponent, Component) +function MenuDroppoComponent () { + Component.call(this) +} + +MenuDroppoComponent.prototype.render = function () { + const { containerClassName = '' } = this.props + const speed = this.props.speed || '300ms' + const useCssTransition = this.props.useCssTransition + const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 + + this.manageListeners() + + const style = this.props.style || {} + if (!('position' in style)) { + style.position = 'fixed' + } + style.zIndex = zIndex + + return ( + h('div', { + style, + className: `.menu-droppo-container ${containerClassName}`, + }, [ + h('style', ` + .menu-droppo-enter { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + + .menu-droppo-enter.menu-droppo-enter-active { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave.menu-droppo-leave-active { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + `), + + useCssTransition + ? h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'menu-droppo', + transitionEnterTimeout: parseInt(speed), + transitionLeaveTimeout: parseInt(speed), + }, this.renderPrimary()) + : this.renderPrimary(), + ]) + ) +} + +MenuDroppoComponent.prototype.renderPrimary = function () { + const isOpen = this.props.isOpen + if (!isOpen) { + return null + } + + const innerStyle = this.props.innerStyle || {} + + return ( + h('.menu-droppo', { + key: 'menu-droppo-drawer', + style: innerStyle, + }, + [ this.props.children ]) + ) +} + +MenuDroppoComponent.prototype.manageListeners = function () { + const isOpen = this.props.isOpen + const onClickOutside = this.props.onClickOutside + + if (isOpen) { + this.outsideClickHandler = onClickOutside + } else if (isOpen) { + this.outsideClickHandler = null + } +} + +MenuDroppoComponent.prototype.componentDidMount = function () { + if (this && document.body) { + this.globalClickHandler = this.globalClickOccurred.bind(this) + document.body.addEventListener('click', this.globalClickHandler) + // eslint-disable-next-line react/no-find-dom-node + var container = findDOMNode(this) + this.container = container + } +} + +MenuDroppoComponent.prototype.componentWillUnmount = function () { + if (this && document.body) { + document.body.removeEventListener('click', this.globalClickHandler) + } +} + +MenuDroppoComponent.prototype.globalClickOccurred = function (event) { + const target = event.target + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + + if (target !== container && + !isDescendant(this.container, event.target) && + this.outsideClickHandler) { + this.outsideClickHandler(event) + } +} + +function isDescendant (parent, child) { + var node = child.parentNode + while (node !== null) { + if (node === parent) { + return true + } + node = node.parentNode + } + + return false +} diff --git a/ui/app/components/app/modal/index.js b/ui/app/components/app/modal/index.js new file mode 100644 index 000000000..58309abbe --- /dev/null +++ b/ui/app/components/app/modal/index.js @@ -0,0 +1,2 @@ +export { default } from './modal.component' +export { default as ModalContent } from './modal-content' diff --git a/ui/app/components/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/app/modal/modal-content/index.js b/ui/app/components/app/modal/modal-content/index.js new file mode 100644 index 000000000..733cfb3b8 --- /dev/null +++ b/ui/app/components/app/modal/modal-content/index.js @@ -0,0 +1 @@ +export { default } from './modal-content.component' diff --git a/ui/app/components/app/modal/modal-content/index.scss b/ui/app/components/app/modal/modal-content/index.scss new file mode 100644 index 000000000..560505b84 --- /dev/null +++ b/ui/app/components/app/modal/modal-content/index.scss @@ -0,0 +1,19 @@ +.modal-content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 0; + + &__title { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0; + text-align: center; + } + + &__description { + text-align: center; + font-size: .875rem; + } +} diff --git a/ui/app/components/app/modal/modal-content/modal-content.component.js b/ui/app/components/app/modal/modal-content/modal-content.component.js new file mode 100644 index 000000000..ecec0ee5b --- /dev/null +++ b/ui/app/components/app/modal/modal-content/modal-content.component.js @@ -0,0 +1,32 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export default class ModalContent extends PureComponent { + static propTypes = { + title: PropTypes.string, + description: PropTypes.string, + } + + render () { + const { title, description } = this.props + + return ( +
+ { + title && ( +
+ { title } +
+ ) + } + { + description && ( +
+ { description } +
+ ) + } +
+ ) + } +} diff --git a/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js new file mode 100644 index 000000000..17af09f45 --- /dev/null +++ b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js @@ -0,0 +1,44 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import ModalContent from '../modal-content.component' + +describe('ModalContent Component', () => { + it('should render a title', () => { + const wrapper = shallow( + + ) + + assert.equal(wrapper.find('.modal-content__title').length, 1) + assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title') + assert.equal(wrapper.find('.modal-content__description').length, 0) + }) + + it('should render a description', () => { + const wrapper = shallow( + + ) + + assert.equal(wrapper.find('.modal-content__title').length, 0) + assert.equal(wrapper.find('.modal-content__description').length, 1) + assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description') + }) + + it('should render both a title and a description', () => { + const wrapper = shallow( + + ) + + assert.equal(wrapper.find('.modal-content__title').length, 1) + assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title') + assert.equal(wrapper.find('.modal-content__description').length, 1) + assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description') + }) +}) diff --git a/ui/app/components/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 ( +
+ { + headerText && ( +
+
+ { headerText } +
+
+
+ ) + } +
+ { children } +
+
+ { + onCancel && ( + + ) + } + +
+
+ ) + } +} 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() + + 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( + + ) + + 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( + {}} + 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( + {}} + cancelText="Cancel" + onSubmit={() => {}} + submitText="Submit" + > +
+ + ) + + 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( + + ) + + 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( + + ) + + 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 ( +
+ + +
+ ) + } +} diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js new file mode 100644 index 000000000..1a9ae2e07 --- /dev/null +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js @@ -0,0 +1 @@ +export { default } from './cancel-transaction-gas-fee.component' diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss new file mode 100644 index 000000000..ce81dd448 --- /dev/null +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss @@ -0,0 +1,17 @@ +.cancel-transaction-gas-fee { + background: #F1F4F9; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + padding: 12px; + + &__eth { + font-size: 1.5rem; + font-weight: 500; + } + + &__fiat { + font-size: .75rem; + } +} diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..014815503 --- /dev/null +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js @@ -0,0 +1,26 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component' +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' + +describe('CancelTransactionGasFee Component', () => { + it('should render', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + const ethDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(0) + const fiatDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(1) + + assert.equal(ethDisplay.props().value, '0x3b9aca00') + assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth') + + assert.equal(fiatDisplay.props().value, '0x3b9aca00') + assert.equal(fiatDisplay.props().className, 'cancel-transaction-gas-fee__fiat') + }) +}) diff --git a/ui/app/components/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 ( + +
+
+ { t('cancellationGasFee') } +
+
+ +
+
+ { t('attemptToCancelDescription') } +
+
+
+ ) + } +} 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/app/modals/cancel-transaction/index.js b/ui/app/components/app/modals/cancel-transaction/index.js new file mode 100644 index 000000000..7abc871ee --- /dev/null +++ b/ui/app/components/app/modals/cancel-transaction/index.js @@ -0,0 +1 @@ +export { default } from './cancel-transaction.container' 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( + , + { 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( + {}} + />, + { 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/app/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js new file mode 100644 index 000000000..ceaa20a95 --- /dev/null +++ b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js @@ -0,0 +1,39 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal, { ModalContent } from '../../modal' + +export default class ClearApprovedOrigins extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + clearApprovedOrigins: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleClear = () => { + const { clearApprovedOrigins, hideModal } = this.props + clearApprovedOrigins() + hideModal() + } + + render () { + const { t } = this.context + + return ( + this.props.hideModal()} + submitText={t('ok')} + cancelText={t('nevermind')} + submitType="secondary" + > + + + ) + } +} 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/app/modals/clear-approved-origins/index.js b/ui/app/components/app/modals/clear-approved-origins/index.js new file mode 100644 index 000000000..b3e321995 --- /dev/null +++ b/ui/app/components/app/modals/clear-approved-origins/index.js @@ -0,0 +1 @@ +export { default } from './clear-approved-origins.container' 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 ( +
+
+ +
+
+ Name + {identity.name} +
+
+ Public Address + { addressSummary(identity.address, 4, 4) } +
+
+ + + +
+
+ ) + } + + render () { + const { t } = this.context + + return ( + +
+ { this.renderSelectedAccount() } +
+ { t('removeAccountDescription') } + + { t('learnMore') } + +
+
+
+ ) + } +} 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/app/modals/confirm-remove-account/index.js b/ui/app/components/app/modals/confirm-remove-account/index.js new file mode 100644 index 000000000..ecb5f7790 --- /dev/null +++ b/ui/app/components/app/modals/confirm-remove-account/index.js @@ -0,0 +1 @@ +export { default } from './confirm-remove-account.container' diff --git a/ui/app/components/app/modals/confirm-remove-account/index.scss b/ui/app/components/app/modals/confirm-remove-account/index.scss new file mode 100644 index 000000000..3be3a1967 --- /dev/null +++ b/ui/app/components/app/modals/confirm-remove-account/index.scss @@ -0,0 +1,58 @@ +.confirm-remove-account { + &__description { + text-align: center; + font-size: .875rem; + } + + &__account { + border: 1px solid #b7b7b7; + border-radius: 4px; + padding: 10px; + display: flex; + margin-top: 10px; + margin-bottom: 20px; + width: 100%; + + &__identicon { + margin-right: 10px; + } + + &__name, + &__address { + margin-right: 10px; + font-size: 14px; + } + + &__name { + width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__label { + font-size: 11px; + display: block; + color: #9b9b9b; + } + + &__link { + margin-top: 14px; + + img { + width: 15px; + height: 15px; + } + } + + @media screen and (max-width: 575px) { + &__name { + width: 90px; + } + } + } + + &__link { + color: #2f9ae0; + } +} \ No newline at end of file diff --git a/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js new file mode 100644 index 000000000..f1a4542ac --- /dev/null +++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js @@ -0,0 +1,38 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal, { ModalContent } from '../../modal' + +export default class ConfirmResetAccount extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + resetAccount: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleReset = () => { + this.props.resetAccount() + .then(() => this.props.hideModal()) + } + + render () { + const { t } = this.context + + return ( + this.props.hideModal()} + submitText={t('reset')} + cancelText={t('nevermind')} + submitType="secondary" + > + + + ) + } +} 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/app/modals/confirm-reset-account/index.js b/ui/app/components/app/modals/confirm-reset-account/index.js new file mode 100644 index 000000000..ca4d9c5bf --- /dev/null +++ b/ui/app/components/app/modals/confirm-reset-account/index.js @@ -0,0 +1 @@ +export { default } from './confirm-reset-account.container' 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 ( +
+
+
+
+ { this.context.t('customGas') } +
+
hideModal()} + /> +
+
+ this.setState({ gasPrice: value })} + title={t('gasPrice')} + copy={t('gasPriceCalculation')} + /> + this.setState({ gasLimit: value })} + title={t('gasLimit')} + copy={t('gasLimitCalculation')} + /> +
+
+ { !valid &&
{ t(errorKey) }
} +
this.handleRevert()} + > + { t('revert') } +
+
+ + +
+
+
+
+ ) + } +} 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/app/modals/customize-gas/index.js b/ui/app/components/app/modals/customize-gas/index.js new file mode 100644 index 000000000..3a0ab7edc --- /dev/null +++ b/ui/app/components/app/modals/customize-gas/index.js @@ -0,0 +1 @@ +export { default } from './customize-gas.container' diff --git a/ui/app/components/app/modals/customize-gas/index.scss b/ui/app/components/app/modals/customize-gas/index.scss new file mode 100644 index 000000000..e10452691 --- /dev/null +++ b/ui/app/components/app/modals/customize-gas/index.scss @@ -0,0 +1,110 @@ +.customize-gas { + border: 1px solid #D8D8D8; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); + font-family: Roboto; + display: flex; + flex-flow: column; + + @media screen and (max-width: $break-small) { + width: 100vw; + height: 100vh; + } + + &__header { + height: 52px; + border-bottom: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__title { + margin-left: 19.25px; + } + + &__close::after { + content: '\00D7'; + font-size: 1.8em; + color: $dusty-gray; + font-family: sans-serif; + cursor: pointer; + margin-right: 19.25px; + } + + &__content { + display: flex; + flex-flow: column nowrap; + height: 100%; + } + + &__body { + display: flex; + margin-bottom: 24px; + + @media screen and (max-width: $break-small) { + flex-flow: column; + flex: 1 1 auto; + } + } + + &__footer { + height: 75px; + border-top: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + position: relative; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__buttons { + display: flex; + justify-content: space-between; + margin-right: 21.25px; + } + + &__revert, &__cancel, &__save, &__save__error { + display: flex; + justify-content: center; + align-items: center; + padding: 0 3px; + cursor: pointer; + } + + &__revert { + color: $silver-chalice; + font-size: 16px; + margin-left: 21.25px; + } + + &__cancel, &__save, &__save__error { + width: 85.74px; + min-width: initial; + } + + &__save__error { + opacity: 0.5; + cursor: auto; + } + + &__error-message { + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + line-height: 12px; + color: $red; + } +} 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..72bb1df89 --- /dev/null +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -0,0 +1,220 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../../store/actions') +const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util') +const ShapeshiftForm = require('../shapeshift-form') + +import Button from '../../ui/button' + +let DIRECT_DEPOSIT_ROW_TITLE +let DIRECT_DEPOSIT_ROW_TEXT +let COINBASE_ROW_TITLE +let COINBASE_ROW_TEXT +let SHAPESHIFT_ROW_TITLE +let SHAPESHIFT_ROW_TEXT +let FAUCET_ROW_TITLE + +function mapStateToProps (state) { + return { + network: state.metamask.network, + address: state.metamask.selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + toCoinbase: (address) => { + dispatch(actions.buyEth({ network: '1', address, amount: 0 })) + }, + hideModal: () => { + dispatch(actions.hideModal()) + }, + hideWarning: () => { + dispatch(actions.hideWarning()) + }, + showAccountDetailModal: () => { + dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) + }, + toFaucet: network => dispatch(actions.buyEth({ network })), + } +} + +inherits(DepositEtherModal, Component) +function DepositEtherModal (props, context) { + Component.call(this) + + // need to set after i18n locale has loaded + DIRECT_DEPOSIT_ROW_TITLE = context.t('directDepositEther') + DIRECT_DEPOSIT_ROW_TEXT = context.t('directDepositEtherExplainer') + COINBASE_ROW_TITLE = context.t('buyCoinbase') + COINBASE_ROW_TEXT = context.t('buyCoinbaseExplainer') + SHAPESHIFT_ROW_TITLE = context.t('depositShapeShift') + SHAPESHIFT_ROW_TEXT = context.t('depositShapeShiftExplainer') + FAUCET_ROW_TITLE = context.t('testFaucet') + + this.state = { + buyingWithShapeshift: false, + } +} + +DepositEtherModal.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(DepositEtherModal) + + +DepositEtherModal.prototype.facuetRowText = function (networkName) { + return this.context.t('getEtherFromFaucet', [networkName]) +} + +DepositEtherModal.prototype.renderRow = function ({ + logo, + title, + text, + buttonLabel, + onButtonClick, + hide, + className, + hideButton, + hideTitle, + onBackClick, + showBackButton, +}) { + if (hide) { + return null + } + + return h('div', { + className: className || 'deposit-ether-modal__buy-row', + }, [ + + onBackClick && showBackButton && h('div.deposit-ether-modal__buy-row__back', { + onClick: onBackClick, + }, [ + + h('i.fa.fa-arrow-left.cursor-pointer'), + + ]), + + h('div.deposit-ether-modal__buy-row__logo-container', [logo]), + + h('div.deposit-ether-modal__buy-row__description', [ + + !hideTitle && h('div.deposit-ether-modal__buy-row__description__title', [title]), + + h('div.deposit-ether-modal__buy-row__description__text', [text]), + + ]), + + !hideButton && h('div.deposit-ether-modal__buy-row__button', [ + h(Button, { + type: 'primary', + className: 'deposit-ether-modal__deposit-button', + large: true, + onClick: onButtonClick, + }, [buttonLabel]), + ]), + + ]) +} + +DepositEtherModal.prototype.render = function () { + const { network, toCoinbase, address, toFaucet } = this.props + const { buyingWithShapeshift } = this.state + + const isTestNetwork = ['3', '4', '42'].find(n => n === network) + const networkName = getNetworkDisplayName(network) + + return h('div.page-container.page-container--full-width.page-container--full-height', {}, [ + + h('div.page-container__header', [ + + h('div.page-container__title', [this.context.t('depositEther')]), + + h('div.page-container__subtitle', [ + this.context.t('needEtherInWallet'), + ]), + + h('div.page-container__header-close', { + onClick: () => { + this.setState({ buyingWithShapeshift: false }) + this.props.hideWarning() + this.props.hideModal() + }, + }), + + ]), + + h('.page-container__content', {}, [ + + h('div.deposit-ether-modal__buy-rows', [ + + this.renderRow({ + logo: h('img.deposit-ether-modal__logo', { + src: './images/deposit-eth.svg', + }), + title: DIRECT_DEPOSIT_ROW_TITLE, + text: DIRECT_DEPOSIT_ROW_TEXT, + buttonLabel: this.context.t('viewAccount'), + onButtonClick: () => this.goToAccountDetailsModal(), + hide: buyingWithShapeshift, + }), + + this.renderRow({ + logo: h('i.fa.fa-tint.fa-2x'), + title: FAUCET_ROW_TITLE, + text: this.facuetRowText(networkName), + buttonLabel: this.context.t('getEther'), + onButtonClick: () => toFaucet(network), + hide: !isTestNetwork || buyingWithShapeshift, + }), + + this.renderRow({ + logo: h('div.deposit-ether-modal__logo', { + style: { + backgroundImage: 'url(\'./images/coinbase logo.png\')', + height: '40px', + }, + }), + title: COINBASE_ROW_TITLE, + text: COINBASE_ROW_TEXT, + buttonLabel: this.context.t('continueToCoinbase'), + onButtonClick: () => toCoinbase(address), + hide: isTestNetwork || buyingWithShapeshift, + }), + + this.renderRow({ + logo: h('div.deposit-ether-modal__logo', { + style: { + backgroundImage: 'url(\'./images/shapeshift logo.png\')', + }, + }), + title: SHAPESHIFT_ROW_TITLE, + text: SHAPESHIFT_ROW_TEXT, + buttonLabel: this.context.t('shapeshiftBuy'), + onButtonClick: () => this.setState({ buyingWithShapeshift: true }), + hide: isTestNetwork, + hideButton: buyingWithShapeshift, + hideTitle: buyingWithShapeshift, + onBackClick: () => this.setState({ buyingWithShapeshift: false }), + showBackButton: this.state.buyingWithShapeshift, + className: buyingWithShapeshift && 'deposit-ether-modal__buy-row__shapeshift-buy', + }), + + buyingWithShapeshift && h(ShapeshiftForm), + + ]), + + ]), + ]) +} + +DepositEtherModal.prototype.goToAccountDetailsModal = function () { + this.props.hideWarning() + this.props.hideModal() + this.props.showAccountDetailModal() +} diff --git a/ui/app/components/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/app/modals/index.js b/ui/app/components/app/modals/index.js new file mode 100644 index 000000000..1db1d33d4 --- /dev/null +++ b/ui/app/components/app/modals/index.js @@ -0,0 +1,5 @@ +const Modal = require('./modal') + +module.exports = { + Modal, +} 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 ( + hideModal()} + submitText={t('tryAgain')} + > + + + ) +} + +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 ( +
+
+
+
+ + +
+
+ +
+
Help Us Improve MetaMask
+
+
+ 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. +
+
+ MetaMask will.. +
+ +
+
+ +
+ Always allow you to opt-out via Settings +
+
+
+ +
+ Send anonymized click & pageview events +
+
+
+ +
+ Maintain a public aggregate dashboard to educate the community +
+
+
+ +
+ Never collect keys, addresses, transactions, balances, hashes, or any personal information +
+
+
+ +
+ Never collect your full IP address +
+
+
+ +
+ Never sell data for profit. Ever! +
+
+
+
+
+ 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 + Privacy Policy here + . +
+
+
+ { + 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} + /> +
+
+
+ ) + } +} 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/app/modals/qr-scanner/index.js b/ui/app/components/app/modals/qr-scanner/index.js new file mode 100644 index 000000000..470dee1f4 --- /dev/null +++ b/ui/app/components/app/modals/qr-scanner/index.js @@ -0,0 +1,2 @@ +import QrScanner from './qr-scanner.container' +module.exports = QrScanner diff --git a/ui/app/components/app/modals/qr-scanner/index.scss b/ui/app/components/app/modals/qr-scanner/index.scss new file mode 100644 index 000000000..6fa81d51f --- /dev/null +++ b/ui/app/components/app/modals/qr-scanner/index.scss @@ -0,0 +1,83 @@ +.qr-scanner { + width: 100%; + height: 100%; + background-color: #fff; + display: flex; + flex-flow: column; + border-radius: 8px; + + &__title { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0; + text-align: center; + } + + &__content { + padding-left: 20px; + padding-right: 20px; + + &__video-wrapper { + overflow: hidden; + width: 100%; + height: 275px; + display: flex; + align-items: center; + justify-content: center; + + video { + transform: scaleX(-1); + width: auto; + height: 275px; + } + } + } + + &__status { + text-align: center; + font-size: 14px; + padding: 15px; + } + + &__image { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0 0; + text-align: center; + } + + &__error { + text-align: center; + font-size: 16px; + padding: 15px; + } + + &__footer { + padding: 20px; + flex-direction: row; + display: flex; + + button { + margin-right: 15px; + } + + button:last-of-type { + margin-right: 0; + background-color: #009eec; + border: none; + color: #fff; + } + } + + &__close::after { + content: '\00D7'; + font-size: 35px; + color: #9b9b9b; + position: absolute; + top: 4px; + right: 20px; + cursor: pointer; + font-weight: 300; + } +} + 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 ( +
+
+ ) + } + + 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 ( +
+
+ +
+ +
+
+ { title } +
+
+ {msg} +
+ +
+ ) + } + + render () { + const { t } = this.context + + if (this.props.error) { + return this.renderErrorModal() + } + + return ( +
+
+
+ { `${t('scanQrCode')}` } +
+
+ { this.renderVideo() } +
+
+ {this.state.msg} +
+
+ ) + } +} 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/app/modals/reject-transactions/index.js b/ui/app/components/app/modals/reject-transactions/index.js new file mode 100644 index 000000000..fcdc372b6 --- /dev/null +++ b/ui/app/components/app/modals/reject-transactions/index.js @@ -0,0 +1 @@ +export { default } from './reject-transactions.container' diff --git a/ui/app/components/app/modals/reject-transactions/index.scss b/ui/app/components/app/modals/reject-transactions/index.scss new file mode 100644 index 000000000..753466883 --- /dev/null +++ b/ui/app/components/app/modals/reject-transactions/index.scss @@ -0,0 +1,6 @@ +.reject-transactions { + &__description { + text-align: center; + font-size: .875rem; + } +} diff --git a/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js new file mode 100644 index 000000000..60b259bdc --- /dev/null +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types' +import React, { PureComponent } from 'react' +import Modal from '../../modal' + +export default class RejectTransactionsModal extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static propTypes = { + onSubmit: PropTypes.func.isRequired, + hideModal: PropTypes.func.isRequired, + unapprovedTxCount: PropTypes.number.isRequired, + } + + onSubmit = async () => { + const { onSubmit, hideModal } = this.props + + await onSubmit() + hideModal() + } + + render () { + const { t } = this.context + const { hideModal, unapprovedTxCount } = this.props + + return ( + +
+
+ { t('rejectTxsDescription', [unapprovedTxCount]) } +
+
+
+ ) + } +} 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/app/modals/transaction-confirmed/index.js b/ui/app/components/app/modals/transaction-confirmed/index.js new file mode 100644 index 000000000..7776b969e --- /dev/null +++ b/ui/app/components/app/modals/transaction-confirmed/index.js @@ -0,0 +1 @@ +export { default } from './transaction-confirmed.container' diff --git a/ui/app/components/app/modals/transaction-confirmed/index.scss b/ui/app/components/app/modals/transaction-confirmed/index.scss new file mode 100644 index 000000000..c97371fb6 --- /dev/null +++ b/ui/app/components/app/modals/transaction-confirmed/index.scss @@ -0,0 +1,22 @@ +.transaction-confirmed { + &__title { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0; + text-align: center; + } + + &__description { + text-align: center; + font-size: .875rem; + } + + &__content { + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + } +} diff --git a/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js new file mode 100644 index 000000000..0a98eb1a1 --- /dev/null +++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal from '../../modal' + +export default class TransactionConfirmed extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + onSubmit: PropTypes.func, + hideModal: PropTypes.func, + } + + handleSubmit = () => { + const { hideModal, onSubmit } = this.props + + hideModal() + + if (onSubmit && typeof onSubmit === 'function') { + onSubmit() + } + } + + render () { + const { t } = this.context + + return ( + +
+ +
+ { `${t('confirmed')}!` } +
+
+ { t('initialTransactionConfirmed') } +
+
+
+ ) + } +} 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/app/network-display/index.js b/ui/app/components/app/network-display/index.js new file mode 100644 index 000000000..f6878ae5b --- /dev/null +++ b/ui/app/components/app/network-display/index.js @@ -0,0 +1,2 @@ +import NetworkDisplay from './network-display.container' +module.exports = NetworkDisplay diff --git a/ui/app/components/app/network-display/index.scss b/ui/app/components/app/network-display/index.scss new file mode 100644 index 000000000..e9f2f2057 --- /dev/null +++ b/ui/app/components/app/network-display/index.scss @@ -0,0 +1,57 @@ +.network-display { + &__container { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 10px; + border-radius: 4px; + height: 25px; + + &--colored { + background-color: lighten(rgb(125, 128, 130), 45%); + } + + &--mainnet { + background-color: lighten($blue-lagoon, 68%); + } + + &--ropsten { + background-color: lighten($crimson, 45%); + } + + &--kovan { + background-color: lighten($purple, 65%); + } + + &--rinkeby { + background-color: lighten($tulip-tree, 35%); + } + } + + &__name { + font-size: .75rem; + padding-left: 5px; + } + + &__icon { + height: 10px; + width: 10px; + border-radius: 10px; + + &--mainnet { + background-color: $blue-lagoon; + } + + &--ropsten { + background-color: $crimson; + } + + &--kovan { + background-color: $purple; + } + + &--rinkeby { + background-color: $tulip-tree; + } + } +} 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 + ?
+ :
+ } + + render () { + const { colored, network, provider: { type, nickname } } = this.props + const networkClass = networkToClassHash[network] + + return ( +
+ { + networkClass + ?
+ :
+ } +
+ { type === 'rpc' && nickname ? nickname : this.context.t(type) } +
+
+ ) + } +} diff --git a/ui/app/components/app/network-display/network-display.container.js b/ui/app/components/app/network-display/network-display.container.js new file mode 100644 index 000000000..99a14fff4 --- /dev/null +++ b/ui/app/components/app/network-display/network-display.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import NetworkDisplay from './network-display.component' + +const mapStateToProps = ({ metamask: { network, provider } }) => { + return { + network, + provider, + } +} + +export default connect(mapStateToProps)(NetworkDisplay) 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/app/notice.js b/ui/app/components/app/notice.js new file mode 100644 index 000000000..bb7e0814c --- /dev/null +++ b/ui/app/components/app/notice.js @@ -0,0 +1,138 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode +const connect = require('react-redux').connect + +Notice.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect()(Notice) + + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + 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, + onClick: () => { + this.setState({disclaimerDisabled: true}, () => onConfirm()) + }, + style: { + marginTop: '18px', + }, + }, this.context.t('accept')), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + // 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}) + } +} + +Notice.prototype.componentWillUnmount = function () { + // eslint-disable-next-line react/no-find-dom-node + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/app/components/app/provider-page-container/index.js b/ui/app/components/app/provider-page-container/index.js new file mode 100644 index 000000000..927c35940 --- /dev/null +++ b/ui/app/components/app/provider-page-container/index.js @@ -0,0 +1,3 @@ +export {default} from './provider-page-container.component' +export {default as ProviderPageContainerContent} from './provider-page-container-content' +export {default as ProviderPageContainerHeader} from './provider-page-container-header' diff --git a/ui/app/components/app/provider-page-container/index.scss b/ui/app/components/app/provider-page-container/index.scss new file mode 100644 index 000000000..8d35ac179 --- /dev/null +++ b/ui/app/components/app/provider-page-container/index.scss @@ -0,0 +1,121 @@ +.provider-approval-container { + display: flex; + + &__header { + display: flex; + flex-direction: column; + align-items: flex-end; + border-bottom: 1px solid $geyser; + padding: 9px; + } + + &__content { + display: flex; + overflow-y: auto; + flex: 1; + flex-direction: column; + justify-content: space-between; + color: #7C808E; + + h1, h2 { + color: #4A4A4A; + display: flex; + justify-content: center; + text-align: center; + } + + h2 { + font-size: 16px; + line-height: 18px; + padding: 20px; + } + + h1 { + font-size: 22px; + line-height: 26px; + padding: 20px; + } + + p { + padding: 0 40px; + text-align: center; + font-size: 12px; + line-height: 18px; + } + + a, a:hover { + color: $dodger-blue; + } + + .provider-approval-visual { + display: flex; + flex-direction: row; + justify-content: space-evenly; + position: relative; + margin: 0 32px; + + section { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + } + + h1 { + font-size: 14px; + line-height: 18px; + padding: 8px 0 0; + } + + h2 { + font-size: 10px; + line-height: 14px; + padding: 0; + color: #A2A4AC; + } + + &__check { + width: 40px; + height: 40px; + background: white url("/images/provider-approval-check.svg") no-repeat; + margin-top: 14px; + } + + &__identicon { + width: 64px; + height: 64px; + + &--default { + background-color: #777A87; + color: white; + width: 64px; + height: 64px; + border-radius: 32px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } + } + + &:before { + border-top: 2px dashed #CDD1E4; + content: ""; + margin: 0 auto; + position: absolute; + top: 32px; + left: 0; + bottom: 0; + right: 0; + width: 65%; + z-index: -1; + } + } + + .secure-badge { + display: flex; + justify-content: center; + padding: 25px; + } + } +} diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/index.js b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js new file mode 100644 index 000000000..73e491adc --- /dev/null +++ b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js @@ -0,0 +1 @@ +export {default} from './provider-page-container-content.container' 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 ( +
+
+ {siteImage ? ( + + ) : ( + + {siteTitle.charAt(0).toUpperCase()} + + )} +

{siteTitle}

+

{origin}

+
+ +
+ +

{selectedIdentity.name}

+
+
+ ) + } + + render () { + const { siteTitle } = this.props + const { t } = this.context + + return ( +
+
+

{t('connectRequest')}

+ {this.renderConnectVisual()} +

{t('providerRequest', [siteTitle])}

+

+ {t('providerRequestInfo')} +
+ + {t('learnMore')}. + +

+
+
+ +
+
+ ) + } +} 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/app/provider-page-container/provider-page-container-header/index.js b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js new file mode 100644 index 000000000..430627d3a --- /dev/null +++ b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js @@ -0,0 +1 @@ +export {default} from './provider-page-container-header.component' diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..41bf6c3dd --- /dev/null +++ b/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js @@ -0,0 +1,12 @@ +import React, {PureComponent} from 'react' +import NetworkDisplay from '../../network-display' + +export default class ProviderPageContainerHeader extends PureComponent { + render () { + return ( +
+ +
+ ) + } +} 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 ( +
+ + + this.onCancel()} + cancelText={this.context.t('cancel')} + onSubmit={() => this.onSubmit()} + submitText={this.context.t('connect')} + submitButtonType="confirm" + /> +
+ ) + } +} diff --git a/ui/app/components/app/selected-account/index.js b/ui/app/components/app/selected-account/index.js new file mode 100644 index 000000000..eb342181f --- /dev/null +++ b/ui/app/components/app/selected-account/index.js @@ -0,0 +1,2 @@ +import SelectedAccount from './selected-account.container' +module.exports = SelectedAccount diff --git a/ui/app/components/app/selected-account/index.scss b/ui/app/components/app/selected-account/index.scss new file mode 100644 index 000000000..5339a228b --- /dev/null +++ b/ui/app/components/app/selected-account/index.scss @@ -0,0 +1,38 @@ +.selected-account { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 1; + + &__name { + max-width: 200px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-align: center; + } + + &__address { + font-size: .75rem; + color: $silver-chalice; + } + + &__clickable { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5px 15px; + border-radius: 10px; + cursor: pointer; + + &:hover { + background-color: #e8e6e8; + } + + &:active { + background-color: #d9d7da; + } + } +} 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 ( +
+ +
{ + this.setState({ copied: true }) + setTimeout(() => this.setState({ copied: false }), 3000) + copyToClipboard(checksummedAddress) + }} + > +
+ { selectedIdentity.name } +
+
+ { addressSlicer(checksummedAddress) } +
+
+
+
+ ) + } +} + +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/app/selected-account/tests/selected-account-component.test.js b/ui/app/components/app/selected-account/tests/selected-account-component.test.js new file mode 100644 index 000000000..78a94d1c8 --- /dev/null +++ b/ui/app/components/app/selected-account/tests/selected-account-component.test.js @@ -0,0 +1,16 @@ +import React from 'react' +import assert from 'assert' +import { render } from 'enzyme' +import SelectedAccount from '../selected-account.component' + +describe('SelectedAccount Component', () => { + it('should render checksummed address', () => { + const wrapper = render(, { context: { t: () => {}}}) + // Checksummed version of address is displayed + assert.equal(wrapper.find('.selected-account__address').text(), '0x1B82...5C9D') + assert.equal(wrapper.find('.selected-account__name').text(), 'testName') + }) +}) diff --git a/ui/app/components/app/send/README.md b/ui/app/components/app/send/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/app/send/account-list-item/account-list-item-README.md b/ui/app/components/app/send/account-list-item/account-list-item-README.md new file mode 100644 index 000000000..e69de29bb 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 (
handleClick && handleClick({ name, address, balance })} + > + +
+ + +
{ name || address }
+ + {icon &&
{ icon }
} + +
+ + {displayAddress && name &&
+ { checksumAddress(address) } +
} + + { + displayBalance && ( + +
+
+ + { + balanceIsCached ? * : null + } +
+ { + showFiat && ( + + ) + } +
+
+ ) + } + +
) + } +} 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/app/send/account-list-item/index.js b/ui/app/components/app/send/account-list-item/index.js new file mode 100644 index 000000000..907485cf7 --- /dev/null +++ b/ui/app/components/app/send/account-list-item/index.js @@ -0,0 +1 @@ +export { default } from './account-list-item.container' 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(} + />, { 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/app/send/index.js b/ui/app/components/app/send/index.js new file mode 100644 index 000000000..b5114babc --- /dev/null +++ b/ui/app/components/app/send/index.js @@ -0,0 +1 @@ +export { default } from './send.container' diff --git a/ui/app/components/app/send/send-content/index.js b/ui/app/components/app/send/send-content/index.js new file mode 100644 index 000000000..891c17e6a --- /dev/null +++ b/ui/app/components/app/send/send-content/index.js @@ -0,0 +1 @@ +export { default } from './send-content.component' diff --git a/ui/app/components/app/send/send-content/send-amount-row/README.md b/ui/app/components/app/send/send-content/send-amount-row/README.md new file mode 100644 index 000000000..e69de29bb 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 + : ( +
+ + {this.context.t('max')} + +
+ ) + } + +} 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/app/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 new file mode 100644 index 000000000..69fec1994 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getMaxModeOn, +} + +module.exports = selectors + +function getMaxModeOn (state) { + return state.metamask.send.maxModeOn +} 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/app/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 new file mode 100644 index 000000000..ee8271494 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js @@ -0,0 +1 @@ +export { default } from './amount-max-button.container' diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..b04d3897f --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -0,0 +1,89 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import AmountMaxButton from '../amount-max-button.component.js' + +const propsMethodSpies = { + setAmountToMax: sinon.spy(), + setMaxModeTo: sinon.spy(), +} + +const MOCK_EVENT = { preventDefault: () => {} } + +sinon.spy(AmountMaxButton.prototype, 'setMaxAmount') + +describe('AmountMaxButton Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.setAmountToMax.resetHistory() + propsMethodSpies.setMaxModeTo.resetHistory() + AmountMaxButton.prototype.setMaxAmount.resetHistory() + }) + + describe('setMaxAmount', () => { + + it('should call setAmountToMax with the correct params', () => { + assert.equal(propsMethodSpies.setAmountToMax.callCount, 0) + instance.setMaxAmount() + assert.equal(propsMethodSpies.setAmountToMax.callCount, 1) + assert.deepEqual( + propsMethodSpies.setAmountToMax.getCall(0).args, + [{ + balance: 'mockBalance', + gasTotal: 'mockGasTotal', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + }) + + describe('render', () => { + it('should render an element with a send-v2__amount-max class', () => { + assert(wrapper.exists('.send-v2__amount-max')) + }) + + it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { + const { + onClick, + } = wrapper.find('.send-v2__amount-max').props() + + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) + onClick(MOCK_EVENT) + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.setMaxModeTo.getCall(0).args, + [true] + ) + }) + + it('should not render anything when maxModeOn is true', () => { + wrapper.setProps({ maxModeOn: true }) + assert.ok(!wrapper.exists('.send-v2__amount-max')) + }) + + it('should render the expected text when maxModeOn is false', () => { + wrapper.setProps({ maxModeOn: false }) + assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t') + }) + }) +}) 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/app/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 new file mode 100644 index 000000000..655fe1969 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js @@ -0,0 +1,22 @@ +import assert from 'assert' +import { + getMaxModeOn, +} from '../amount-max-button.selectors.js' + +describe('amount-max-button selectors', () => { + + describe('getMaxModeOn()', () => { + it('should', () => { + const state = { + metamask: { + send: { + maxModeOn: null, + }, + }, + } + + assert.equal(getMaxModeOn(state), null) + }) + }) + +}) 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/app/send/send-content/send-amount-row/index.js b/ui/app/components/app/send/send-content/send-amount-row/index.js new file mode 100644 index 000000000..abc6852fe --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/index.js @@ -0,0 +1 @@ +export { default } from './send-amount-row.container' 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 ( + this.validateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + error={inError} + value={amount} + /> + ) + } + + render () { + const { gasTotal, inError } = this.props + + return ( + + {!inError && gasTotal && } + { this.renderInput() } + + ) + } + +} 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/app/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 new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..fb08c7ed7 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + sendAmountIsInError, +} + +module.exports = selectors + +function sendAmountIsInError (state) { + return Boolean(state.send.errors.amount) +} 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(, { 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/app/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 new file mode 100644 index 000000000..4672cb8a7 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js @@ -0,0 +1,34 @@ +import assert from 'assert' +import { + sendAmountIsInError, +} from '../send-amount-row.selectors.js' + +describe('send-amount-row selectors', () => { + + describe('sendAmountIsInError()', () => { + it('should return true if send.errors.amount is truthy', () => { + const state = { + send: { + errors: { + amount: 'abc', + }, + }, + } + + assert.equal(sendAmountIsInError(state), true) + }) + + it('should return false if send.errors.amount is falsy', () => { + const state = { + send: { + errors: { + amount: null, + }, + }, + } + + assert.equal(sendAmountIsInError(state), false) + }) + }) + +}) 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 ( + +
+ + this.props.scanQrCode()} + /> + + + {(this.props.showHexData && ( + + ))} +
+
+ ) + } + +} diff --git a/ui/app/components/app/send/send-content/send-dropdown-list/index.js b/ui/app/components/app/send/send-content/send-dropdown-list/index.js new file mode 100644 index 000000000..04af6536c --- /dev/null +++ b/ui/app/components/app/send/send-content/send-dropdown-list/index.js @@ -0,0 +1 @@ +export { default } from './send-dropdown-list.component' 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 + ? + : null + } + + render () { + const { + accounts, + closeDropdown, + onSelect, + activeAddress, + } = this.props + + return (
+
closeDropdown()} + /> +
+ {accounts.map((account, index) => { + onSelect(account) + closeDropdown() + }} + icon={this.getListItemIcon(account.address, activeAddress)} + key={`send-dropdown-account-#${index}`} + />)} +
+
) + } + +} diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..b92dd4dfe --- /dev/null +++ b/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js @@ -0,0 +1,105 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendDropdownList from '../send-dropdown-list.component.js' + +import AccountListItem from '../../../account-list-item/account-list-item.container' + +const propsMethodSpies = { + closeDropdown: sinon.spy(), + onSelect: sinon.spy(), +} + +sinon.spy(SendDropdownList.prototype, 'getListItemIcon') + +describe('SendDropdownList Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.closeDropdown.resetHistory() + propsMethodSpies.onSelect.resetHistory() + SendDropdownList.prototype.getListItemIcon.resetHistory() + }) + + describe('getListItemIcon', () => { + it('should return check icon if the passed addresses are the same', () => { + assert.deepEqual( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'), + + ) + }) + + it('should return null if the passed addresses are different', () => { + assert.equal( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'), + null + ) + }) + }) + + describe('render', () => { + it('should render a single div with two children', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.children().length, 2) + }) + + it('should render the children with the correct classes', () => { + assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area')) + assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list')) + }) + + it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => { + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + wrapper.childAt(0).props().onClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + }) + + it('should render an AccountListItem for each item in accounts', () => { + assert.equal(wrapper.childAt(1).children().length, 3) + assert(wrapper.childAt(1).children().every(AccountListItem)) + }) + + it('should pass the correct props to the AccountListItem', () => { + wrapper.childAt(1).children().forEach((accountListItem, index) => { + const { + account, + className, + handleClick, + } = accountListItem.props() + assert.deepEqual(account, { address: 'mockAccount' + index }) + assert.equal(className, 'account-list-item__dropdown') + assert.equal(propsMethodSpies.onSelect.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.onSelect.callCount, 1) + assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index }) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + }) + }) + + it('should call this.getListItemIcon for each AccountListItem', () => { + assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3) + const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls() + assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index)) + }) + }) +}) diff --git a/ui/app/components/app/send/send-content/send-from-row/index.js b/ui/app/components/app/send/send-content/send-from-row/index.js new file mode 100644 index 000000000..0a79726b2 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-from-row/index.js @@ -0,0 +1 @@ +export { default } from './send-from-row.container' 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 ( + +
+ +
+
+ ) + } +} 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/app/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 new file mode 100644 index 000000000..03ef4806b --- /dev/null +++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getFromDropdownOpen, +} + +module.exports = selectors + +function getFromDropdownOpen (state) { + return state.send.fromDropdownOpen +} 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( + , + { 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/app/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 new file mode 100644 index 000000000..ecb57bbc3 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js @@ -0,0 +1,20 @@ +import assert from 'assert' +import { + getFromDropdownOpen, +} from '../send-from-row.selectors.js' + +describe('send-from-row selectors', () => { + + describe('getFromDropdownOpen()', () => { + it('should get send.fromDropdownOpen', () => { + const state = { + send: { + fromDropdownOpen: null, + }, + } + + assert.equal(getFromDropdownOpen(state), null) + }) + }) + +}) diff --git a/ui/app/components/app/send/send-content/send-gas-row/README.md b/ui/app/components/app/send/send-content/send-gas-row/README.md new file mode 100644 index 000000000..e69de29bb 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 ( +
+ {gasTotal + ? ( +
+ + +
+ ) + : gasLoadingError + ?
+ {this.context.t('setGasPrice')} +
+ :
+ {this.context.t('loading')} +
+ } + +
+ ) + } +} diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..dba0edb7b --- /dev/null +++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js @@ -0,0 +1 @@ +export { default } from './gas-fee-display.component' 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(, {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/app/send/send-content/send-gas-row/index.js b/ui/app/components/app/send/send-content/send-gas-row/index.js new file mode 100644 index 000000000..3c7ff1d5f --- /dev/null +++ b/ui/app/components/app/send/send-content/send-gas-row/index.js @@ -0,0 +1 @@ +export { default } from './send-gas-row.container' 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
{ + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Clicked "Advanced Options"', + }, + }) + showCustomizeGasModal() + }}> + { this.context.t('advancedOptions') } +
+ } + + renderContent () { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + showCustomizeGasModal, + gasPriceButtonGroupProps, + gasButtonGroupShown, + advancedInlineGasShown, + resetGasButtons, + setGasPrice, + setGasLimit, + gasPrice, + gasLimit, + insufficientBalance, + } = this.props + const { metricsEvent } = this.context + + const gasPriceButtonGroup =
+ { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Changed Gas Button', + }, + }) + gasPriceButtonGroupProps.handleGasPriceSelection(...args) + }} + /> + { this.renderAdvancedOptionsButton() } +
+ const gasFeeDisplay = showCustomizeGasModal()} + /> + const advancedGasInputs =
+ setGasPrice(newGasPrice, gasLimit)} + updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)} + customGasPrice={gasPrice} + customGasLimit={gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe={true} + isSpeedUp={false} + /> + { this.renderAdvancedOptionsButton() } +
+ + if (advancedInlineGasShown) { + return advancedGasInputs + } else if (gasButtonGroupShown) { + return gasPriceButtonGroup + } else { + return gasFeeDisplay + } + } + + render () { + const { gasFeeError } = this.props + + return ( + + { this.renderContent() } + + ) + } + +} 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/app/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 new file mode 100644 index 000000000..e69de29bb 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(, { 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/app/send/send-content/send-hex-data-row/index.js b/ui/app/components/app/send/send-content/send-hex-data-row/index.js new file mode 100644 index 000000000..08c341067 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-hex-data-row/index.js @@ -0,0 +1 @@ +export { default } from './send-hex-data-row.container' diff --git a/ui/app/components/app/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 new file mode 100644 index 000000000..62a74a77b --- /dev/null +++ b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js @@ -0,0 +1,42 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' + +export default class SendHexDataRow extends Component { + static propTypes = { + data: PropTypes.string, + inError: PropTypes.bool, + updateSendHexData: PropTypes.func.isRequired, + updateGas: PropTypes.func.isRequired, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onInput = (event) => { + const {updateSendHexData, updateGas} = this.props + const data = event.target.value.replace(/\n/g, '') || null + updateSendHexData(data) + updateGas({ data }) + } + + render () { + const {inError} = this.props + const {t} = this.context + + return ( + +