aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/app/actions.js330
-rw-r--r--ui/app/app.js34
-rw-r--r--ui/app/components/account-menu/index.js69
-rw-r--r--ui/app/components/alert/index.js62
-rw-r--r--ui/app/components/app-header/app-header.component.js3
-rw-r--r--ui/app/components/button-group/button-group.component.js61
-rw-r--r--ui/app/components/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/button-group/index.js1
-rw-r--r--ui/app/components/button-group/index.scss38
-rw-r--r--ui/app/components/button-group/tests/button-group-component.test.js97
-rw-r--r--ui/app/components/button/button.component.js21
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js52
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/index.scss50
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js64
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js105
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js28
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss17
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js56
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss54
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js22
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss18
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.js4
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.scss66
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js63
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/index.scss27
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js118
-rw-r--r--ui/app/components/confirm-page-container/index.js8
-rw-r--r--ui/app/components/confirm-page-container/index.scss5
-rw-r--r--ui/app/components/customize-gas-modal/index.js59
-rw-r--r--ui/app/components/dropdowns/account-dropdown-mini.js2
-rw-r--r--ui/app/components/dropdowns/components/dropdown.js1
-rw-r--r--ui/app/components/dropdowns/components/network-dropdown-icon.js3
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js3
-rw-r--r--ui/app/components/dropdowns/token-menu-dropdown.js6
-rw-r--r--ui/app/components/ens-input.js79
-rw-r--r--ui/app/components/identicon.js1
-rw-r--r--ui/app/components/index.scss12
-rw-r--r--ui/app/components/input-number.js12
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js93
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js20
-rw-r--r--ui/app/components/modals/confirm-remove-account/index.js2
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js140
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.container.js22
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.util.js34
-rw-r--r--ui/app/components/modals/customize-gas/index.js1
-rw-r--r--ui/app/components/modals/customize-gas/index.scss110
-rw-r--r--ui/app/components/modals/index.scss54
-rw-r--r--ui/app/components/modals/modal.js42
-rw-r--r--ui/app/components/network-display.js56
-rw-r--r--ui/app/components/network-display/index.js2
-rw-r--r--ui/app/components/network-display/index.scss54
-rw-r--r--ui/app/components/network-display/network-display.component.js69
-rw-r--r--ui/app/components/network-display/network-display.container.js11
-rw-r--r--ui/app/components/page-container/index.js3
-rw-r--r--ui/app/components/page-container/index.scss186
-rw-r--r--ui/app/components/page-container/page-container-footer/page-container-footer.component.js10
-rw-r--r--ui/app/components/page-container/page-container-header.component.js35
-rw-r--r--ui/app/components/page-container/page-container-header/page-container-header.component.js33
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.component.js21
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.container.js15
-rw-r--r--ui/app/components/pages/confirm-approve/index.js1
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js64
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js12
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/index.js1
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js39
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js45
-rw-r--r--ui/app/components/pages/confirm-send-ether/index.js1
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.component.js29
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.container.js52
-rw-r--r--ui/app/components/pages/confirm-send-token/index.js1
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js88
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js34
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/index.js2
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js345
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js181
-rw-r--r--ui/app/components/pages/confirm-transaction-base/index.js1
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js85
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js3
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js20
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js4
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/index.js2
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.component.js157
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.container.js33
-rw-r--r--ui/app/components/pages/confirm-transaction/index.js2
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js143
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js149
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js241
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js3
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js3
-rw-r--r--ui/app/components/pages/create-account/index.js27
-rw-r--r--ui/app/components/pages/create-account/new-account.js2
-rw-r--r--ui/app/components/pages/home.js108
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js313
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js72
-rw-r--r--ui/app/components/pending-tx/confirm-deploy-contract.js358
-rw-r--r--ui/app/components/pending-tx/confirm-send-ether.js692
-rw-r--r--ui/app/components/pending-tx/confirm-send-token.js696
-rw-r--r--ui/app/components/pending-tx/index.js165
-rw-r--r--ui/app/components/selected-account/selected-account.component.js11
-rw-r--r--ui/app/components/send/README.md (renamed from ui/app/components/send_/README.md)0
-rw-r--r--ui/app/components/send/account-list-item/account-list-item-README.md (renamed from ui/app/components/send_/account-list-item/account-list-item-README.md)0
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.component.js (renamed from ui/app/components/send_/account-list-item/account-list-item.component.js)11
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.container.js (renamed from ui/app/components/send_/account-list-item/account-list-item.container.js)4
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.scss (renamed from ui/app/components/send_/account-list-item/account-list-item.scss)0
-rw-r--r--ui/app/components/send/account-list-item/index.js1
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-component.test.js (renamed from ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js)2
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-container.test.js (renamed from ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js)2
-rw-r--r--ui/app/components/send/currency-display/currency-display.js (renamed from ui/app/components/send/currency-display.js)48
-rw-r--r--ui/app/components/send/currency-display/index.js1
-rw-r--r--ui/app/components/send/gas-fee-display-v2.js53
-rw-r--r--ui/app/components/send/index.js1
-rw-r--r--ui/app/components/send/send-content/index.js1
-rw-r--r--ui/app/components/send/send-content/send-amount-row/README.md (renamed from ui/app/components/send_/send-content/send-amount-row/README.md)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js1
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js)29
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js)9
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.scss (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js)31
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js)20
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-content-README.md (renamed from ui/app/components/send_/send-content/send-content-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-content.component.js (renamed from ui/app/components/send_/send-content/send-content.component.js)2
-rw-r--r--ui/app/components/send/send-content/send-content.scss (renamed from ui/app/components/send_/send-content/send-content.scss)0
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/index.js1
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js (renamed from ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js (renamed from ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/index.js1
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row-README.md (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.component.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.container.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/README.md (renamed from ui/app/components/send_/send-content/send-gas-row/README.md)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js61
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js1
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js55
-rw-r--r--ui/app/components/send/send-content/send-gas-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js)18
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js)9
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.scss (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js14
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js (renamed from ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js)7
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js (renamed from ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js)8
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js49
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js40
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js21
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/index.js1
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js1
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row-README.md (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.component.js)15
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.container.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.utils.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js)6
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js)12
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js)6
-rw-r--r--ui/app/components/send/send-content/tests/send-content-component.test.js (renamed from ui/app/components/send_/send-content/tests/send-content-component.test.js)0
-rw-r--r--ui/app/components/send/send-footer/README.md (renamed from ui/app/components/send_/send-footer/README.md)0
-rw-r--r--ui/app/components/send/send-footer/index.js1
-rw-r--r--ui/app/components/send/send-footer/send-footer.component.js (renamed from ui/app/components/send_/send-footer/send-footer.component.js)19
-rw-r--r--ui/app/components/send/send-footer/send-footer.container.js (renamed from ui/app/components/send_/send-footer/send-footer.container.js)9
-rw-r--r--ui/app/components/send/send-footer/send-footer.scss (renamed from ui/app/components/send_/send-footer/send-footer.scss)0
-rw-r--r--ui/app/components/send/send-footer/send-footer.selectors.js (renamed from ui/app/components/send_/send-footer/send-footer.selectors.js)0
-rw-r--r--ui/app/components/send/send-footer/send-footer.utils.js (renamed from ui/app/components/send_/send-footer/send-footer.utils.js)38
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-component.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-component.test.js)13
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-container.test.js)5
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-selectors.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-utils.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-utils.test.js)24
-rw-r--r--ui/app/components/send/send-header/README.md (renamed from ui/app/components/send_/send-header/README.md)0
-rw-r--r--ui/app/components/send/send-header/index.js1
-rw-r--r--ui/app/components/send/send-header/send-header.component.js (renamed from ui/app/components/send_/send-header/send-header.component.js)8
-rw-r--r--ui/app/components/send/send-header/send-header.container.js (renamed from ui/app/components/send_/send-header/send-header.container.js)0
-rw-r--r--ui/app/components/send/send-header/send-header.selectors.js (renamed from ui/app/components/send_/send-header/send-header.selectors.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-component.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-component.test.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-container.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-container.test.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-selectors.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-selectors.test.js)0
-rw-r--r--ui/app/components/send/send.component.js (renamed from ui/app/components/send_/send.component.js)27
-rw-r--r--ui/app/components/send/send.constants.js (renamed from ui/app/components/send_/send.constants.js)2
-rw-r--r--ui/app/components/send/send.container.js (renamed from ui/app/components/send_/send.container.js)2
-rw-r--r--ui/app/components/send/send.scss (renamed from ui/app/components/send_/send.scss)0
-rw-r--r--ui/app/components/send/send.selectors.js (renamed from ui/app/components/send_/send.selectors.js)10
-rw-r--r--ui/app/components/send/send.utils.js (renamed from ui/app/components/send_/send.utils.js)49
-rw-r--r--ui/app/components/send/tests/send-component.test.js (renamed from ui/app/components/send_/tests/send-component.test.js)75
-rw-r--r--ui/app/components/send/tests/send-container.test.js (renamed from ui/app/components/send_/tests/send-container.test.js)12
-rw-r--r--ui/app/components/send/tests/send-selectors-test-data.js (renamed from ui/app/components/send_/tests/send-selectors-test-data.js)0
-rw-r--r--ui/app/components/send/tests/send-selectors.test.js (renamed from ui/app/components/send_/tests/send-selectors.test.js)10
-rw-r--r--ui/app/components/send/tests/send-utils.test.js (renamed from ui/app/components/send_/tests/send-utils.test.js)78
-rw-r--r--ui/app/components/send/to-autocomplete.component.js141
-rw-r--r--ui/app/components/send/to-autocomplete/index.js1
-rw-r--r--ui/app/components/send/to-autocomplete/to-autocomplete.js (renamed from ui/app/components/send/to-autocomplete.js)2
-rw-r--r--ui/app/components/send_/account-list-item/index.js1
-rw-r--r--ui/app/components/send_/index.js1
-rw-r--r--ui/app/components/send_/send-content/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-dropdown-list/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-from-row/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js9
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js22
-rw-r--r--ui/app/components/send_/send-content/send-row-wrapper/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js1
-rw-r--r--ui/app/components/send_/send-content/send-to-row/index.js1
-rw-r--r--ui/app/components/send_/send-footer/index.js1
-rw-r--r--ui/app/components/send_/send-header/index.js1
-rw-r--r--ui/app/components/sender-to-recipient.js72
-rw-r--r--ui/app/components/sender-to-recipient/index.js1
-rw-r--r--ui/app/components/sender-to-recipient/index.scss (renamed from ui/app/css/itcss/components/sender-to-recipient.scss)26
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js117
-rw-r--r--ui/app/components/shapeshift-form.js2
-rw-r--r--ui/app/components/signature-request.js16
-rw-r--r--ui/app/components/tabs/index.js3
-rw-r--r--ui/app/components/tabs/index.scss11
-rw-r--r--ui/app/components/tabs/tab/index.js2
-rw-r--r--ui/app/components/tabs/tab/index.scss15
-rw-r--r--ui/app/components/tabs/tab/tab.component.js31
-rw-r--r--ui/app/components/tabs/tabs.component.js62
-rw-r--r--ui/app/components/token-balance.js8
-rw-r--r--ui/app/components/token-list.js5
-rw-r--r--ui/app/components/tooltip-v2.js4
-rw-r--r--ui/app/components/tx-list-item.js44
-rw-r--r--ui/app/components/tx-view.js37
-rw-r--r--ui/app/components/wallet-view.js2
-rw-r--r--ui/app/conf-tx.js139
-rw-r--r--ui/app/constants/error-keys.js3
-rw-r--r--ui/app/conversion-util.js2
-rw-r--r--ui/app/conversion-util.test.js22
-rw-r--r--ui/app/css/itcss/components/account-menu.scss18
-rw-r--r--ui/app/css/itcss/components/alert.scss57
-rw-r--r--ui/app/css/itcss/components/buttons.scss25
-rw-r--r--ui/app/css/itcss/components/currency-display.scss15
-rw-r--r--ui/app/css/itcss/components/hero-balance.scss15
-rw-r--r--ui/app/css/itcss/components/index.scss4
-rw-r--r--ui/app/css/itcss/components/modal.scss31
-rw-r--r--ui/app/css/itcss/components/network.scss13
-rw-r--r--ui/app/css/itcss/components/new-account.scss297
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss17
-rw-r--r--ui/app/css/itcss/components/request-signature.scss1
-rw-r--r--ui/app/css/itcss/components/send.scss4
-rw-r--r--ui/app/css/itcss/components/token-list.scss1
-rw-r--r--ui/app/css/itcss/components/transaction-list.scss10
-rw-r--r--ui/app/css/itcss/generic/index.scss189
-rw-r--r--ui/app/css/itcss/settings/variables.scss1
-rw-r--r--ui/app/css/itcss/tools/utilities.scss2
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js386
-rw-r--r--ui/app/ducks/send.duck.js7
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js675
-rw-r--r--ui/app/ducks/tests/send-duck.test.js10
-rw-r--r--ui/app/helpers/confirm-transaction/util.js148
-rw-r--r--ui/app/helpers/confirm-transaction/util.test.js137
-rw-r--r--ui/app/helpers/with-token-tracker.js3
-rw-r--r--ui/app/i18n-provider.js5
-rw-r--r--ui/app/main-container.js3
-rw-r--r--ui/app/metamask-connect.js27
-rw-r--r--ui/app/reducers.js3
-rw-r--r--ui/app/reducers/app.js26
-rw-r--r--ui/app/reducers/metamask.js8
-rw-r--r--ui/app/routes.js23
-rw-r--r--ui/app/selectors.js24
-rw-r--r--ui/app/selectors/confirm-transaction.js196
-rw-r--r--ui/app/token-util.js7
-rw-r--r--ui/app/util.js14
-rw-r--r--ui/i18n-helper.js25
299 files changed, 7887 insertions, 3234 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 1edf692b6..7a8d9667d 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -6,11 +6,12 @@ const {
calcGasTotal,
calcTokenBalance,
estimateGas,
- estimateGasPriceFromRecentBlocks,
-} = require('./components/send_/send.utils')
+} = 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')
var actions = {
_setBackgroundConnection: _setBackgroundConnection,
@@ -27,6 +28,11 @@ var actions = {
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,
// network dropdown open
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
@@ -79,9 +85,14 @@ var actions = {
addNewKeyring,
importNewAccount,
addNewAccount,
+ connectHardware,
+ checkHardwareStatus,
+ forgetDevice,
+ unlockTrezorAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen,
resetAccount,
+ removeAccount,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN',
@@ -165,6 +176,7 @@ var actions = {
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',
@@ -175,6 +187,8 @@ var actions = {
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,
@@ -182,6 +196,7 @@ var actions = {
setSendTokenBalance,
updateSendTokenBalance,
updateSendFrom,
+ updateSendHexData,
updateSendTo,
updateSendAmount,
updateSendMemo,
@@ -190,6 +205,8 @@ var actions = {
updateSendErrors,
clearSend,
setSelectedAddress,
+ gasLoadingStarted,
+ gasLoadingFinished,
// app messages
confirmSeedWords: confirmSeedWords,
showAccountDetail: showAccountDetail,
@@ -530,6 +547,26 @@ function resetAccount () {
}
}
+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())
@@ -596,6 +633,88 @@ function addNewAccount () {
}
}
+function checkHardwareStatus (deviceName) {
+ log.debug(`background.checkHardwareStatus`, deviceName)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.checkHardwareStatus(deviceName, (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) {
+ log.debug(`background.connectHardware`, deviceName, page)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.connectHardware(deviceName, page, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(accounts)
+ })
+ })
+ }
+}
+
+function unlockTrezorAccount (index) {
+ log.debug(`background.unlockTrezorAccount`, index)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.unlockTrezorAccount(index, (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,
@@ -626,7 +745,7 @@ function setCurrentCurrency (currencyCode) {
function signMsg (msgData) {
log.debug('action - signMsg')
- return (dispatch) => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -643,6 +762,12 @@ function signMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -651,7 +776,7 @@ function signMsg (msgData) {
function signPersonalMsg (msgData) {
log.debug('action - signPersonalMsg')
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -668,6 +793,12 @@ function signPersonalMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -676,7 +807,7 @@ function signPersonalMsg (msgData) {
function signTypedMsg (msgData) {
log.debug('action - signTypedMsg')
- return (dispatch) => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -693,6 +824,12 @@ function signTypedMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -701,11 +838,10 @@ function signTypedMsg (msgData) {
function signTx (txData) {
return (dispatch) => {
- dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => {
- dispatch(actions.hideLoadingIndication())
- if (err) return dispatch(actions.displayWarning(err.message))
- dispatch(actions.hideWarning())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
})
dispatch(actions.showConfTxPage({}))
}
@@ -740,20 +876,28 @@ function updateGasData ({
to,
value,
}) {
- const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return (dispatch) => {
- return Promise.all([
- Promise.resolve(estimatedGasPrice),
- estimateGas({
- estimateGasMethod: background.estimateGas,
- blockGasLimit,
- selectedAddress,
- selectedToken,
- to,
- value,
- gasPrice: estimatedGasPrice,
- }),
- ])
+ dispatch(actions.gasLoadingStarted())
+ return new Promise((resolve, reject) => {
+ background.getGasPrice((err, data) => {
+ if (err) return reject(err)
+ return resolve(data)
+ })
+ })
+ .then(estimateGasPrice => {
+ return Promise.all([
+ Promise.resolve(estimateGasPrice),
+ estimateGas({
+ estimateGasMethod: background.estimateGas,
+ blockGasLimit,
+ selectedAddress,
+ selectedToken,
+ to,
+ value,
+ estimateGasPrice,
+ }),
+ ])
+ })
.then(([gasPrice, gas]) => {
dispatch(actions.setGasPrice(gasPrice))
dispatch(actions.setGasLimit(gas))
@@ -762,14 +906,28 @@ function updateGasData ({
.then((gasEstimate) => {
dispatch(actions.setGasTotal(gasEstimate))
dispatch(updateSendErrors({ gasLoadingError: null }))
+ dispatch(actions.gasLoadingFinished())
})
.catch(err => {
log.error(err)
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
+ dispatch(actions.gasLoadingFinished())
})
}
}
+function gasLoadingStarted () {
+ return {
+ type: actions.GAS_LOADING_STARTED,
+ }
+}
+
+function gasLoadingFinished () {
+ return {
+ type: actions.GAS_LOADING_FINISHED,
+ }
+}
+
function updateSendTokenBalance ({
selectedToken,
tokenContract,
@@ -814,6 +972,13 @@ function updateSendFrom (from) {
}
}
+function updateSendHexData (value) {
+ return {
+ type: actions.UPDATE_SEND_HEX_DATA,
+ value,
+ }
+}
+
function updateSendTo (to, nickname = '') {
return {
type: actions.UPDATE_SEND_TO,
@@ -858,7 +1023,7 @@ function clearSend () {
function sendTx (txData) {
log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
- return (dispatch) => {
+ return (dispatch, getState) => {
log.debug(`actions calling background.approveTransaction`)
background.approveTransaction(txData.id, (err) => {
if (err) {
@@ -866,6 +1031,11 @@ function sendTx (txData) {
return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
})
}
}
@@ -885,29 +1055,41 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
function updateTransaction (txData) {
log.info('actions: updateTx: ' + JSON.stringify(txData))
- return (dispatch) => {
+ return dispatch => {
log.debug(`actions calling background.updateTx`)
- background.updateTransaction(txData, (err) => {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
- if (err) {
- dispatch(actions.txError(err))
- dispatch(actions.goHome())
- return log.error(err.message)
- }
- dispatch(actions.showConfTxPage({ id: txData.id }))
+ 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) => {
+ return (dispatch, getState) => {
log.debug(`actions calling background.updateAndApproveTx`)
+ dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.updateAndApproveTransaction(txData, err => {
- dispatch(actions.hideLoadingIndication())
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.clearSend())
@@ -918,10 +1100,23 @@ function updateAndApproveTx (txData) {
reject(err)
}
- dispatch(actions.completedTx(txData.id))
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
+ })
}
}
@@ -948,7 +1143,7 @@ function txError (err) {
}
function cancelMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -962,6 +1157,12 @@ function cancelMsg (msgData) {
}
dispatch(actions.completedTx(msgData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -969,7 +1170,7 @@ function cancelMsg (msgData) {
}
function cancelPersonalMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -983,6 +1184,12 @@ function cancelPersonalMsg (msgData) {
}
dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -990,7 +1197,7 @@ function cancelPersonalMsg (msgData) {
}
function cancelTypedMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -1004,6 +1211,12 @@ function cancelTypedMsg (msgData) {
}
dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -1011,15 +1224,33 @@ function cancelTypedMsg (msgData) {
}
function cancelTx (txData) {
- return dispatch => {
+ return (dispatch, getState) => {
log.debug(`background.cancelTransaction`)
+ dispatch(actions.showLoadingIndication())
+
return new Promise((resolve, reject) => {
- background.cancelTransaction(txData.id, () => {
+ 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))
- resolve(txData)
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return txData
})
- })
}
}
@@ -1562,6 +1793,19 @@ function hideSidebar () {
}
}
+function showAlert (msg) {
+ return {
+ type: actions.ALERT_OPEN,
+ value: msg,
+ }
+}
+
+function hideAlert () {
+ return {
+ type: actions.ALERT_CLOSE,
+ }
+}
+
function showLoadingIndication (message) {
return {
diff --git a/ui/app/app.js b/ui/app/app.js
index d0e48a368..dbb6146d1 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -11,8 +11,8 @@ const log = require('loglevel')
// init
const InitializeScreen = require('../../mascara/src/app/first-time').default
// accounts
-const SendTransactionScreen = require('./components/send_/send.container')
-const ConfirmTxScreen = require('./conf-tx')
+const SendTransactionScreen = require('./components/send/send.container')
+const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
const WalletView = require('./components/wallet-view')
@@ -22,8 +22,7 @@ const Home = require('./components/pages/home')
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
-const UnlockPage = require('./components/pages/unlock-page')
-const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
+const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
@@ -37,9 +36,13 @@ const AccountMenu = require('./components/account-menu')
// Global Modals
const Modal = require('./components/modals/index').Modal
+// Global Alert
+const Alert = require('./components/alert')
const AppHeader = require('./components/app-header')
+import UnlockPage from './components/pages/unlock-page'
+
// Routes
const {
DEFAULT_ROUTE,
@@ -76,7 +79,10 @@ class App extends Component {
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
- h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }),
+ h(Authenticated, {
+ path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
+ component: ConfirmTransaction,
+ }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
@@ -89,6 +95,7 @@ class App extends Component {
render () {
const {
isLoading,
+ alertMessage,
loadingMessage,
network,
isMouseUser,
@@ -122,6 +129,9 @@ class App extends Component {
// global modal
h(Modal, {}, []),
+ // global alert
+ h(Alert, {visible: this.props.alertOpen, msg: alertMessage}),
+
h(AppHeader),
// sidebar
@@ -145,14 +155,6 @@ class App extends Component {
)
}
- renderGlobalModal () {
- return h(Modal, {
- ref: 'modalRef',
- }, [
- // h(BuyOptions, {}, []),
- ])
- }
-
renderSidebar () {
return h('div', [
h('style', `
@@ -261,11 +263,13 @@ App.propTypes = {
setCurrentCurrencyToUSD: PropTypes.func,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
+ alertMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
+ alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
@@ -301,6 +305,8 @@ function mapStateToProps (state) {
const {
networkDropdownOpen,
sidebarOpen,
+ alertOpen,
+ alertMessage,
isLoading,
loadingMessage,
} = appState
@@ -326,6 +332,8 @@ function mapStateToProps (state) {
// state from plugin
networkDropdownOpen,
sidebarOpen,
+ alertOpen,
+ alertMessage,
isLoading,
loadingMessage,
noActiveNotices,
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index f34631ca8..9c063d31e 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -9,11 +9,17 @@ const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
+const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
+const Tooltip = require('../tooltip')
+
+
const {
SETTINGS_ROUTE,
INFO_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
DEFAULT_ROUTE,
} = require('../../routes')
@@ -63,6 +69,9 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.hideSidebar())
dispatch(actions.toggleAccountMenu())
},
+ showRemoveAccountConfirmationModal: (identity) => {
+ return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
+ },
}
}
@@ -106,6 +115,18 @@ AccountMenu.prototype.render = function () {
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
text: this.context.t('importAccount'),
}),
+ h(Item, {
+ onClick: () => {
+ toggleAccountMenu()
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
+ } else {
+ history.push(CONNECT_HARDWARE_ROUTE)
+ }
+ },
+ icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
+ text: this.context.t('connectHardwareWallet'),
+ }),
h(Divider),
h(Item, {
onClick: () => {
@@ -136,7 +157,8 @@ AccountMenu.prototype.renderAccounts = function () {
} = this.props
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
- return accountOrder.map((address) => {
+ return accountOrder.filter(address => !!identities[address]).map((address) => {
+
const identity = identities[address]
const isSelected = identity.address === selectedAddress
@@ -170,16 +192,53 @@ AccountMenu.prototype.renderAccounts = function () {
h('div.account-menu__balance', formattedBalance),
]),
- this.indicateIfLoose(keyring),
+ this.renderKeyringType(keyring),
+ this.renderRemoveAccount(keyring, identity),
],
)
})
}
-AccountMenu.prototype.indicateIfLoose = function (keyring) {
+AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) {
+ // Any account that's not from the HD wallet Keyring can be removed
+ const type = keyring.type
+ const isRemovable = type !== 'HD Key Tree'
+ if (isRemovable) {
+ return h(Tooltip, {
+ title: this.context.t('removeAccount'),
+ position: 'bottom',
+ }, [
+ h('a.remove-account-icon', {
+ onClick: (e) => this.removeAccount(e, identity),
+ }, ''),
+ ])
+ }
+ return null
+}
+
+AccountMenu.prototype.removeAccount = function (e, identity) {
+ e.preventDefault()
+ e.stopPropagation()
+ const { showRemoveAccountConfirmationModal } = this.props
+ showRemoveAccountConfirmationModal(identity)
+}
+
+AccountMenu.prototype.renderKeyringType = function (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
- return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null
+ let label
+ switch (type) {
+ case 'Trezor Hardware':
+ label = this.context.t('hardware')
+ break
+ case 'Simple Key Pair':
+ label = this.context.t('imported')
+ break
+ default:
+ label = ''
+ }
+
+ return label !== '' ? h('.keyring-label.allcaps', label) : null
+
} catch (e) { return }
}
diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js
new file mode 100644
index 000000000..5620d847a
--- /dev/null
+++ b/ui/app/components/alert/index.js
@@ -0,0 +1,62 @@
+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
index 62b04562a..07ca6cf84 100644
--- a/ui/app/components/app-header/app-header.component.js
+++ b/ui/app/components/app-header/app-header.component.js
@@ -91,7 +91,6 @@ class AppHeader extends Component {
network,
provider,
history,
- location,
isUnlocked,
} = this.props
@@ -126,7 +125,7 @@ class AppHeader extends Component {
network={network}
provider={provider}
onClick={event => this.handleNetworkIndicatorClick(event)}
- disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE}
+ disabled={this.isConfirming()}
/>
</div>
{ this.renderAccountMenu() }
diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js
new file mode 100644
index 000000000..f99f710ce
--- /dev/null
+++ b/ui/app/components/button-group/button-group.component.js
@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class ButtonGroup extends PureComponent {
+ static propTypes = {
+ defaultActiveButtonIndex: PropTypes.number,
+ disabled: PropTypes.bool,
+ children: PropTypes.array,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ }
+
+ static defaultProps = {
+ className: 'button-group',
+ }
+
+ state = {
+ activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
+ }
+
+ handleButtonClick (activeButtonIndex) {
+ this.setState({ activeButtonIndex })
+ }
+
+ renderButtons () {
+ const { children, disabled } = this.props
+
+ return React.Children.map(children, (child, index) => {
+ return child && (
+ <button
+ className={classnames(
+ 'button-group__button',
+ { 'button-group__button--active': index === this.state.activeButtonIndex },
+ )}
+ onClick={() => {
+ this.handleButtonClick(index)
+ child.props.onClick && child.props.onClick()
+ }}
+ disabled={disabled || child.props.disabled}
+ key={index}
+ >
+ { child.props.children }
+ </button>
+ )
+ })
+ }
+
+ render () {
+ const { className, style } = this.props
+
+ return (
+ <div
+ className={className}
+ style={style}
+ >
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js
new file mode 100644
index 000000000..14e1a7e49
--- /dev/null
+++ b/ui/app/components/button-group/button-group.stories.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import ButtonGroup from './'
+import Button from '../button'
+import { text, boolean } from '@storybook/addon-knobs/react'
+
+storiesOf('ButtonGroup', module)
+ .add('with Buttons', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ defaultActiveButtonIndex={1}
+ >
+ <Button
+ onClick={action('cheap')}
+ >
+ {text('Button1', 'Cheap')}
+ </Button>
+ <Button
+ onClick={action('average')}
+ >
+ {text('Button2', 'Average')}
+ </Button>
+ <Button
+ onClick={action('fast')}
+ >
+ {text('Button3', 'Fast')}
+ </Button>
+ </ButtonGroup>
+ )
+ .add('with a disabled Button', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ >
+ <Button
+ onClick={action('enabled')}
+ >
+ {text('Button1', 'Enabled')}
+ </Button>
+ <Button
+ onClick={action('disabled')}
+ disabled
+ >
+ {text('Button2', 'Disabled')}
+ </Button>
+ </ButtonGroup>
+ )
diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js
new file mode 100644
index 000000000..df470bd57
--- /dev/null
+++ b/ui/app/components/button-group/index.js
@@ -0,0 +1 @@
+export { default } from './button-group.component'
diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss
new file mode 100644
index 000000000..29713c75b
--- /dev/null
+++ b/ui/app/components/button-group/index.scss
@@ -0,0 +1,38 @@
+.button-group {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &__button {
+ font-family: Roboto;
+ font-size: 1rem;
+ color: $tundora;
+ border-style: solid;
+ border-color: $alto;
+ border-width: 1px 1px 1px;
+ border-left: 0;
+ flex: 1;
+ padding: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:first-child {
+ border-left: 1px solid $alto;
+ border-radius: 4px 0 0 4px;
+ }
+
+ &:last-child {
+ border-radius: 0 4px 4px 0;
+ }
+
+ &--active {
+ background-color: $dodger-blue;
+ color: $white;
+ }
+
+ &:disabled {
+ opacity: .5;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js
new file mode 100644
index 000000000..f07bb97c8
--- /dev/null
+++ b/ui/app/components/button-group/tests/button-group-component.test.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ButtonGroup from '../button-group.component.js'
+
+const childButtonSpies = {
+ onClick: sinon.spy(),
+}
+
+sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
+sinon.spy(ButtonGroup.prototype, 'renderButtons')
+
+const mockButtons = [
+ <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
+ <button onClick={childButtonSpies.onClick} key={'b'}></button>,
+ <button onClick={childButtonSpies.onClick} key={'c'}></button>,
+]
+
+describe('ButtonGroup Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<ButtonGroup
+ defaultActiveButtonIndex={1}
+ disabled={false}
+ className="someClassName"
+ style={ { color: 'red' } }
+ >{mockButtons}</ButtonGroup>)
+ })
+
+ afterEach(() => {
+ childButtonSpies.onClick.resetHistory()
+ ButtonGroup.prototype.handleButtonClick.resetHistory()
+ ButtonGroup.prototype.renderButtons.resetHistory()
+ })
+
+ describe('handleButtonClick', () => {
+ it('should set the activeButtonIndex', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.instance().handleButtonClick(2)
+ assert.equal(wrapper.state('activeButtonIndex'), 2)
+ })
+ })
+
+ describe('renderButtons', () => {
+ it('should render a button for each child', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ assert.equal(childButtons.length, 3)
+ })
+
+ it('should render the correct button with an active state', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ const activeChildButton = wrapper.find('.button-group__button--active')
+ assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
+ })
+
+ it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
+ assert.equal(childButtonSpies.onClick.callCount, 0)
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.at(0).props().onClick()
+ childButtons.at(1).props().onClick()
+ childButtons.at(2).props().onClick()
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
+ assert.equal(childButtonSpies.onClick.callCount, 3)
+ })
+
+ it('should render all child buttons as disabled if props.disabled is true', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.forEach(button => {
+ assert.equal(button.props().disabled, undefined)
+ })
+ wrapper.setProps({ disabled: true })
+ const disabledChildButtons = wrapper.find('[disabled=true]')
+ assert.equal(disabledChildButtons.length, 3)
+ })
+
+ it('should render the children of the button', () => {
+ const mockClass = wrapper.find('.mockClass')
+ assert.equal(mockClass.length, 1)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with the expected class and style', () => {
+ assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
+ assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
+ })
+
+ it('should call renderButtons when rendering', () => {
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
+ wrapper.instance().render()
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
+ })
+ })
+})
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index e8e798445..1e0ef1b64 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -5,15 +5,24 @@ import classnames from 'classnames'
const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary'
+const CLASSNAME_CONFIRM = 'btn-confirm'
const CLASSNAME_LARGE = 'btn--large'
const typeHash = {
default: CLASSNAME_DEFAULT,
primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY,
+ confirm: CLASSNAME_CONFIRM,
}
-class Button extends Component {
+export default class Button extends Component {
+ static propTypes = {
+ type: PropTypes.string,
+ large: PropTypes.bool,
+ className: PropTypes.string,
+ children: PropTypes.string,
+ }
+
render () {
const { type, large, className, ...buttonProps } = this.props
@@ -31,13 +40,3 @@ class Button extends Component {
)
}
}
-
-Button.propTypes = {
- type: PropTypes.string,
- large: PropTypes.bool,
- className: PropTypes.string,
- children: PropTypes.string,
-}
-
-export default Button
-
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
new file mode 100644
index 000000000..f0703dde2
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const ConfirmDetailRow = props => {
+ const {
+ label,
+ fiatText,
+ ethText,
+ onHeaderClick,
+ fiatTextColor,
+ headerText,
+ headerTextClassName,
+ } = props
+
+ return (
+ <div className="confirm-detail-row">
+ <div className="confirm-detail-row__label">
+ { label }
+ </div>
+ <div className="confirm-detail-row__details">
+ <div
+ className={classnames('confirm-detail-row__header-text', headerTextClassName)}
+ onClick={() => onHeaderClick && onHeaderClick()}
+ >
+ { headerText }
+ </div>
+ <div
+ className="confirm-detail-row__fiat"
+ style={{ color: fiatTextColor }}
+ >
+ { fiatText }
+ </div>
+ <div className="confirm-detail-row__eth">
+ { ethText }
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmDetailRow.propTypes = {
+ label: PropTypes.string,
+ fiatText: PropTypes.string,
+ ethText: PropTypes.string,
+ fiatTextColor: PropTypes.string,
+ onHeaderClick: PropTypes.func,
+ headerText: PropTypes.string,
+ headerTextClassName: PropTypes.string,
+}
+
+export default ConfirmDetailRow
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/confirm-page-container/confirm-detail-row/index.js
new file mode 100644
index 000000000..056afff04
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-detail-row.component'
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
new file mode 100644
index 000000000..dd6f87c17
--- /dev/null
+++ b/ui/app/components/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;
+ }
+
+ &__fiat {
+ font-size: 1.5rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__eth {
+ color: $oslo-gray;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__header-text {
+ font-size: .75rem;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ color: $scorpion;
+
+ &--edit {
+ color: $curious-blue;
+ cursor: pointer;
+ }
+
+ &--total {
+ font-size: .625rem;
+ }
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
new file mode 100644
index 000000000..6f2489071
--- /dev/null
+++ b/ui/app/components/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(<ConfirmDetailRow
+ errorType={'mockErrorType'}
+ label={'mockLabel'}
+ showError={false}
+ fiatText = {'mockFiatText'}
+ ethText = {'mockEthText'}
+ fiatTextColor= {'mockColor'}
+ onHeaderClick= {propsMethodSpies.onHeaderClick}
+ headerText = {'mockHeaderText'}
+ headerTextClassName = {'mockHeaderClass'}
+ />)
+ })
+
+ 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 fiatText as a child of the confirm-detail-row__fiat', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText')
+ })
+
+ it('should render the ethText as a child of the confirm-detail-row__eth', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText')
+ })
+
+ it('should set the fiatTextColor on confirm-detail-row__fiat', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__fiat').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/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
new file mode 100644
index 000000000..08923af88
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { Tabs, Tab } from '../../tabs'
+import {
+ ConfirmPageContainerSummary,
+ ConfirmPageContainerError,
+ ConfirmPageContainerWarning,
+} from './'
+
+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,
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ summaryComponent: PropTypes.node,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ titleComponent: PropTypes.func,
+ warning: PropTypes.string,
+ }
+
+ renderContent () {
+ const { detailsComponent, dataComponent } = this.props
+
+ if (detailsComponent && dataComponent) {
+ return this.renderTabs()
+ } else {
+ return detailsComponent || dataComponent
+ }
+ }
+
+ renderTabs () {
+ const { detailsComponent, dataComponent } = this.props
+
+ return (
+ <Tabs>
+ <Tab name="Details">
+ { detailsComponent }
+ </Tab>
+ <Tab name="Data">
+ { dataComponent }
+ </Tab>
+ </Tabs>
+ )
+ }
+
+ render () {
+ const {
+ action,
+ errorKey,
+ errorMessage,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ nonce,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ warning,
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content">
+ {
+ warning && (
+ <ConfirmPageContainerWarning warning={warning} />
+ )
+ }
+ {
+ summaryComponent || (
+ <ConfirmPageContainerSummary
+ className={classnames({
+ 'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
+ })}
+ action={action}
+ title={title}
+ subtitle={subtitle}
+ hideSubtitle={hideSubtitle}
+ identiconAddress={identiconAddress}
+ nonce={nonce}
+ />
+ )
+ }
+ { this.renderContent() }
+ {
+ (errorKey || errorMessage) && (
+ <div className="confirm-page-container-content__error-container">
+ <ConfirmPageContainerError
+ errorMessage={errorMessage}
+ errorKey={errorKey}
+ />
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
new file mode 100644
index 000000000..70ebdeb20
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const ConfirmPageContainerError = (props, context) => {
+ const { errorMessage, errorKey } = props
+ const error = errorKey ? context.t(errorKey) : errorMessage
+
+ return (
+ <div className="confirm-page-container-error">
+ <img
+ src="/images/alert-red.svg"
+ className="confirm-page-container-error__icon"
+ />
+ { `ALERT: ${error}` }
+ </div>
+ )
+}
+
+ConfirmPageContainerError.propTypes = {
+ errorMessage: PropTypes.string,
+ errorKey: PropTypes.string,
+}
+
+ConfirmPageContainerError.contextTypes = {
+ t: PropTypes.func,
+}
+
+export default ConfirmPageContainerError
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
new file mode 100644
index 000000000..4ac95d0e3
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-error.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
new file mode 100644
index 000000000..e99b0f631
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
@@ -0,0 +1,17 @@
+.confirm-page-container-error {
+ height: 32px;
+ border: 1px solid $monzo;
+ color: $monzo;
+ background: lighten($monzo, 56%);
+ border-radius: 4px;
+ font-size: .75rem;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ padding-left: 16px;
+
+ &__icon {
+ margin-right: 8px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
new file mode 100644
index 000000000..3b1ee62c5
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../../../identicon'
+
+const ConfirmPageContainerSummary = props => {
+ const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
+
+ return (
+ <div className={classnames('confirm-page-container-summary', className)}>
+ <div className="confirm-page-container-summary__action-row">
+ <div className="confirm-page-container-summary__action">
+ { action }
+ </div>
+ {
+ nonce && (
+ <div className="confirm-page-container-summary__nonce">
+ { `#${nonce}` }
+ </div>
+ )
+ }
+ </div>
+ <div className="confirm-page-container-summary__title">
+ {
+ identiconAddress && (
+ <Identicon
+ className="confirm-page-container-summary__identicon"
+ diameter={36}
+ address={identiconAddress}
+ />
+ )
+ }
+ <div className="confirm-page-container-summary__title-text">
+ { title }
+ </div>
+ </div>
+ {
+ hideSubtitle || <div className="confirm-page-container-summary__subtitle">
+ { subtitle }
+ </div>
+ }
+ </div>
+ )
+}
+
+ConfirmPageContainerSummary.propTypes = {
+ action: PropTypes.string,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ hideSubtitle: PropTypes.bool,
+ className: PropTypes.string,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+}
+
+export default ConfirmPageContainerSummary
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js b/ui/app/components/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/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/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/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/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/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/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/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 (
+ <div className="confirm-page-container-warning">
+ <img
+ className="confirm-page-container-warning__icon"
+ src="/images/alert.svg"
+ />
+ <div className="confirm-page-container-warning__warning">
+ { props.warning }
+ </div>
+ </div>
+ )
+}
+
+ConfirmPageContainerWarning.propTypes = {
+ warning: PropTypes.string,
+}
+
+export default ConfirmPageContainerWarning
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/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/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/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/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/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/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
new file mode 100644
index 000000000..1469dd438
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
@@ -0,0 +1,4 @@
+export { default } from './confirm-page-container-content.component'
+export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
+export { default as ConfirmPageContainerError } from './confirm-page-container-error'
+export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
new file mode 100644
index 000000000..39797a43f
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
@@ -0,0 +1,66 @@
+@import './confirm-page-container-error/index';
+
+@import './confirm-page-container-warning/index';
+
+@import './confirm-page-container-summary/index';
+
+.confirm-page-container-content {
+ overflow-y: auto;
+ flex: 1;
+
+ &__error-container {
+ padding: 0 16px 16px 16px;
+ }
+
+ &__details {
+ box-sizing: border-box;
+ padding: 0 24px;
+ }
+
+ &__data {
+ padding: 16px;
+ color: $oslo-gray;
+ }
+
+ &__data-box {
+ background-color: #f9fafa;
+ padding: 12px;
+ font-size: .75rem;
+ margin-bottom: 16px;
+ word-wrap: break-word;
+ max-height: 200px;
+ overflow-y: auto;
+
+ &-label {
+ text-transform: uppercase;
+ padding: 8px 0 12px;
+ font-size: 12px;
+ }
+ }
+
+ &__data-field {
+ display: flex;
+ flex-direction: row;
+
+ &-label {
+ font-weight: 500;
+ padding-right: 16px;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+ }
+
+ &__gas-fee {
+ border-bottom: 1px solid $geyser;
+ }
+
+ &__function-type {
+ font-size: .875rem;
+ font-weight: 500;
+ text-transform: capitalize;
+ color: $black;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
new file mode 100644
index 000000000..e6fe8f82c
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+} from '../../../../../app/scripts/lib/enums'
+import NetworkDisplay from '../../network-display'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ showEdit: PropTypes.bool,
+ onEdit: PropTypes.func,
+ children: PropTypes.node,
+ }
+
+ renderTop () {
+ const { onEdit, showEdit } = this.props
+ const windowType = window.METAMASK_UI_TYPE
+ const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
+ windowType !== ENVIRONMENT_TYPE_POPUP
+
+ if (!showEdit && isFullScreen) {
+ return null
+ }
+
+ return (
+ <div className="confirm-page-container-header__row">
+ <div
+ className="confirm-page-container-header__back-button-container"
+ style={{
+ visibility: showEdit ? 'initial' : 'hidden',
+ }}
+ >
+ <img
+ src="/images/caret-left.svg"
+ />
+ <span
+ className="confirm-page-container-header__back-button"
+ onClick={() => onEdit()}
+ >
+ { this.context.t('edit') }
+ </span>
+ </div>
+ { !isFullScreen && <NetworkDisplay /> }
+ </div>
+ )
+ }
+
+ render () {
+ const { children } = this.props
+
+ return (
+ <div className="confirm-page-container-header">
+ { this.renderTop() }
+ { children }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js
new file mode 100644
index 000000000..71feb6931
--- /dev/null
+++ b/ui/app/components/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/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
new file mode 100644
index 000000000..43e1e4427
--- /dev/null
+++ b/ui/app/components/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: 13px 13px 13px 24px;
+ flex: 0 0 auto;
+ }
+
+ &__back-button-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js
new file mode 100644
index 000000000..24ff05353
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js
@@ -0,0 +1,118 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SenderToRecipient from '../sender-to-recipient'
+import { PageContainerFooter } from '../page-container'
+import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ // Header
+ action: PropTypes.string,
+ hideSubtitle: PropTypes.bool,
+ onEdit: PropTypes.func,
+ showEdit: PropTypes.bool,
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ titleComponent: PropTypes.func,
+ // 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,
+ summaryComponent: PropTypes.node,
+ warning: PropTypes.string,
+ // Footer
+ 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,
+ hideSubtitle,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ onCancel,
+ onSubmit,
+ identiconAddress,
+ nonce,
+ warning,
+ } = this.props
+
+ return (
+ <div className="page-container">
+ <ConfirmPageContainerHeader
+ showEdit={showEdit}
+ onEdit={() => onEdit()}
+ >
+ <SenderToRecipient
+ senderName={fromName}
+ senderAddress={fromAddress}
+ recipientName={toName}
+ recipientAddress={toAddress}
+ />
+ </ConfirmPageContainerHeader>
+ {
+ contentComponent || (
+ <ConfirmPageContainerContent
+ action={action}
+ title={title}
+ titleComponent={titleComponent}
+ subtitle={subtitle}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={detailsComponent}
+ dataComponent={dataComponent}
+ errorMessage={errorMessage}
+ errorKey={errorKey}
+ identiconAddress={identiconAddress}
+ nonce={nonce}
+ warning={warning}
+ />
+ )
+ }
+ <PageContainerFooter
+ onCancel={() => onCancel()}
+ onSubmit={() => onSubmit()}
+ submitText={this.context.t('confirm')}
+ submitButtonType="confirm"
+ disabled={disabled}
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/index.js b/ui/app/components/confirm-page-container/index.js
new file mode 100644
index 000000000..ee88aa5d3
--- /dev/null
+++ b/ui/app/components/confirm-page-container/index.js
@@ -0,0 +1,8 @@
+export { default } from './confirm-page-container.component'
+export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
+export { default as ConfirmDetailRow } from './confirm-detail-row'
+export {
+ default as ConfirmPageContainerContent,
+ ConfirmPageContainerSummary,
+ ConfirmPageContainerError,
+} from './confirm-page-container-content'
diff --git a/ui/app/components/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss
new file mode 100644
index 000000000..af7a5b555
--- /dev/null
+++ b/ui/app/components/confirm-page-container/index.scss
@@ -0,0 +1,5 @@
+@import './confirm-page-container-content/index';
+
+@import './confirm-page-container-header/index';
+
+@import './confirm-detail-row/index';
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
index c8522a3c7..c255fd64d 100644
--- a/ui/app/components/customize-gas-modal/index.js
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -16,11 +16,11 @@ const {
MIN_GAS_PRICE_DEC,
MIN_GAS_LIMIT_DEC,
MIN_GAS_PRICE_GWEI,
-} = require('../send_/send.constants')
+} = require('../send/send.constants')
const {
isBalanceSufficient,
-} = require('../send_/send.utils')
+} = require('../send/send.utils')
const {
conversionUtil,
@@ -31,8 +31,7 @@ const {
} = require('../../conversion-util')
const {
- getGasPrice,
- getGasLimit,
+ getGasIsLoading,
getForceGasMin,
conversionRateSelector,
getSendAmount,
@@ -43,6 +42,11 @@ const {
getSendMaxModeState,
} = require('../../selectors')
+const {
+ getGasPrice,
+ getGasLimit,
+} = require('../send/send.selectors')
+
function mapStateToProps (state) {
const selectedToken = getSelectedToken(state)
const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
@@ -51,6 +55,7 @@ function mapStateToProps (state) {
return {
gasPrice: getGasPrice(state),
gasLimit: getGasLimit(state),
+ gasIsLoading: getGasIsLoading(state),
forceGasMin: getForceGasMin(state),
conversionRate,
amount: getSendAmount(state),
@@ -73,7 +78,7 @@ function mapDispatchToProps (dispatch) {
}
}
-function getOriginalState (props) {
+function getFreshState (props) {
const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
@@ -97,7 +102,11 @@ inherits(CustomizeGasModal, Component)
function CustomizeGasModal (props) {
Component.call(this)
- this.state = getOriginalState(props)
+ const originalState = getFreshState(props)
+ this.state = {
+ ...originalState,
+ originalState,
+ }
}
CustomizeGasModal.contextTypes = {
@@ -106,6 +115,36 @@ CustomizeGasModal.contextTypes = {
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 {
@@ -137,7 +176,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
}
CustomizeGasModal.prototype.revert = function () {
- this.setState(getOriginalState(this.props))
+ this.setState(this.state.originalState)
}
CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
@@ -233,7 +272,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
}
CustomizeGasModal.prototype.render = function () {
- const { hideModal, forceGasMin } = this.props
+ const { hideModal, forceGasMin, gasIsLoading } = this.props
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
let convertedGasPrice = conversionUtil(gasPrice, {
@@ -266,7 +305,7 @@ CustomizeGasModal.prototype.render = function () {
toNumericBase: 'dec',
})
- return h('div.send-v2__customize-gas', {}, [
+ return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
h('div.send-v2__customize-gas__content', {
}, [
h('div.send-v2__customize-gas__header', {}, [
@@ -288,6 +327,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasPrice(value),
title: this.context.t('gasPrice'),
copy: this.context.t('gasPriceCalculation'),
+ gasIsLoading,
}),
h(GasModalCard, {
@@ -297,6 +337,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasLimit(value),
title: this.context.t('gasLimit'),
copy: this.context.t('gasLimitCalculation'),
+ gasIsLoading,
}),
]),
diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js
index a7a908d3b..261eb0aa2 100644
--- a/ui/app/components/dropdowns/account-dropdown-mini.js
+++ b/ui/app/components/dropdowns/account-dropdown-mini.js
@@ -1,7 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
+const AccountListItem = require('../send/account-list-item/account-list-item.component').default
module.exports = AccountDropdownMini
diff --git a/ui/app/components/dropdowns/components/dropdown.js b/ui/app/components/dropdowns/components/dropdown.js
index 0336dbb8b..149f063a7 100644
--- a/ui/app/components/dropdowns/components/dropdown.js
+++ b/ui/app/components/dropdowns/components/dropdown.js
@@ -87,7 +87,6 @@ class DropdownMenuItem extends Component {
padding: '8px 0px',
fontSize: '18px',
fontStyle: 'normal',
- fontFamily: 'Montserrat Regular',
cursor: 'pointer',
display: 'flex',
justifyContent: 'flex-start',
diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js
index 7e94e0af5..a45da4c10 100644
--- a/ui/app/components/dropdowns/components/network-dropdown-icon.js
+++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js
@@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () {
backgroundColor,
isSelected,
innerBorder = 'none',
+ diameter = '12',
} = this.props
return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
@@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () {
style: {
background: backgroundColor,
border: innerBorder,
+ height: `${diameter}px`,
+ width: `${diameter}px`,
},
})
)
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
index dcd6b4370..e5363ff56 100644
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ b/ui/app/components/dropdowns/network-dropdown.js
@@ -71,7 +71,6 @@ NetworkDropdown.prototype.render = function () {
const rpcList = props.frequentRpcList
const isOpen = this.props.networkDropdownOpen
const dropdownMenuItemStyle = {
- fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
@@ -286,7 +285,6 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget(rpc),
style: {
- fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
@@ -325,7 +323,6 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) {
onClick: () => props.setRpcTarget(rpcTarget),
closeMenu: () => this.props.hideNetworkDropdown(),
style: {
- fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js
index fac7c451b..5a794c7c1 100644
--- a/ui/app/components/dropdowns/token-menu-dropdown.js
+++ b/ui/app/components/dropdowns/token-menu-dropdown.js
@@ -54,7 +54,7 @@ TokenMenuDropdown.prototype.render = function () {
showHideTokenConfirmationModal(this.props.token)
this.props.onClose()
},
- text: this.context.t('hideToken'),
+ text: this.context.t('hideToken'),
}),
h(Item, {
onClick: (e) => {
@@ -62,7 +62,7 @@ TokenMenuDropdown.prototype.render = function () {
copyToClipboard(this.props.token.address)
this.props.onClose()
},
- text: this.context.t('copyContractAddress'),
+ text: this.context.t('copyContractAddress'),
}),
h(Item, {
onClick: (e) => {
@@ -71,7 +71,7 @@ TokenMenuDropdown.prototype.render = function () {
global.platform.openWindow({ url })
this.props.onClose()
},
- text: this.context.t('viewOnEtherscan'),
+ text: this.context.t('viewOnEtherscan'),
}),
])
}
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index aff4b6ef6..b9f99b3d1 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -10,8 +10,9 @@ 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')
+const ToAutoComplete = require('./send/to-autocomplete').default
const log = require('loglevel')
+const { isValidENSAddress } = require('../util')
EnsInput.contextTypes = {
t: PropTypes.func,
@@ -25,31 +26,34 @@ function EnsInput () {
Component.call(this)
}
-EnsInput.prototype.render = function () {
- const props = this.props
- const opts = extend(props, {
- list: 'addresses',
- onChange: (recipient) => {
- const network = this.props.network
- const networkHasEnsSupport = getNetworkEnsSupport(network)
+EnsInput.prototype.onChange = function (recipient) {
+ const network = this.props.network
+ const networkHasEnsSupport = getNetworkEnsSupport(network)
- props.onChange(recipient)
+ this.props.onChange({ toAddress: recipient })
- if (!networkHasEnsSupport) return
+ if (!networkHasEnsSupport) return
- if (recipient.match(ensRE) === null) {
- return this.setState({
- loadingEns: false,
- ensResolution: null,
- ensFailure: null,
- })
- }
+ if (recipient.match(ensRE) === null) {
+ return this.setState({
+ loadingEns: false,
+ ensResolution: null,
+ ensFailure: null,
+ toError: null,
+ })
+ }
- this.setState({
- loadingEns: true,
- })
- this.checkName(recipient)
- },
+ 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),
})
return h('div', {
style: { width: '100%', position: 'relative' },
@@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
nickname: recipient.trim(),
hoverText: address + '\n' + this.context.t('clickCopy'),
ensFailure: false,
+ toError: null,
})
}
})
.catch((reason) => {
- log.error(reason)
- return this.setState({
+ const setStateObj = {
loadingEns: false,
- ensResolution: ZERO_ADDRESS,
+ ensResolution: recipient,
ensFailure: true,
- hoverText: reason.message,
- })
+ 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)
})
}
@@ -105,9 +119,14 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
// 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(ensResolution, nickname)
+ this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError })
}
}
@@ -124,7 +143,9 @@ EnsInput.prototype.ensIcon = function (recipient) {
}
EnsInput.prototype.ensIconContents = function (recipient) {
- const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS}
+ const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
+
+ if (toError) return
if (loadingEns) {
return h('img', {
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index dce9b0449..424048745 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -36,6 +36,7 @@ IdenticonComponent.prototype.render = function () {
key: 'identicon-' + address,
style: {
display: 'flex',
+ flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
height: diameter,
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index 351640f6e..b3e14ce23 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -1,9 +1,21 @@
+@import './button-group/index';
+
@import './export-text-container/index';
@import './selected-account/index';
@import './info-box/index';
+@import './network-display/index';
+
+@import './confirm-page-container/index';
+
+@import './page-container/index';
+
@import './pages/index';
@import './modals/index';
+
+@import './sender-to-recipient/index';
+
+@import './tabs/index';
diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js
index de5fcca54..59c6842ef 100644
--- a/ui/app/components/input-number.js
+++ b/ui/app/components/input-number.js
@@ -22,12 +22,16 @@ function isValidInput (text) {
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' },
@@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) {
}
InputNumber.prototype.render = function () {
- const { unitLabel, step = 1, placeholder, value = 0 } = this.props
+ const { unitLabel, step = 1, placeholder, value } = this.props
return h('div.customize-gas-input-wrapper', {}, [
h('input', {
@@ -63,11 +67,11 @@ InputNumber.prototype.render = function () {
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', {
- onClick: () => this.setValue(addCurrencies(value, step)),
+ onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
}),
h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' },
- onClick: () => this.setValue(subtractCurrencies(value, step)),
+ onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })),
}),
]),
])
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
new file mode 100644
index 000000000..5a9f0f289
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../button'
+import { addressSummary } from '../../../util'
+import Identicon from '../../identicon'
+import genAccountLink from '../../../../lib/account-link'
+
+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())
+ }
+
+ renderSelectedAccount () {
+ const { identity } = this.props
+ return (
+ <div className="modal-container__account">
+ <div className="modal-container__account__identicon">
+ <Identicon
+ address={identity.address}
+ diameter={32}
+ />
+ </div>
+ <div className="modal-container__account__name">
+ <span className="modal-container__account__label">Name</span>
+ <span className="account_value">{identity.name}</span>
+ </div>
+ <div className="modal-container__account__address">
+ <span className="modal-container__account__label">Public Address</span>
+ <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
+ </div>
+ <div className="modal-container__account__link">
+ <a
+ className=""
+ href={genAccountLink(identity.address, this.props.network)}
+ target={'_blank'}
+ title={this.context.t('etherscanView')}
+ >
+ <img src="images/popout.svg" />
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="modal-container">
+ <div className="modal-container__content">
+ <div className="modal-container__title">
+ { `${t('removeAccount')}` }?
+ </div>
+ { this.renderSelectedAccount() }
+ <div className="modal-container__description">
+ { t('removeAccountDescription') }
+ <a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a>
+ </div>
+ </div>
+ <div className="modal-container__footer">
+ <Button
+ type="default"
+ className="modal-container__footer-button"
+ onClick={() => this.props.hideModal()}
+ >
+ { t('nevermind') }
+ </Button>
+ <Button
+ type="secondary"
+ className="modal-container__footer-button"
+ onClick={() => this.handleRemove()}
+ >
+ { t('remove') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default ConfirmRemoveAccount
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
new file mode 100644
index 000000000..4b194c995
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmRemoveAccount from './confirm-remove-account.component'
+
+const { hideModal, removeAccount } = require('../../../actions')
+
+const mapStateToProps = state => {
+ return {
+ identity: state.appState.modal.modalState.props.identity,
+ network: state.metamask.network,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ removeAccount: (address) => dispatch(removeAccount(address)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount)
diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js
new file mode 100644
index 000000000..9763fbe05
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/index.js
@@ -0,0 +1,2 @@
+import ConfirmRemoveAccount from './confirm-remove-account.container'
+module.exports = ConfirmRemoveAccount
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
new file mode 100644
index 000000000..0337c5413
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/customize-gas.component.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import GasModalCard from '../../customize-gas-modal/gas-modal-card'
+import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
+
+import {
+ getDecimalGasLimit,
+ getDecimalGasPrice,
+ getPrefixedHexGasLimit,
+ getPrefixedHexGasPrice,
+} from './customize-gas.util'
+
+export default class CustomizeGas extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object.isRequired,
+ hideModal: PropTypes.func,
+ validate: PropTypes.func,
+ onSubmit: PropTypes.func,
+ }
+
+ state = {
+ gasPrice: 0,
+ gasLimit: 0,
+ originalGasPrice: 0,
+ originalGasLimit: 0,
+ }
+
+ componentDidMount () {
+ const { txData = {} } = this.props
+ const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData
+
+ const gasLimit = getDecimalGasLimit(hexGasLimit)
+ const gasPrice = getDecimalGasPrice(hexGasPrice)
+
+ this.setState({
+ gasPrice,
+ gasLimit,
+ originalGasPrice: gasPrice,
+ originalGasLimit: gasLimit,
+ })
+ }
+
+ handleRevert () {
+ const { originalGasPrice, originalGasLimit } = this.state
+
+ this.setState({
+ gasPrice: originalGasPrice,
+ gasLimit: originalGasLimit,
+ })
+ }
+
+ handleSave () {
+ const { onSubmit, hideModal } = this.props
+ const { gasLimit, gasPrice } = this.state
+ const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice)
+ const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit)
+
+ Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit }))
+ .then(() => hideModal())
+ }
+
+ validate () {
+ const { gasLimit, gasPrice } = this.state
+ return this.props.validate({
+ gasPrice: getPrefixedHexGasPrice(gasPrice),
+ gasLimit: getPrefixedHexGasLimit(gasLimit),
+ })
+ }
+
+ render () {
+ const { t } = this.context
+ const { hideModal } = this.props
+ const { gasPrice, gasLimit } = this.state
+ const { valid, errorKey } = this.validate()
+
+ return (
+ <div className="customize-gas">
+ <div className="customize-gas__content">
+ <div className="customize-gas__header">
+ <div className="customize-gas__title">
+ { this.context.t('customGas') }
+ </div>
+ <div
+ className="customize-gas__close"
+ onClick={() => hideModal()}
+ />
+ </div>
+ <div className="customize-gas__body">
+ <GasModalCard
+ value={gasPrice}
+ min={MIN_GAS_PRICE_GWEI}
+ step={1}
+ onChange={value => this.setState({ gasPrice: value })}
+ title={t('gasPrice')}
+ copy={t('gasPriceCalculation')}
+ />
+ <GasModalCard
+ value={gasLimit}
+ min={1}
+ step={1}
+ onChange={value => this.setState({ gasLimit: value })}
+ title={t('gasLimit')}
+ copy={t('gasLimitCalculation')}
+ />
+ </div>
+ <div className="customize-gas__footer">
+ { !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> }
+ <div
+ className="customize-gas__revert"
+ onClick={() => this.handleRevert()}
+ >
+ { t('revert') }
+ </div>
+ <div className="customize-gas__buttons">
+ <button
+ className="btn-default customize-gas__cancel"
+ onClick={() => hideModal()}
+ style={{ marginRight: '10px' }}
+ >
+ { t('cancel') }
+ </button>
+ <button
+ className="btn-primary customize-gas__save"
+ onClick={() => this.handleSave()}
+ style={{ marginRight: '10px' }}
+ disabled={!valid}
+ >
+ { t('save') }
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/modals/customize-gas/customize-gas.container.js
new file mode 100644
index 000000000..46a799795
--- /dev/null
+++ b/ui/app/components/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 '../../../actions'
+
+const mapStateToProps = state => {
+ const { appState: { modal: { modalState: { props } } } } = state
+ const { txData, onSubmit, validate } = props
+
+ return {
+ txData,
+ onSubmit,
+ validate,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas)
diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/modals/customize-gas/customize-gas.util.js
new file mode 100644
index 000000000..6ba4a7705
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/customize-gas.util.js
@@ -0,0 +1,34 @@
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil } from '../../../conversion-util'
+
+export function getDecimalGasLimit (hexGasLimit) {
+ return conversionUtil(hexGasLimit, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+}
+
+export function getDecimalGasPrice (hexGasPrice) {
+ return conversionUtil(hexGasPrice, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+}
+
+export function getPrefixedHexGasLimit (gasLimit) {
+ return ethUtil.addHexPrefix(conversionUtil(gasLimit, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ }))
+}
+
+export function getPrefixedHexGasPrice (gasPrice) {
+ return ethUtil.addHexPrefix(conversionUtil(gasPrice, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ }))
+}
diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/modals/customize-gas/index.js
new file mode 100644
index 000000000..3a0ab7edc
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/index.js
@@ -0,0 +1 @@
+export { default } from './customize-gas.container'
diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/modals/customize-gas/index.scss
new file mode 100644
index 000000000..e10452691
--- /dev/null
+++ b/ui/app/components/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/modals/index.scss b/ui/app/components/modals/index.scss
index ad6fe16d3..e198cca44 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -1,3 +1,5 @@
+@import './customize-gas/index';
+
.modal-container {
width: 100%;
height: 100%;
@@ -18,6 +20,58 @@
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;
+ }
+
&__content {
overflow-y: auto;
flex: 1;
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 85e85597a..f59825ed1 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -20,10 +20,13 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account')
+const ConfirmRemoveAccount = require('./confirm-remove-account')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
+import ConfirmCustomizeGasModal from './customize-gas'
+
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
border: '1px solid #CCCFD1',
@@ -241,6 +244,19 @@ const MODALS = {
},
},
+ CONFIRM_REMOVE_ACCOUNT: {
+ contents: h(ConfirmRemoveAccount),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
NEW_ACCOUNT: {
contents: [
h(NewAccountModal, {}, []),
@@ -267,7 +283,31 @@ const MODALS = {
CUSTOMIZE_GAS: {
contents: [
- h(CustomizeGasModal, {}, []),
+ h(CustomizeGasModal),
+ ],
+ mobileModalStyle: {
+ width: '100vw',
+ height: '100vh',
+ top: '0',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '720px',
+ height: '377px',
+ top: '80px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ },
+
+ CONFIRM_CUSTOMIZE_GAS: {
+ contents: [
+ h(ConfirmCustomizeGasModal),
],
mobileModalStyle: {
width: '100vw',
diff --git a/ui/app/components/network-display.js b/ui/app/components/network-display.js
deleted file mode 100644
index 59719d9a4..000000000
--- a/ui/app/components/network-display.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
-
-const networkToColorHash = {
- 1: '#038789',
- 3: '#e91550',
- 42: '#690496',
- 4: '#ebb33f',
-}
-
-class NetworkDisplay extends Component {
- renderNetworkIcon () {
- const { network } = this.props
- const networkColor = networkToColorHash[network]
-
- return networkColor
- ? h(NetworkDropdownIcon, { backgroundColor: networkColor })
- : h('i.fa.fa-question-circle.fa-med', {
- style: {
- margin: '0 4px',
- color: 'rgb(125, 128, 130)',
- },
- })
- }
-
- render () {
- const { provider: { type } } = this.props
- return h('.network-display__container', [
- this.renderNetworkIcon(),
- h('.network-name', this.context.t(type)),
- ])
- }
-}
-
-NetworkDisplay.propTypes = {
- network: PropTypes.string,
- provider: PropTypes.object,
- t: PropTypes.func,
-}
-
-const mapStateToProps = ({ metamask: { network, provider } }) => {
- return {
- network,
- provider,
- }
-}
-
-NetworkDisplay.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(NetworkDisplay)
-
diff --git a/ui/app/components/network-display/index.js b/ui/app/components/network-display/index.js
new file mode 100644
index 000000000..f6878ae5b
--- /dev/null
+++ b/ui/app/components/network-display/index.js
@@ -0,0 +1,2 @@
+import NetworkDisplay from './network-display.container'
+module.exports = NetworkDisplay
diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss
new file mode 100644
index 000000000..2085cff67
--- /dev/null
+++ b/ui/app/components/network-display/index.scss
@@ -0,0 +1,54 @@
+.network-display {
+ &__container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ background-color: lighten(rgb(125, 128, 130), 45%);
+ padding: 0 10px;
+ border-radius: 4px;
+ height: 25px;
+
+ &--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/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js
new file mode 100644
index 000000000..38626af20
--- /dev/null
+++ b/ui/app/components/network-display/network-display.component.js
@@ -0,0 +1,69 @@
+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 propTypes = {
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderNetworkIcon () {
+ const { network } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+
+ render () {
+ const { network, provider: { type } } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return (
+ <div className={classnames(
+ 'network-display__container',
+ networkClass && ('network-display__container--' + networkClass)
+ )}>
+ {
+ networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+ <div className="network-display__name">
+ { this.context.t(type) }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/network-display/network-display.container.js
new file mode 100644
index 000000000..99a14fff4
--- /dev/null
+++ b/ui/app/components/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/page-container/index.js b/ui/app/components/page-container/index.js
index 415870b37..913b8c9c6 100644
--- a/ui/app/components/page-container/index.js
+++ b/ui/app/components/page-container/index.js
@@ -1 +1,4 @@
+import PageContainerHeader from './page-container-header'
+import PageContainerFooter from './page-container-footer'
export { default } from './page-container.component'
+export { PageContainerHeader, PageContainerFooter }
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss
new file mode 100644
index 000000000..06c3ef709
--- /dev/null
+++ b/ui/app/components/page-container/index.scss
@@ -0,0 +1,186 @@
+.page-container {
+ width: 408px;
+ background-color: $white;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ z-index: 25;
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+
+ &__header {
+ display: flex;
+ flex-flow: column;
+ border-bottom: 1px solid $geyser;
+ padding: 16px;
+ flex: 0 0 auto;
+ position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
+ }
+
+ &__header-close {
+ color: $tundora;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ cursor: pointer;
+ overflow: hidden;
+
+ &::after {
+ content: '\00D7';
+ font-size: 40px;
+ line-height: 20px;
+ }
+ }
+
+ &__header-row {
+ padding-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__footer {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ border-top: 1px solid $geyser;
+ padding: 16px;
+ flex: 0 0 auto;
+
+ .btn-default,
+ .btn-confirm {
+ font-size: 1rem;
+ }
+ }
+
+ &__footer-button {
+ height: 55px;
+ font-size: 1rem;
+ text-transform: uppercase;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ }
+
+ &__title {
+ color: $black;
+ font-size: 2rem;
+ font-weight: 500;
+ line-height: 2rem;
+ }
+
+ &__subtitle {
+ padding-top: .5rem;
+ line-height: initial;
+ font-size: .9rem;
+ color: $gray;
+ }
+
+ &__tabs {
+ display: flex;
+ margin-top: 16px;
+ }
+
+ &__tab {
+ min-width: 5rem;
+ padding: 8px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 1rem;
+ text-align: center;
+ cursor: pointer;
+ border-bottom: none;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ &--selected {
+ color: $curious-blue;
+ border-bottom: 3px solid $curious-blue;
+ }
+ }
+
+ &--full-width {
+ width: 100% !important;
+ }
+
+ &--full-height {
+ height: 100% !important;
+ max-height: initial !important;
+ min-height: initial !important;
+ }
+
+ &__content {
+ overflow-y: auto;
+ flex: 1;
+ }
+
+ &__warning-container {
+ background: $linen;
+ padding: 20px;
+ display: flex;
+ align-items: start;
+ }
+
+ &__warning-message {
+ padding-left: 15px;
+ }
+
+ &__warning-title {
+ font-weight: 500;
+ }
+
+ &__warning-icon {
+ padding-top: 5px;
+ }
+}
+
+@media screen and (max-width: 250px) {
+ .page-container {
+ &__footer {
+ flex-flow: column-reverse;
+ }
+
+ &__footer-button {
+ width: 100%;
+ margin-bottom: 1rem;
+ margin-right: 0;
+
+ &:first-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 575px) {
+ .page-container {
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ background-color: $white;
+ border-radius: 0;
+ flex: 1;
+ }
+}
+
+@media screen and (min-width: 576px) {
+ .page-container {
+ max-height: 82vh;
+ min-height: 570px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
index 0458ae78a..3d15df294 100644
--- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
+++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
@@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component {
onSubmit: PropTypes.func,
submitText: PropTypes.string,
disabled: PropTypes.bool,
+ submitButtonType: PropTypes.string,
}
static contextTypes = {
@@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component {
onSubmit,
submitText,
disabled,
+ submitButtonType,
} = this.props
return (
@@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {
<Button
type="default"
- large={true}
+ large
className="page-container__footer-button"
- onClick={() => onCancel()}
+ onClick={e => onCancel(e)}
>
{ cancelText || this.context.t('cancel') }
</Button>
<Button
- type="primary"
- large={true}
+ type={submitButtonType || 'primary'}
+ large
className="page-container__footer-button"
disabled={disabled}
onClick={e => onSubmit(e)}
diff --git a/ui/app/components/page-container/page-container-header.component.js b/ui/app/components/page-container/page-container-header.component.js
deleted file mode 100644
index 5c9d63221..000000000
--- a/ui/app/components/page-container/page-container-header.component.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-
-export default class PageContainerHeader extends Component {
-
- static propTypes = {
- title: PropTypes.string,
- subtitle: PropTypes.string,
- onClose: PropTypes.func,
- };
-
- render () {
- const { title, subtitle, onClose } = this.props
-
- return (
- <div className="page-container__header">
-
- <div className="page-container__title">
- {title}
- </div>
-
- <div className="page-container__subtitle">
- {subtitle}
- </div>
-
- <div
- className="page-container__header-close"
- onClick={() => onClose()}
- />
-
- </div>
- )
- }
-
-}
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js
index 28882edce..5a5de1e5a 100644
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ b/ui/app/components/page-container/page-container-header/page-container-header.component.js
@@ -4,13 +4,14 @@ import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component {
static propTypes = {
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
subtitle: PropTypes.string,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
+ children: PropTypes.node,
};
renderHeaderRow () {
@@ -30,25 +31,33 @@ export default class PageContainerHeader extends Component {
}
render () {
- const { title, subtitle, onClose } = this.props
+ const { title, subtitle, onClose, children } = this.props
return (
<div className="page-container__header">
{ this.renderHeaderRow() }
- <div className="page-container__title">
- {title}
- </div>
+ { children }
- <div className="page-container__subtitle">
- {subtitle}
- </div>
+ {
+ title && <div className="page-container__title">
+ { title }
+ </div>
+ }
- <div
- className="page-container__header-close"
- onClick={() => onClose()}
- />
+ {
+ subtitle && <div className="page-container__subtitle">
+ { subtitle }
+ </div>
+ }
+
+ {
+ onClose && <div
+ className="page-container__header-close"
+ onClick={() => onClose()}
+ />
+ }
</div>
)
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js
new file mode 100644
index 000000000..b71eaa1d4
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js
@@ -0,0 +1,21 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+
+export default class ConfirmApprove extends Component {
+ static propTypes = {
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ }
+
+ render () {
+ const { tokenAmount, tokenSymbol } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ tokenAmount={tokenAmount}
+ warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
new file mode 100644
index 000000000..4ef9f4ced
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import ConfirmApprove from './confirm-approve.component'
+import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
+ const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ tokenSymbol,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmApprove)
diff --git a/ui/app/components/pages/confirm-approve/index.js b/ui/app/components/pages/confirm-approve/index.js
new file mode 100644
index 000000000..791297be7
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-approve.container'
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
new file mode 100644
index 000000000..9bc0daab9
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
@@ -0,0 +1,64 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+
+export default class ConfirmDeployContract extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object,
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ origin,
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('origin')}:` }
+ </div>
+ <div>
+ { origin }
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('bytes')}:` }
+ </div>
+ <div>
+ { ethUtil.toBuffer(data).length }
+ </div>
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-box-label">
+ { `${t('hexData')}:` }
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('contractDeployment')}
+ dataComponent={this.renderData()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
new file mode 100644
index 000000000..336ee83ea
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ConfirmDeployContract from './confirm-deploy-contract.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData } = {} } = state
+
+ return {
+ txData,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmDeployContract)
diff --git a/ui/app/components/pages/confirm-deploy-contract/index.js b/ui/app/components/pages/confirm-deploy-contract/index.js
new file mode 100644
index 000000000..c4fb01b52
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-deploy-contract.container'
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
new file mode 100644
index 000000000..442a478b8
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
@@ -0,0 +1,39 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import { SEND_ROUTE } from '../../../routes'
+
+export default class ConfirmSendEther extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ editTransaction: PropTypes.func,
+ history: PropTypes.object,
+ txParams: PropTypes.object,
+ }
+
+ handleEdit ({ txData }) {
+ const { editTransaction, history } = this.props
+ editTransaction(txData)
+ history.push(SEND_ROUTE)
+ }
+
+ shouldHideData () {
+ const { txParams = {} } = this.props
+ return !txParams.data
+ }
+
+ render () {
+ const hideData = this.shouldHideData()
+
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('confirm')}
+ hideData={hideData}
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
new file mode 100644
index 000000000..e48ef54a8
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import { updateSend } from '../../../actions'
+import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
+import ConfirmSendEther from './confirm-send-ether.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData: { txParams } = {} } } = state
+
+ return {
+ txParams,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: txData => {
+ const { id, txParams } = txData
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ value: amount,
+ } = txParams
+
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ }))
+
+ dispatch(clearConfirmTransaction())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendEther)
diff --git a/ui/app/components/pages/confirm-send-ether/index.js b/ui/app/components/pages/confirm-send-ether/index.js
new file mode 100644
index 000000000..2d5767c39
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-ether.container'
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
new file mode 100644
index 000000000..cb39e3d7b
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
@@ -0,0 +1,29 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import { SEND_ROUTE } from '../../../routes'
+
+export default class ConfirmSendToken extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ editTransaction: PropTypes.func,
+ tokenAmount: PropTypes.number,
+ }
+
+ handleEdit (confirmTransactionData) {
+ const { editTransaction, history } = this.props
+ editTransaction(confirmTransactionData)
+ history.push(SEND_ROUTE)
+ }
+
+ render () {
+ const { tokenAmount } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ tokenAmount={tokenAmount}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
new file mode 100644
index 000000000..d60911e59
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
@@ -0,0 +1,52 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import ConfirmSendToken from './confirm-send-token.component'
+import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
+import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
+import { conversionUtil } from '../../../conversion-util'
+import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: ({ txData, tokenData, tokenProps }) => {
+ const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
+ const { params = [] } = tokenData
+ const { value: to } = params[0] || {}
+ const { value: tokenAmountInDec } = params[1] || {}
+ const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+ dispatch(setSelectedToken(tokenAddress))
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount: tokenAmountInHex,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ token: {
+ ...tokenProps,
+ address: tokenAddress,
+ },
+ }))
+ dispatch(clearConfirmTransaction())
+ dispatch(showSendTokenPage())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendToken)
diff --git a/ui/app/components/pages/confirm-send-token/index.js b/ui/app/components/pages/confirm-send-token/index.js
new file mode 100644
index 000000000..409b6ef3d
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-token.container'
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
new file mode 100644
index 000000000..acaed383a
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
@@ -0,0 +1,88 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import {
+ formatCurrency,
+ convertTokenToFiat,
+ addFiat,
+ roundExponential,
+} from '../../../helpers/confirm-transaction/util'
+
+export default class ConfirmTokenTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ contractExchangeRate: PropTypes.number,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ }
+
+ getFiatTransactionAmount () {
+ const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
+
+ return convertTokenToFiat({
+ value: tokenAmount,
+ toCurrency: currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ })
+ }
+
+ getSubtitle () {
+ const { currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return this.context.t('noConversionRateAvailable')
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount)
+ return formatCurrency(roundedFiatTransactionAmount, currentCurrency)
+ }
+ }
+
+ getFiatTotalTextOverride () {
+ const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return formatCurrency(fiatTransactionTotal, currentCurrency)
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
+ const roundedFiatTotal = roundExponential(fiatTotal)
+ return formatCurrency(roundedFiatTotal, currentCurrency)
+ }
+ }
+
+ render () {
+ const {
+ toAddress,
+ tokenAddress,
+ tokenSymbol,
+ tokenAmount,
+ ethTransactionTotal,
+ ...restProps
+ } = this.props
+
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={tokensText}
+ subtitle={this.getSubtitle()}
+ ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`}
+ fiatTotalTextOverride={this.getFiatTotalTextOverride()}
+ {...restProps}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
new file mode 100644
index 000000000..be38acdb0
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux'
+import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
+import {
+ tokenAmountAndToAddressSelector,
+ contractExchangeRateSelector,
+} from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = (state, ownProps) => {
+ const { tokenAmount: ownTokenAmount } = ownProps
+ const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
+ const {
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ tokenProps: { tokenSymbol } = {},
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ } = confirmTransaction
+
+ const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
+ const contractExchangeRate = contractExchangeRateSelector(state)
+
+ return {
+ toAddress,
+ tokenAddress,
+ tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
+ tokenSymbol,
+ currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTokenTransactionBase)
diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/components/pages/confirm-token-transaction-base/index.js
new file mode 100644
index 000000000..e15c5d56b
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/index.js
@@ -0,0 +1,2 @@
+export { default } from './confirm-token-transaction-base.container'
+export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component'
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
new file mode 100644
index 000000000..b170880b4
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,345 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
+import { formatCurrency } from '../../../helpers/confirm-transaction/util'
+import { isBalanceSufficient } from '../../send/send.utils'
+import { DEFAULT_ROUTE } from '../../../routes'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ TRANSACTION_ERROR_KEY,
+} from '../../../constants/error-keys'
+
+export default class ConfirmTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ // react-router props
+ match: PropTypes.object,
+ history: PropTypes.object,
+ // Redux props
+ balance: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ clearConfirmTransaction: PropTypes.func,
+ clearSend: PropTypes.func,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ editTransaction: PropTypes.func,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ hexGasTotal: PropTypes.string,
+ isTxReprice: PropTypes.bool,
+ methodData: PropTypes.object,
+ nonce: PropTypes.string,
+ sendTransaction: PropTypes.func,
+ showCustomizeGasModal: PropTypes.func,
+ showTransactionConfirmedModal: PropTypes.func,
+ toAddress: PropTypes.string,
+ tokenData: PropTypes.object,
+ tokenProps: PropTypes.object,
+ toName: PropTypes.string,
+ transactionStatus: PropTypes.string,
+ txData: PropTypes.object,
+ // Component props
+ action: PropTypes.string,
+ contentComponent: PropTypes.node,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ ethTotalTextOverride: PropTypes.string,
+ fiatTotalTextOverride: PropTypes.string,
+ hideData: PropTypes.bool,
+ hideDetails: PropTypes.bool,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ onCancel: PropTypes.func,
+ onEdit: PropTypes.func,
+ onEditGas: PropTypes.func,
+ onSubmit: PropTypes.func,
+ subtitle: PropTypes.string,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.string,
+ valid: PropTypes.bool,
+ warning: PropTypes.string,
+ }
+
+ state = {
+ submitting: false,
+ }
+
+ componentDidUpdate () {
+ const {
+ transactionStatus,
+ showTransactionConfirmedModal,
+ history,
+ clearConfirmTransaction,
+ } = this.props
+
+ if (transactionStatus === 'dropped') {
+ showTransactionConfirmedModal({
+ onHide: () => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+
+ return
+ }
+ }
+
+ getErrorKey () {
+ const {
+ balance,
+ conversionRate,
+ hexGasTotal,
+ txData: {
+ simulationFails,
+ txParams: {
+ value: amount,
+ } = {},
+ } = {},
+ } = this.props
+
+ const insufficientBalance = balance && !isBalanceSufficient({
+ amount,
+ gasTotal: hexGasTotal || '0x0',
+ balance,
+ conversionRate,
+ })
+
+ if (insufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ if (simulationFails) {
+ return {
+ valid: false,
+ errorKey: TRANSACTION_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+
+ handleEditGas () {
+ const { onEditGas, showCustomizeGasModal } = this.props
+
+ if (onEditGas) {
+ onEditGas()
+ } else {
+ showCustomizeGasModal()
+ }
+ }
+
+ renderDetails () {
+ const {
+ detailsComponent,
+ fiatTransactionFee,
+ ethTransactionFee,
+ currentCurrency,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ fiatTotalTextOverride,
+ ethTotalTextOverride,
+ hideDetails,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency)
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ fiatText={formatCurrency(fiatTransactionFee, currentCurrency)}
+ ethText={`\u2666 ${ethTransactionFee}`}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ />
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ fiatText={fiatTotalTextOverride || formattedCurrency}
+ ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ fiatTextColor="#2f9ae0"
+ />
+ </div>
+ </div>
+ )
+ )
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ methodData: {
+ name,
+ params,
+ } = {},
+ hideData,
+ dataComponent,
+ } = this.props
+
+ if (hideData) {
+ return null
+ }
+
+ return dataComponent || (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('functionType')}:`}
+ <span className="confirm-page-container-content__function-type">
+ { name || t('notFound') }
+ </span>
+ </div>
+ {
+ params && (
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('parameters')}:` }
+ </div>
+ <div>
+ <pre>{ JSON.stringify(params, null, 2) }</pre>
+ </div>
+ </div>
+ )
+ }
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('hexData')}:`}
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ handleEdit () {
+ const { txData, tokenData, tokenProps, onEdit } = this.props
+ onEdit({ txData, tokenData, tokenProps })
+ }
+
+ handleCancel () {
+ const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
+
+ if (onCancel) {
+ onCancel(txData)
+ } else {
+ cancelTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ handleSubmit () {
+ const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
+ const { submitting } = this.state
+
+ if (submitting) {
+ return
+ }
+
+ this.setState({ submitting: true })
+
+ if (onSubmit) {
+ Promise.resolve(onSubmit(txData))
+ .then(this.setState({ submitting: false }))
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ this.setState({ submitting: false })
+ history.push(DEFAULT_ROUTE)
+ })
+ .catch(() => this.setState({ submitting: false }))
+ }
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ ethTransactionAmount,
+ fiatTransactionAmount,
+ valid: propsValid = true,
+ errorMessage,
+ errorKey: propsErrorKey,
+ currentCurrency,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ warning,
+ } = this.props
+ const { submitting } = this.state
+
+ const { name } = methodData
+ const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
+ const { valid, errorKey } = this.getErrorKey()
+
+ return (
+ <ConfirmPageContainer
+ fromName={fromName}
+ fromAddress={fromAddress}
+ toName={toName}
+ toAddress={toAddress}
+ showEdit={onEdit && !isTxReprice}
+ action={action || name || this.context.t('unknownFunction')}
+ title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`}
+ subtitle={subtitle || `\u2666 ${ethTransactionAmount}`}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={this.renderDetails()}
+ dataComponent={this.renderData()}
+ contentComponent={contentComponent}
+ nonce={nonce}
+ identiconAddress={identiconAddress}
+ errorMessage={errorMessage}
+ errorKey={propsErrorKey || errorKey}
+ warning={warning}
+ disabled={!propsValid || !valid || submitting}
+ onEdit={() => this.handleEdit()}
+ onCancel={() => this.handleCancel()}
+ onSubmit={() => this.handleSubmit()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
new file mode 100644
index 000000000..0c0deff18
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,181 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+import contractMap from 'eth-contract-metadata'
+import ConfirmTransactionBase from './confirm-transaction-base.component'
+import {
+ clearConfirmTransaction,
+ updateGasAndCalculate,
+} from '../../../ducks/confirm-transaction.duck'
+import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
+} from '../../../constants/error-keys'
+import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
+import { isBalanceSufficient } from '../../send/send.utils'
+import { conversionGreaterThan } from '../../../conversion-util'
+import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
+import { addressSlicer } from '../../../util'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const mapStateToProps = (state, props) => {
+ const { toAddress: propsToAddress } = props
+ const { confirmTransaction, metamask } = state
+ const {
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexGasTotal,
+ tokenData,
+ methodData,
+ txData,
+ tokenProps,
+ nonce,
+ } = confirmTransaction
+ const { txParams = {}, lastGasPrice, id: transactionId } = txData
+ const { from: fromAddress, to: txParamsToAddress } = txParams
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ accounts,
+ selectedAddress,
+ selectedAddressTxList,
+ } = metamask
+
+ const { balance } = accounts[selectedAddress]
+ const { name: fromName } = identities[selectedAddress]
+ const toAddress = propsToAddress || txParamsToAddress
+ const toName = identities[toAddress]
+ ? identities[toAddress].name
+ : casedContractMap[toAddress] ? casedContractMap[toAddress].name : addressSlicer(toAddress)
+
+ const isTxReprice = Boolean(lastGasPrice)
+
+ const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ return {
+ balance,
+ fromAddress,
+ fromName,
+ toAddress,
+ toName,
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexGasTotal,
+ txData,
+ tokenData,
+ methodData,
+ tokenProps,
+ isTxReprice,
+ currentCurrency,
+ conversionRate,
+ transactionStatus,
+ nonce,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ clearSend: () => dispatch(clearSend()),
+ showTransactionConfirmedModal: ({ onHide }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide }))
+ },
+ showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
+ return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))
+ },
+ updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
+ return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
+ },
+ cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
+ sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
+ }
+}
+
+const getValidateEditGas = ({ balance, conversionRate, txData }) => {
+ const { txParams: { value: amount } = {} } = txData
+
+ return ({ gasLimit, gasPrice }) => {
+ const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
+ const hasSufficientBalance = isBalanceSufficient({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ if (!hasSufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { balance, conversionRate, txData } = stateProps
+ const {
+ showCustomizeGasModal: dispatchShowCustomizeGasModal,
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
+ txData,
+ onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
+ validate: validateEditGas,
+ }),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(ConfirmTransactionBase)
diff --git a/ui/app/components/pages/confirm-transaction-base/index.js b/ui/app/components/pages/confirm-transaction-base/index.js
new file mode 100644
index 000000000..9996e9aeb
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-transaction-base.container'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
new file mode 100644
index 000000000..0280f73c6
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Redirect } from 'react-router-dom'
+import Loading from '../../loading-screen'
+import {
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../../routes'
+import { isConfirmDeployContract } from './confirm-transaction-switch.util'
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+} from './confirm-transaction-switch.constants'
+
+export default class ConfirmTransactionSwitch extends Component {
+ static propTypes = {
+ txData: PropTypes.object,
+ methodData: PropTypes.object,
+ fetchingMethodData: PropTypes.bool,
+ }
+
+ redirectToTransaction () {
+ const {
+ txData,
+ methodData: { name },
+ fetchingMethodData,
+ } = this.props
+ const { id, txParams: { data } = {} } = txData
+
+ if (isConfirmDeployContract(txData)) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (fetchingMethodData) {
+ return <Loading />
+ }
+
+ if (data) {
+ const methodName = name && name.toLowerCase()
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_APPROVE: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_TRANSFER_FROM: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ default: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ }
+ }
+
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ render () {
+ const { txData } = this.props
+
+ if (txData.txParams) {
+ return this.redirectToTransaction()
+ } else if (txData.msgParams) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ return <Loading />
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
new file mode 100644
index 000000000..9db4a2f96
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
@@ -0,0 +1,3 @@
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
+export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
new file mode 100644
index 000000000..3d7fc78cc
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmTransactionSwitch from './confirm-transaction-switch.component'
+
+const mapStateToProps = state => {
+ const {
+ confirmTransaction: {
+ txData,
+ methodData,
+ fetchingMethodData,
+ },
+ } = state
+
+ return {
+ txData,
+ methodData,
+ fetchingMethodData,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTransactionSwitch)
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
new file mode 100644
index 000000000..536aa5212
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
@@ -0,0 +1,4 @@
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
diff --git a/ui/app/components/pages/confirm-transaction-switch/index.js b/ui/app/components/pages/confirm-transaction-switch/index.js
new file mode 100644
index 000000000..c288acb1a
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
+module.exports = ConfirmTransactionSwitch
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
new file mode 100644
index 000000000..3ac656d73
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
@@ -0,0 +1,157 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import Loading from '../../loading-screen'
+import ConfirmTransactionSwitch from '../confirm-transaction-switch'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import ConfirmSendEther from '../confirm-send-ether'
+import ConfirmSendToken from '../confirm-send-token'
+import ConfirmDeployContract from '../confirm-deploy-contract'
+import ConfirmApprove from '../confirm-approve'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import ConfTx from '../../../conf-tx'
+import {
+ DEFAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../../routes'
+
+export default class ConfirmTransaction extends Component {
+ static propTypes = {
+ history: PropTypes.object.isRequired,
+ totalUnapprovedCount: PropTypes.number.isRequired,
+ match: PropTypes.object,
+ send: PropTypes.object,
+ unconfirmedTransactions: PropTypes.array,
+ setTransactionToConfirm: PropTypes.func,
+ confirmTransaction: PropTypes.object,
+ clearConfirmTransaction: PropTypes.func,
+ }
+
+ getParamsTransactionId () {
+ const { match: { params: { id } = {} } } = this.props
+ return id || null
+ }
+
+ componentDidMount () {
+ const {
+ totalUnapprovedCount = 0,
+ send = {},
+ history,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ } = this.props
+
+ if (!totalUnapprovedCount && !send.to) {
+ history.replace(DEFAULT_ROUTE)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ componentDidUpdate () {
+ const {
+ setTransactionToConfirm,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ clearConfirmTransaction,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
+ clearConfirmTransaction()
+ setTransactionToConfirm(paramsTransactionId)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ setTransactionToConfirm () {
+ const {
+ history,
+ unconfirmedTransactions,
+ setTransactionToConfirm,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId) {
+ // Check to make sure params ID is valid
+ const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
+
+ if (!tx) {
+ history.replace(DEFAULT_ROUTE)
+ } else {
+ setTransactionToConfirm(paramsTransactionId)
+ }
+ } else if (unconfirmedTransactions.length) {
+ const totalUnconfirmed = unconfirmedTransactions.length
+ const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
+ const { id: transactionId, loadingDefaults } = transaction
+
+ if (!loadingDefaults) {
+ setTransactionToConfirm(transactionId)
+ }
+ }
+ }
+
+ render () {
+ const { confirmTransaction: { txData: { id } } = {} } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ // Show routes when state.confirmTransaction has been set and when either the ID in the params
+ // isn't specified or is specified and matches the ID in state.confirmTransaction in order to
+ // support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
+ return id && (!paramsTransactionId || paramsTransactionId === id + '')
+ ? (
+ <Switch>
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
+ component={ConfirmDeployContract}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
+ component={ConfirmTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
+ component={ConfirmSendEther}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
+ component={ConfirmSendToken}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
+ component={ConfirmApprove}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
+ component={ConfirmTokenTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
+ component={ConfTx}
+ />
+ <Route path="*" component={ConfirmTransactionSwitch} />
+ </Switch>
+ )
+ : <Loading />
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
new file mode 100644
index 000000000..1bc2f1efb
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import {
+ setTransactionToConfirm,
+ clearConfirmTransaction,
+} from '../../../ducks/confirm-transaction.duck'
+import ConfirmTransaction from './confirm-transaction.component'
+import { getTotalUnapprovedCount } from '../../../selectors'
+import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask: { send }, confirmTransaction } = state
+
+ return {
+ totalUnapprovedCount: getTotalUnapprovedCount(state),
+ send,
+ confirmTransaction,
+ unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+)(ConfirmTransaction)
diff --git a/ui/app/components/pages/confirm-transaction/index.js b/ui/app/components/pages/confirm-transaction/index.js
new file mode 100644
index 000000000..4bf42d85c
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransaction from './confirm-transaction.container'
+module.exports = ConfirmTransaction
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..c722d1f55
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,143 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const genAccountLink = require('../../../../../lib/account-link.js')
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
+ h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
+ ])
+ )
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ [
+ h('span.hw-account-list__item__index', a.index + 1),
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
+ h('span.hw-account-list__item__balance', `${a.balance}`),
+ ]),
+ ]),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(-1),
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(1),
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ const disabled = this.props.selectedAccount === null
+ const buttonProps = {}
+ if (disabled) {
+ buttonProps.disabled = true
+ }
+
+ return h('div.new-account-connect-form__buttons', {}, [
+ h(
+ 'button.btn-default.btn--large.new-account-connect-form__button',
+ {
+ onClick: this.props.onCancel.bind(this),
+ },
+ [this.context.t('cancel')]
+ ),
+
+ h(
+ `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
+ {
+ onClick: this.props.onUnlockAccount.bind(this),
+ ...buttonProps,
+ },
+ [this.context.t('unlock')]
+ ),
+ ])
+ }
+
+ renderForgetDevice () {
+ return h('div.hw-forget-device-container', {}, [
+ h('a', {
+ onClick: this.props.onForgetDevice.bind(this),
+ }, this.context.t('forgetDevice')),
+ ])
+ }
+
+ render () {
+ return h('div.new-account-connect-form.account-list', {}, [
+ this.renderHeader(),
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ this.renderForgetDevice(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ onForgetDevice: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..cb2b86595
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,149 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div.new-account-connect-form.unsupported-browser', {}, [
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ ]),
+ h(
+ 'button.btn-primary.btn--large',
+ {
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ },
+ this.context.t('downloadGoogleChrome')
+ ),
+ ])
+ )
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect__header', {}, [
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
+ ])
+ )
+ }
+
+ renderTrezorAffiliateLink () {
+ return h('div.hw-connect__get-trezor', {}, [
+ h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)),
+ h('a.hw-connect__get-trezor__link', {
+ href: 'https://shop.trezor.io/?a=metamask',
+ target: '_blank',
+ }, this.context.t('orderOneHere')),
+ ])
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ 'button.btn-primary.btn--large',
+ { onClick: this.props.connectToTrezor.bind(this) },
+ this.props.btnText
+ )
+ }
+
+ scrollToTutorial = (e) => {
+ if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
+ }
+
+ renderLearnMore () {
+ return (
+ h('p.hw-connect__learn-more', {
+ onClick: this.scrollToTutorial,
+ }, [
+ this.context.t('learnMore'),
+ h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
+ ])
+ )
+ }
+
+ renderTutorialSteps () {
+ const steps = [
+ {
+ asset: 'hardware-wallet-step-1',
+ dimensions: {width: '225px', height: '75px'},
+ },
+ {
+ asset: 'hardware-wallet-step-2',
+ dimensions: {width: '300px', height: '100px'},
+ },
+ {
+ asset: 'hardware-wallet-step-3',
+ dimensions: {width: '120px', height: '90px'},
+ },
+ ]
+
+ return h('.hw-tutorial', {
+ ref: node => { this.referenceNode = node },
+ },
+ steps.map((step, i) => (
+ h('div.hw-connect', {}, [
+ h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
+ h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
+ h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
+ ])
+ ))
+ )
+ }
+
+ renderFooter () {
+ return (
+ h('div.hw-connect__footer', {}, [
+ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
+ this.renderConnectToTrezorButton(),
+ h('p.hw-connect__footer__msg', {}, [
+ this.context.t(`havingTroubleConnecting`),
+ h('a.hw-connect__footer__link', {
+ href: 'https://support.metamask.io/',
+ target: '_blank',
+ }, this.context.t('getHelp')),
+ ]),
+ ])
+ )
+ }
+
+ renderConnectScreen () {
+ return (
+ h('div.new-account-connect-form', {}, [
+ this.renderHeader(),
+ this.renderTrezorAffiliateLink(),
+ this.renderConnectToTrezorButton(),
+ this.renderLearnMore(),
+ this.renderTutorialSteps(),
+ this.renderFooter(),
+ ])
+ )
+ }
+
+ render () {
+ if (this.props.browserSupported) {
+ return this.renderConnectScreen()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToTrezor: PropTypes.func.isRequired,
+ btnText: PropTypes.string.isRequired,
+ browserSupported: PropTypes.bool.isRequired,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..3f66e7098
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,241 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../../actions')
+const ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../../routes')
+const { formatBalance } = require('../../../../util')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ btnText: context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ browserSupported: true,
+ unlocked: false,
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { accounts } = nextProps
+ const newAccounts = this.state.accounts.map(a => {
+ const normalizedAddress = a.address.toLowerCase()
+ const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
+ a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return a
+ })
+ this.setState({accounts: newAccounts})
+ }
+
+
+ componentDidMount () {
+ this.checkIfUnlocked()
+ }
+
+ async checkIfUnlocked () {
+ const unlocked = await this.props.checkHardwareStatus('trezor')
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(0)
+ }
+ }
+
+ connectToTrezor = () => {
+ if (this.state.accounts.length) {
+ return null
+ }
+ this.setState({ btnText: this.context.t('connecting')})
+ this.getPage(0)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ showTemporaryAlert () {
+ this.props.showAlert(this.context.t('hardwareWalletConnected'))
+ // Autohide the alert after 5 seconds
+ setTimeout(_ => {
+ this.props.hideAlert()
+ }, 5000)
+ }
+
+ getPage = (page) => {
+ this.props
+ .connectHardware('trezor', page)
+ .then(accounts => {
+ if (accounts.length) {
+
+ // If we just loaded the accounts for the first time
+ // (device previously locked) show the global alert
+ if (this.state.accounts.length === 0 && !this.state.unlocked) {
+ this.showTemporaryAlert()
+ }
+
+ const newState = { unlocked: true }
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ accounts.forEach((a, i) => {
+ if (a.address.toLowerCase() === this.props.address) {
+ newState.selectedAccount = a.index.toString()
+ }
+ })
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
+ newState.selectedAccount = null
+ }
+
+
+ // Map accounts with balances
+ newState.accounts = accounts.map(account => {
+ const normalizedAddress = account.address.toLowerCase()
+ const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
+ account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return account
+ })
+
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ if (e === 'Window blocked') {
+ this.setState({ browserSupported: false })
+ }
+ this.setState({ btnText: this.context.t('connectToTrezor') })
+ })
+ }
+
+ onForgetDevice = () => {
+ this.props.forgetDevice('trezor')
+ .then(_ => {
+ this.setState({
+ error: null,
+ btnText: this.context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ unlocked: false,
+ })
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onUnlockAccount = () => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockTrezorAccount(this.state.selectedAccount)
+ .then(_ => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToTrezor: this.connectToTrezor,
+ btnText: this.state.btnText,
+ browserSupported: this.state.browserSupported,
+ })
+ }
+
+ return h(AccountList, {
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onForgetDevice: this.onForgetDevice,
+ onCancel: this.onCancel,
+ })
+ }
+
+ render () {
+ return h('div', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ checkHardwareStatus: PropTypes.func,
+ forgetDevice: PropTypes.func,
+ showAlert: PropTypes.func,
+ hideAlert: PropTypes.func,
+ unlockTrezorAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+ address: PropTypes.string,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {}, accounts = [] },
+ } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ connectHardware: (deviceName, page) => {
+ return dispatch(actions.connectHardware(deviceName, page))
+ },
+ checkHardwareStatus: (deviceName) => {
+ return dispatch(actions.checkHardwareStatus(deviceName))
+ },
+ forgetDevice: (deviceName) => {
+ return dispatch(actions.forgetDevice(deviceName))
+ },
+ unlockTrezorAccount: index => {
+ return dispatch(actions.unlockTrezorAccount(index))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ showAlert: (msg) => dispatch(actions.showAlert(msg)),
+ hideAlert: () => dispatch(actions.hideAlert()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
index 1dc2ba534..dd57256a3 100644
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ b/ui/app/components/pages/create-account/import-account/json.js
@@ -109,12 +109,13 @@ class JsonImportSubview extends Component {
.then(({ selectedAddress }) => {
if (selectedAddress) {
history.push(DEFAULT_ROUTE)
+ displayWarning(null)
} else {
displayWarning('Error importing account.')
setSelectedAddress(firstAddress)
}
})
- .catch(err => displayWarning(err))
+ .catch(err => err && displayWarning(err.message || err))
}
}
diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js
index 5df3777da..1db999f2f 100644
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ b/ui/app/components/pages/create-account/import-account/private-key.js
@@ -99,10 +99,11 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
.then(({ selectedAddress }) => {
if (selectedAddress) {
history.push(DEFAULT_ROUTE)
+ displayWarning(null)
} else {
displayWarning('Error importing account.')
setSelectedAddress(firstAddress)
}
})
- .catch(err => displayWarning(err))
+ .catch(err => err && displayWarning(err.message || err))
}
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
index 6e3b93742..d3de1ea01 100644
--- a/ui/app/components/pages/create-account/index.js
+++ b/ui/app/components/pages/create-account/index.js
@@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors')
const classnames = require('classnames')
const NewAccountCreateForm = require('./new-account')
const NewAccountImportForm = require('./import-account')
-const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../../routes')
class CreateAccountPage extends Component {
renderTabs () {
@@ -36,13 +41,26 @@ class CreateAccountPage extends Component {
}, [
this.context.t('import'),
]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
])
}
render () {
return h('div.new-account', {}, [
h('div.new-account__header', [
- h('div.new-account__title', this.context.t('newAccount') ),
+ h('div.new-account__title', this.context.t('newAccount')),
this.renderTabs(),
]),
h('div.new-account__form', [
@@ -57,6 +75,11 @@ class CreateAccountPage extends Component {
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
]),
]),
])
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 9c94990e0..402b8f03b 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component {
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
@@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => {
})
},
showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
}
}
diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js
index c53413d3b..5e3fdc9af 100644
--- a/ui/app/components/pages/home.js
+++ b/ui/app/components/pages/home.js
@@ -1,6 +1,6 @@
const { Component } = require('react')
+const { connect } = require('react-redux')
const PropTypes = require('prop-types')
-const connect = require('../../metamask-connect')
const { Redirect, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
@@ -27,19 +27,17 @@ const {
NOTICE_ROUTE,
} = require('../../routes')
+const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
+
class Home extends Component {
componentDidMount () {
const {
history,
- unapprovedTxs = {},
- unapprovedMsgCount = 0,
- unapprovedPersonalMsgCount = 0,
- unapprovedTypedMessagesCount = 0,
+ unconfirmedTransactionsCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
- if (Object.keys(unapprovedTxs).length ||
- unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
+ if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
@@ -83,51 +81,6 @@ class Home extends Component {
})
}
- // if (!props.noActiveNotices) {
- // log.debug('rendering notice screen for unread notices.')
- // return h(NoticeScreen, {
- // notice: props.nextUnreadNotice,
- // key: 'NoticeScreen',
- // onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
- // })
- // } else if (props.lostAccounts && props.lostAccounts.length > 0) {
- // log.debug('rendering notice screen for lost accounts view.')
- // return h(NoticeScreen, {
- // notice: generateLostAccountsNotice(props.lostAccounts),
- // key: 'LostAccountsNotice',
- // onConfirm: () => props.dispatch(actions.markAccountsFound()),
- // })
- // }
-
- // if (props.seedWords) {
- // log.debug('rendering seed words')
- // return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
- // }
-
- // show initialize screen
- // if (!isInitialized || forgottenPassword) {
- // // show current view
- // log.debug('rendering an initialize screen')
- // // switch (props.currentView.name) {
-
- // // case 'restoreVault':
- // // log.debug('rendering restore vault screen')
- // // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
-
- // // default:
- // // log.debug('rendering menu screen')
- // // return h(InitializeScreen, {key: 'menuScreenInit'})
- // // }
- // }
-
- // // show unlock screen
- // if (!props.isUnlocked) {
- // return h(MainContainer, {
- // currentViewName: props.currentView.name,
- // isUnlocked: props.isUnlocked,
- // })
- // }
-
// show current view
switch (currentView.name) {
@@ -135,59 +88,10 @@ class Home extends Component {
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
- // case 'sendTransaction':
- // log.debug('rendering send tx screen')
-
- // // Going to leave this here until we are ready to delete SendTransactionScreen v1
- // // const SendComponentToRender = checkFeatureToggle('send-v2')
- // // ? SendTransactionScreen2
- // // : SendTransactionScreen
-
- // return h(SendTransactionScreen2, {key: 'send-transaction'})
-
- // case 'sendToken':
- // log.debug('rendering send token screen')
-
- // // Going to leave this here until we are ready to delete SendTransactionScreen v1
- // // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
- // // ? SendTransactionScreen2
- // // : SendTokenScreen
-
- // return h(SendTransactionScreen2, {key: 'sendToken'})
-
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
- // case 'confTx':
- // log.debug('rendering confirm tx screen')
- // return h(Redirect, {
- // to: {
- // pathname: CONFIRM_TRANSACTION_ROUTE,
- // },
- // })
- // return h(ConfirmTxScreen, {key: 'confirm-tx'})
-
- // case 'add-token':
- // log.debug('rendering add-token screen from unlock screen.')
- // return h(AddTokenScreen, {key: 'add-token'})
-
- // case 'config':
- // log.debug('rendering config screen')
- // return h(Settings, {key: 'config'})
-
- // case 'import-menu':
- // log.debug('rendering import screen')
- // return h(Import, {key: 'import-menu'})
-
- // case 'reveal-seed-conf':
- // log.debug('rendering reveal seed confirmation screen')
- // return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
-
- // case 'info':
- // log.debug('rendering info screen')
- // return h(Settings, {key: 'info', tab: 'info'})
-
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
@@ -261,6 +165,7 @@ Home.propTypes = {
isPopup: PropTypes.bool,
isMouseUser: PropTypes.bool,
t: PropTypes.func,
+ unconfirmedTransactionsCount: PropTypes.number,
}
function mapStateToProps (state) {
@@ -324,6 +229,7 @@ function mapStateToProps (state) {
// state needed to get account dropdown temporarily rendering from app bar
selected,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js
index 33575bfbb..d90a33e49 100644
--- a/ui/app/components/pages/keychains/restore-vault.js
+++ b/ui/app/components/pages/keychains/restore-vault.js
@@ -1,178 +1,189 @@
-const { withRouter } = require('react-router-dom')
-const PropTypes = require('prop-types')
-const { compose } = require('recompose')
-const PersistentForm = require('../../../../lib/persistent-form')
-const connect = require('../../../metamask-connect')
-const h = require('react-hyperscript')
-const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions')
-const { DEFAULT_ROUTE } = require('../../../routes')
-const log = require('loglevel')
-
-class RestoreVaultPage extends PersistentForm {
- constructor (props) {
- super(props)
-
- this.state = {
- error: null,
- }
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import {
+ createNewVaultAndRestore,
+ unMarkPasswordForgotten,
+} from '../../../actions'
+import { DEFAULT_ROUTE } from '../../../routes'
+import TextField from '../../text-field'
+
+class RestoreVaultPage extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
}
- createOnEnter (event) {
- if (event.key === 'Enter') {
- this.createNewVaultAndRestore()
- }
+ static propTypes = {
+ warning: PropTypes.string,
+ createNewVaultAndRestore: PropTypes.func.isRequired,
+ leaveImportSeedScreenState: PropTypes.func,
+ history: PropTypes.object,
+ isLoading: PropTypes.bool,
+ };
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: null,
+ passwordError: null,
+ confirmPasswordError: null,
}
- cancel () {
- this.props.unMarkPasswordForgotten()
- .then(this.props.history.push(DEFAULT_ROUTE))
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .match(/\w+/g)
+ .join(' ')
}
- createNewVaultAndRestore () {
- this.setState({ error: null })
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = null
+
+ if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ }
+
+ this.setState({ seedPhrase, seedPhraseError })
+ }
- // check password
- var passwordBox = document.getElementById('password-box')
- var password = passwordBox.value
- var passwordConfirmBox = document.getElementById('password-box-confirm')
- var passwordConfirm = passwordConfirmBox.value
+ handlePasswordChange (password) {
+ const { confirmPassword } = this.state
+ let confirmPasswordError = null
+ let passwordError = null
- if (password.length < 8) {
- this.setState({ error: 'Password not long enough' })
- return
+ if (password && password.length < 8) {
+ passwordError = this.context.t('passwordNotLongEnough')
}
- if (password !== passwordConfirm) {
- this.setState({ error: 'Passwords don\'t match' })
- return
+ if (confirmPassword && password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
}
- // check seed
- var seedBox = document.querySelector('textarea.twelve-word-phrase')
- var seed = seedBox.value.trim()
- if (seed.split(' ').length !== 12) {
- this.setState({ error: 'Seed phrases are 12 words long' })
- return
+ this.setState({ password, passwordError, confirmPasswordError })
+ }
+
+ handleConfirmPasswordChange (confirmPassword) {
+ const { password } = this.state
+ let confirmPasswordError = null
+
+ if (password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
}
- // submit
- this.props.createNewVaultAndRestore(password, seed)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
- .catch(({ message }) => {
- this.setState({ error: message })
- log.error(message)
- })
+ this.setState({ confirmPassword, confirmPasswordError })
+ }
+
+ onClick = () => {
+ const { password, seedPhrase } = this.state
+ const {
+ createNewVaultAndRestore,
+ leaveImportSeedScreenState,
+ history,
+ } = this.props
+
+ leaveImportSeedScreenState()
+ createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
+ .then(() => history.push(DEFAULT_ROUTE))
+ }
+
+ hasError () {
+ const { passwordError, confirmPasswordError, seedPhraseError } = this.state
+ return passwordError || confirmPasswordError || seedPhraseError
}
render () {
- const { error } = this.state
- this.persistentFormParentId = 'restore-vault-form'
+ const {
+ seedPhrase,
+ password,
+ confirmPassword,
+ seedPhraseError,
+ passwordError,
+ confirmPasswordError,
+ } = this.state
+ const { t } = this.context
+ const { isLoading } = this.props
+ const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
return (
- h('.initialize-screen.flex-column.flex-center.flex-grow', [
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginBottom: 24,
- width: '100%',
- fontSize: '20px',
- padding: 6,
- },
- }, [
- this.props.t('restoreVault'),
- ]),
-
- // wallet seed entry
- h('h3', 'Wallet Seed'),
- h('textarea.twelve-word-phrase.letter-spacey', {
- dataset: {
- persistentFormId: 'wallet-seed',
- },
- placeholder: this.props.t('secretPhrase'),
- }),
-
- // password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box',
- placeholder: this.props.t('newPassword8Chars'),
- dataset: {
- persistentFormId: 'password',
- },
- style: {
- width: 260,
- marginTop: 12,
- },
- }),
-
- // confirm password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box-confirm',
- placeholder: this.props.t('confirmPassword'),
- onKeyPress: this.createOnEnter.bind(this),
- dataset: {
- persistentFormId: 'password-confirmation',
- },
- style: {
- width: 260,
- marginTop: 16,
- },
- }),
-
- error && (
- h('span.error.in-progress-notification', error)
- ),
-
- // submit
- h('.flex-row.flex-space-between', {
- style: {
- marginTop: 30,
- width: '50%',
- },
- }, [
-
- // cancel
- h('button.primary', {
- onClick: () => this.cancel(),
- }, this.props.t('cancel')),
-
- // submit
- h('button.primary', {
- onClick: this.createNewVaultAndRestore.bind(this),
- }, this.props.t('ok')),
-
- ]),
- ])
+ <div className="first-view-main-wrapper">
+ <div className="first-view-main">
+ <div className="import-account">
+ <a
+ className="import-account__back-button"
+ onClick={e => {
+ e.preventDefault()
+ this.props.history.goBack()
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ <div className="import-account__title">
+ { this.context.t('restoreAccountWithSeed') }
+ </div>
+ <div className="import-account__selector-label">
+ { this.context.t('secretPhrase') }
+ </div>
+ <div className="import-account__input-wrapper">
+ <label className="import-account__input-label">Wallet Seed</label>
+ <textarea
+ className="import-account__secret-phrase"
+ onChange={e => this.handleSeedPhraseChange(e.target.value)}
+ value={this.state.seedPhrase}
+ placeholder={this.context.t('separateEachWord')}
+ />
+ </div>
+ <span className="error">
+ { seedPhraseError }
+ </span>
+ <TextField
+ id="password"
+ label={t('newPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.password}
+ onChange={event => this.handlePasswordChange(event.target.value)}
+ error={passwordError}
+ autoComplete="new-password"
+ margin="normal"
+ largeLabel
+ />
+ <TextField
+ id="confirm-password"
+ label={t('confirmPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.confirmPassword}
+ onChange={event => this.handleConfirmPasswordChange(event.target.value)}
+ error={confirmPasswordError}
+ autoComplete="confirm-password"
+ margin="normal"
+ largeLabel
+ />
+ <button
+ className="first-time-flow__button"
+ onClick={() => !disabled && this.onClick()}
+ disabled={disabled}
+ >
+ {this.context.t('restore')}
+ </button>
+ </div>
+ </div>
+ </div>
)
}
}
-RestoreVaultPage.propTypes = {
- history: PropTypes.object,
-}
-
-const mapStateToProps = state => {
- const { appState: { warning, forgottenPassword } } = state
-
- return {
- warning,
- forgottenPassword,
- }
+RestoreVaultPage.contextTypes = {
+ t: PropTypes.func,
}
-const mapDispatchToProps = dispatch => {
- return {
- createNewVaultAndRestore: (password, seed) => {
- return dispatch(createNewVaultAndRestore(password, seed))
+export default connect(
+ ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
+ dispatch => ({
+ leaveImportSeedScreenState: () => {
+ dispatch(unMarkPasswordForgotten())
},
- unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
+ createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
+ })
)(RestoreVaultPage)
diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
index a1d3f9181..94915df76 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -2,19 +2,27 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '@material-ui/core/Button'
import TextField from '../../text-field'
-
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
-const { getEnvironmentType } = require('../../../../../app/scripts/lib/util')
-const getCaretCoordinates = require('textarea-caret')
-const EventEmitter = require('events').EventEmitter
-const Mascot = require('../../mascot')
-const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes')
-
-class UnlockPage extends Component {
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import getCaretCoordinates from 'textarea-caret'
+import { EventEmitter } from 'events'
+import Mascot from '../../mascot'
+import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
+
+export default class UnlockPage extends Component {
static contextTypes = {
t: PropTypes.func,
}
+ static propTypes = {
+ forgotPassword: PropTypes.func,
+ tryUnlockMetamask: PropTypes.func,
+ markPasswordForgotten: PropTypes.func,
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ useOldInterface: PropTypes.func,
+ }
+
constructor (props) {
super(props)
@@ -23,6 +31,7 @@ class UnlockPage extends Component {
error: null,
}
+ this.submitting = false
this.animationEventEmitter = new EventEmitter()
}
@@ -41,20 +50,21 @@ class UnlockPage extends Component {
const { password } = this.state
const { tryUnlockMetamask, history } = this.props
- if (password === '') {
+ if (password === '' || this.submitting) {
return
}
this.setState({ error: null })
+ this.submitting = true
try {
await tryUnlockMetamask(password)
+ this.submitting = false
+ history.push(DEFAULT_ROUTE)
} catch ({ message }) {
this.setState({ error: message })
- return
+ this.submitting = false
}
-
- history.push(DEFAULT_ROUTE)
}
handleInputChange ({ target }) {
@@ -98,7 +108,9 @@ class UnlockPage extends Component {
}
render () {
- const { error } = this.state
+ const { password, error } = this.state
+ const { t } = this.context
+ const { markPasswordForgotten, history } = this.props
return (
<div className="unlock-page__container">
@@ -111,18 +123,18 @@ class UnlockPage extends Component {
/>
</div>
<h1 className="unlock-page__title">
- { this.context.t('welcomeBack') }
+ { t('welcomeBack') }
</h1>
- <div>{ this.context.t('unlockMessage') }</div>
+ <div>{ t('unlockMessage') }</div>
<form
className="unlock-page__form"
onSubmit={event => this.handleSubmit(event)}
>
<TextField
id="password"
- label={this.context.t('password')}
+ label={t('password')}
type="password"
- value={this.state.password}
+ value={password}
onChange={event => this.handleInputChange(event)}
error={error}
autoFocus
@@ -136,28 +148,28 @@ class UnlockPage extends Component {
<div
className="unlock-page__link"
onClick={() => {
- this.props.markPasswordForgotten()
- this.props.history.push(RESTORE_VAULT_ROUTE)
+ markPasswordForgotten()
+ history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
- { this.context.t('restoreFromSeed') }
+ { t('restoreFromSeed') }
</div>
<div
className="unlock-page__link unlock-page__link--import"
onClick={() => {
- this.props.markPasswordForgotten()
- this.props.history.push(RESTORE_VAULT_ROUTE)
+ markPasswordForgotten()
+ history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
- { this.context.t('importUsingSeed') }
+ { t('importUsingSeed') }
</div>
</div>
</div>
@@ -165,15 +177,3 @@ class UnlockPage extends Component {
)
}
}
-
-UnlockPage.propTypes = {
- forgotPassword: PropTypes.func,
- tryUnlockMetamask: PropTypes.func,
- markPasswordForgotten: PropTypes.func,
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- t: PropTypes.func,
- useOldInterface: PropTypes.func,
-}
-
-export default UnlockPage
diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js
deleted file mode 100644
index af3a14f57..000000000
--- a/ui/app/components/pending-tx/confirm-deploy-contract.js
+++ /dev/null
@@ -1,358 +0,0 @@
-const { Component } = require('react')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const actions = require('../../actions')
-const clone = require('clone')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
-const { conversionUtil } = require('../../conversion-util')
-const SenderToRecipient = require('../sender-to-recipient')
-const NetworkDisplay = require('../network-display')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-
-class ConfirmDeployContract extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- valid: false,
- submitting: false,
- }
- }
-
- onSubmit (event) {
- event.preventDefault()
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams()) {
- this.props.sendTransaction(txMeta, event)
- } else {
- this.props.displayWarning(this.context.t('invalidGasParams'))
- this.setState({ submitting: false })
- }
- }
-
- cancel (event, txMeta) {
- event.preventDefault()
- this.props.cancelTransaction(txMeta)
- }
-
- checkValidity () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
- }
-
- getFormEl () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
- }
-
- // After a customizable state value has been updated,
- gatherTxMeta () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
- }
-
- verifyGasParams () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
- }
-
- _notZeroOrEmptyString (obj) {
- return obj !== '' && obj !== '0x0'
- }
-
- bnMultiplyByFraction (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
- }
-
- getData () {
- const { identities } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- return {
- from: {
- address: txParams.from,
- name: identities[txParams.from].name,
- },
- memo: txParams.memo || '',
- }
- }
-
- getAmount () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- const FIAT = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- fromDenomination: 'WEI',
- conversionRate,
- })
- const ETH = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- fiat: Number(FIAT),
- token: Number(ETH),
- }
-
- }
-
- getGasFee () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- // Gas
- const gas = txParams.gas
- const gasBn = hexToBn(gas)
-
- // Gas Price
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasPriceBn = hexToBn(gasPrice)
-
- const txFeeBn = gasBn.mul(gasPriceBn)
-
- const FIAT = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
-
- return {
- fiat: Number(FIAT),
- eth: Number(ETH),
- }
- }
-
- renderGasFee () {
- const { currentCurrency } = this.props
- const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`),
-
- h(
- 'div.confirm-screen-row-detail',
- `${ethGas} ETH`
- ),
- ]),
- ])
- )
- }
-
- renderHeroAmount () {
- const { currentCurrency } = this.props
- const { fiat: fiatAmount } = this.getAmount()
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { memo = '' } = txParams
-
- return (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`),
- h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', memo),
- ]),
- ])
- )
- }
-
- renderTotalPlusGas () {
- const { currentCurrency } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div.confirm-screen-section-column', [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`),
- h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`),
- ]),
- ])
- )
- }
-
- render () {
- const { backToAccountDetail, selectedAddress } = this.props
- const txMeta = this.gatherTxMeta()
-
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- } = this.getData()
-
- this.inputs = []
-
- return (
- h('.page-container', [
- h('.page-container__header', [
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => backToAccountDetail(selectedAddress),
- }, this.context.t('back')),
- window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay),
- ]),
- h('.page-container__title', this.context.t('confirmContract')),
- h('.page-container__subtitle', this.context.t('pleaseReviewTransaction')),
- ]),
- // Main Send token Card
- h('.page-container__content', [
-
- h(SenderToRecipient, {
- senderName: fromName,
- senderAddress: fromAddress,
- }),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're deploying a new contract.`,
- // ]),
-
- this.renderHeroAmount(),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', this.context.t('newContract')),
- ]),
- ]),
-
- this.renderGasFee(),
-
- this.renderTotalPlusGas(),
-
- ]),
- ]),
-
- h('form#pending-tx-form', {
- onSubmit: event => this.onSubmit(event),
- }, [
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: event => this.cancel(event, txMeta),
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, this.context.t('confirm')),
- ]),
- ]),
- ])
- )
- }
-}
-
-ConfirmDeployContract.propTypes = {
- sendTransaction: PropTypes.func,
- cancelTransaction: PropTypes.func,
- backToAccountDetail: PropTypes.func,
- displayWarning: PropTypes.func,
- identities: PropTypes.object,
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- selectedAddress: PropTypes.string,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const {
- conversionRate,
- identities,
- currentCurrency,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- return {
- currentCurrency,
- conversionRate,
- identities,
- selectedAddress,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- displayWarning: warning => actions.displayWarning(warning),
- }
-}
-
-ConfirmDeployContract.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract)
diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js
deleted file mode 100644
index bbf5683f0..000000000
--- a/ui/app/components/pending-tx/confirm-send-ether.js
+++ /dev/null
@@ -1,692 +0,0 @@
-const Component = require('react').Component
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const actions = require('../../actions')
-const clone = require('clone')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
-const classnames = require('classnames')
-const {
- conversionUtil,
- addCurrencies,
- multiplyCurrencies,
-} = require('../../conversion-util')
-const {
- calcGasTotal,
- isBalanceSufficient,
-} = require('../send_/send.utils')
-const GasFeeDisplay = require('../send/gas-fee-display-v2')
-const SenderToRecipient = require('../sender-to-recipient')
-const NetworkDisplay = require('../network-display')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
-const {
- ENVIRONMENT_TYPE_POPUP,
- ENVIRONMENT_TYPE_NOTIFICATION,
-} = require('../../../../app/scripts/lib/enums')
-
-import {
- updateSendErrors,
-} from '../../ducks/send.duck'
-
-ConfirmSendEther.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendEther)
-
-
-function mapStateToProps (state) {
- const {
- conversionRate,
- identities,
- currentCurrency,
- send,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- const { balance } = accounts[selectedAddress]
- return {
- conversionRate,
- identities,
- selectedAddress,
- currentCurrency,
- send,
- balance,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- clearSend: () => dispatch(actions.clearSend()),
- editTransaction: txMeta => {
- const { id, txParams } = txMeta
- const {
- gas: gasLimit,
- gasPrice,
- to,
- value: amount,
- } = txParams
-
- dispatch(actions.updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount,
- errors: { to: null, amount: null },
- editingTransactionId: id,
- }))
- },
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
- const { id, txParams, lastGasPrice } = txMeta
- const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- fromDenomination: 'WEI',
- }))
- }
-
- dispatch(actions.updateSend({
- gasLimit: sendGasLimit || txGasLimit,
- gasPrice: sendGasPrice || txGasPrice,
- editingTransactionId: id,
- gasTotal: sendGasTotal,
- forceGasMin,
- }))
- dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
- },
- updateSendErrors: error => dispatch(updateSendErrors(error)),
- }
-}
-
-inherits(ConfirmSendEther, Component)
-function ConfirmSendEther () {
- Component.call(this)
- this.state = {}
- this.onSubmit = this.onSubmit.bind(this)
-}
-
-ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
- const {
- balance: oldBalance,
- conversionRate: oldConversionRate,
- } = prevProps
- const {
- updateSendErrors,
- balance,
- conversionRate,
- send: {
- errors: {
- simulationFails,
- },
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
-
- const shouldUpdateBalanceSendErrors = balance && [
- balance !== oldBalance,
- conversionRate !== oldConversionRate,
- ].some(x => Boolean(x))
-
- if (shouldUpdateBalanceSendErrors) {
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- updateSendErrors({
- insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds',
- })
- }
-
- const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
-
- if (shouldUpdateSimulationSendError) {
- updateSendErrors({
- simulationFails: !txMeta.simulationFails ? false : 'transactionError',
- })
- }
-}
-
-ConfirmSendEther.prototype.componentWillMount = function () {
- this.updateComponentSendErrors({})
-}
-
-ConfirmSendEther.prototype.componentDidUpdate = function (prevProps) {
- this.updateComponentSendErrors(prevProps)
-}
-
-ConfirmSendEther.prototype.getAmount = function () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- const FIAT = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- fromDenomination: 'WEI',
- conversionRate,
- })
- const ETH = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- FIAT,
- ETH,
- }
-
-}
-
-ConfirmSendEther.prototype.getGasFee = function () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- // Gas
- const gas = txParams.gas
- const gasBn = hexToBn(gas)
-
- // From latest master
-// const gasLimit = new BN(parseInt(blockGasLimit))
-// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
-// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
-// const safeGasLimit = safeGasLimitBN.toString(10)
-
- // Gas Price
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasPriceBn = hexToBn(gasPrice)
-
- const txFeeBn = gasBn.mul(gasPriceBn)
-
- const FIAT = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
-
- return {
- FIAT,
- ETH,
- gasFeeInHex: txFeeBn.toString(16),
- }
-}
-
-ConfirmSendEther.prototype.getData = function () {
- const { identities } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const account = identities ? identities[txParams.from] || {} : {}
- const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH, gasFeeInHex } = this.getGasFee()
- const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount()
-
- const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, {
- toNumericBase: 'dec',
- numberOfDecimals: 2,
- })
- const totalInETH = addCurrencies(gasFeeInETH, amountInETH, {
- toNumericBase: 'dec',
- numberOfDecimals: 6,
- })
-
- return {
- from: {
- address: txParams.from,
- name: account.name,
- },
- to: {
- address: txParams.to,
- name: identities[txParams.to] ? identities[txParams.to].name : this.context.t('newRecipient'),
- },
- memo: txParams.memo || '',
- gasFeeInFIAT,
- gasFeeInETH,
- amountInFIAT,
- amountInETH,
- totalInFIAT,
- totalInETH,
- gasFeeInHex,
- }
-}
-
-ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, currencyCode) {
- const upperCaseCurrencyCode = currencyCode.toUpperCase()
-
- return currencies.find(currency => currency.code === upperCaseCurrencyCode)
- ? currencyFormatter.format(Number(value), {
- code: upperCaseCurrencyCode,
- })
- : value
-}
-
-ConfirmSendEther.prototype.editTransaction = function () {
- const { editTransaction, history } = this.props
- const txMeta = this.gatherTxMeta()
- editTransaction(txMeta)
- history.push(SEND_ROUTE)
-}
-
-ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) {
- const windowType = window.METAMASK_UI_TYPE
- const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
- windowType !== ENVIRONMENT_TYPE_POPUP
-
- if (isTxReprice && isFullScreen) {
- return null
- }
-
- return (
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => this.editTransaction(),
- style: {
- visibility: isTxReprice ? 'hidden' : 'initial',
- },
- }, 'Edit'),
- !isFullScreen && h(NetworkDisplay),
- ])
- )
-}
-
-ConfirmSendEther.prototype.renderHeader = function (isTxReprice) {
- const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
- const subtitle = isTxReprice
- ? this.context.t('speedUpSubtitle')
- : this.context.t('pleaseReviewTransaction')
-
- return (
- h('.page-container__header', [
- this.renderHeaderRow(isTxReprice),
- h('.page-container__title', title),
- h('.page-container__subtitle', subtitle),
- ])
- )
-}
-
-ConfirmSendEther.prototype.render = function () {
- const {
- currentCurrency,
- clearSend,
- conversionRate,
- currentCurrency: convertedCurrency,
- showCustomizeGasModal,
- send: {
- gasTotal,
- gasLimit: sendGasLimit,
- gasPrice: sendGasPrice,
- errors,
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
- const isTxReprice = Boolean(txMeta.lastGasPrice)
- const txParams = txMeta.txParams || {}
-
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- to: {
- address: toAddress,
- name: toName,
- },
- memo,
- gasFeeInHex,
- amountInFIAT,
- totalInFIAT,
- totalInETH,
- } = this.getData()
-
- const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency)
- const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
-
- // This is from the latest master
- // It handles some of the errors that we are not currently handling
- // Leaving as comments fo reference
-
- // const balanceBn = hexToBn(balance)
- // const insufficientBalance = balanceBn.lt(maxCost)
- // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
- // const showRejectAll = props.unconfTxListLength > 1
-// const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
-// const gasLimitSpecified = txMeta.gasLimitSpecified
-
- this.inputs = []
-
- return (
- // Main Send token Card
- h('.page-container', [
- this.renderHeader(isTxReprice),
- h('.page-container__content', [
- h(SenderToRecipient, {
- senderName: fromName,
- senderAddress: fromAddress,
- recipientName: toName,
- recipientAddress: txParams.to,
- }),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
- // ]),
-
- h('h3.flex-center.confirm-screen-send-amount', [`${convertedAmountInFiat}`]),
- h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', toName),
- h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h(GasFeeDisplay, {
- gasTotal: gasTotal || gasFeeInHex,
- conversionRate,
- convertedCurrency,
- onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
- }),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div', {
- className: classnames({
- 'confirm-screen-section-column--with-error': errors['insufficientFunds'],
- 'confirm-screen-section-column': !errors['insufficientFunds'],
- }),
- }, [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency.toUpperCase()}`),
- h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
- ]),
-
- this.renderErrorMessage('insufficientFunds'),
- ]),
- ]),
-
-// These are latest errors handling from master
-// Leaving as comments as reference when we start implementing error handling
-// h('style', `
-// .conf-buttons button {
-// margin-left: 10px;
-// text-transform: uppercase;
-// }
-// `),
-
-// txMeta.simulationFails ?
-// h('.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Transaction Error. Exception thrown in contract code.')
-// : null,
-
-// !isValidAddress ?
-// h('.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
-// : null,
-
-// insufficientBalance ?
-// h('span.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Insufficient balance for transaction')
-// : null,
-
-// // send + cancel
-// h('.flex-row.flex-space-around.conf-buttons', {
-// style: {
-// display: 'flex',
-// justifyContent: 'flex-end',
-// margin: '14px 25px',
-// },
-// }, [
-// h('button', {
-// onClick: (event) => {
-// this.resetGasFields()
-// event.preventDefault()
-// },
-// }, 'Reset'),
-
-// // Accept Button or Buy Button
-// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
-// h('input.confirm.btn-green', {
-// type: 'submit',
-// value: 'SUBMIT',
-// style: { marginLeft: '10px' },
-// disabled: buyDisabled,
-// }),
-
-// h('button.cancel.btn-red', {
-// onClick: props.cancelTransaction,
-// }, 'Reject'),
-// ]),
-// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
-// style: {
-// display: 'flex',
-// justifyContent: 'flex-end',
-// margin: '14px 25px',
-// },
-// }, [
-// h('button.cancel.btn-red', {
-// onClick: props.cancelAllTransactions,
-// }, 'Reject All'),
-// ]) : null,
-// ]),
-// ])
-// )
-// }
- ]),
-
- h('form#pending-tx-form', {
- className: 'confirm-screen-form',
- onSubmit: this.onSubmit,
- }, [
- this.renderErrorMessage('simulationFails'),
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: (event) => {
- clearSend()
- this.cancel(event, txMeta)
- },
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, this.context.t('confirm')),
- ]),
- ]),
- ])
- )
-}
-
-ConfirmSendEther.prototype.renderErrorMessage = function (message) {
- const { send: { errors } } = this.props
-
- return errors[message]
- ? h('div.confirm-screen-error', [ errors[message] ])
- : null
-}
-
-ConfirmSendEther.prototype.onSubmit = function (event) {
- event.preventDefault()
- const { updateSendErrors } = this.props
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams() && balanceIsSufficient) {
- this.props.sendTransaction(txMeta, event)
- } else if (!balanceIsSufficient) {
- updateSendErrors({ insufficientFunds: 'insufficientFunds' })
- } else {
- updateSendErrors({ invalidGasParams: 'invalidGasParams' })
- this.setState({ submitting: false })
- }
-}
-
-ConfirmSendEther.prototype.cancel = function (event, txMeta) {
- event.preventDefault()
- const { cancelTransaction } = this.props
-
- cancelTransaction(txMeta)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
- const {
- balance,
- conversionRate,
- } = this.props
- const {
- txParams: {
- gas,
- gasPrice,
- value: amount,
- },
- } = txMeta
- const gasTotal = calcGasTotal(gas, gasPrice)
-
- return isBalanceSufficient({
- amount,
- gasTotal,
- balance,
- conversionRate,
- })
-}
-
-ConfirmSendEther.prototype.checkValidity = function () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
-}
-
-ConfirmSendEther.prototype.getFormEl = function () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
-}
-
-// After a customizable state value has been updated,
-ConfirmSendEther.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
- const {
- lastGasPrice,
- txParams: {
- gasPrice: txGasPrice,
- gas: txGasLimit,
- },
- } = txData
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- }))
- }
-
- txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
- txData.txParams.gas = sendGasLimit || txGasLimit
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
-}
-
-ConfirmSendEther.prototype.verifyGasParams = function () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
-}
-
-ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) {
- return obj !== '' && obj !== '0x0'
-}
-
-ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js
deleted file mode 100644
index ee066b8f4..000000000
--- a/ui/app/components/pending-tx/confirm-send-token.js
+++ /dev/null
@@ -1,696 +0,0 @@
-const Component = require('react').Component
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const tokenAbi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(tokenAbi)
-const actions = require('../../actions')
-const clone = require('clone')
-const Identicon = require('../identicon')
-const GasFeeDisplay = require('../send/gas-fee-display-v2.js')
-const NetworkDisplay = require('../network-display')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const {
- conversionUtil,
- multiplyCurrencies,
- addCurrencies,
-} = require('../../conversion-util')
-const {
- calcGasTotal,
- isBalanceSufficient,
-} = require('../send_/send.utils')
-const {
- calcTokenAmount,
-} = require('../../token-util')
-const classnames = require('classnames')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-
-const {
- getTokenExchangeRate,
- getSelectedAddress,
- getSelectedTokenContract,
-} = require('../../selectors')
-const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
-
-import {
- updateSendErrors,
-} from '../../ducks/send.duck'
-
-const {
- ENVIRONMENT_TYPE_POPUP,
- ENVIRONMENT_TYPE_NOTIFICATION,
-} = require('../../../../app/scripts/lib/enums')
-
-ConfirmSendToken.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendToken)
-
-
-function mapStateToProps (state, ownProps) {
- const { token: { address }, txData } = ownProps
- const { txParams } = txData || {}
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
-
- const {
- conversionRate,
- identities,
- currentCurrency,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = getSelectedAddress(state)
- const tokenExchangeRate = getTokenExchangeRate(state, address)
- const { balance } = accounts[selectedAddress]
- return {
- conversionRate,
- identities,
- selectedAddress,
- tokenExchangeRate,
- tokenData: tokenData || {},
- currentCurrency: currentCurrency.toUpperCase(),
- send: state.metamask.send,
- tokenContract: getSelectedTokenContract(state),
- balance,
- }
-}
-
-function mapDispatchToProps (dispatch, ownProps) {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- editTransaction: txMeta => {
- const { token: { address } } = ownProps
- const { txParams = {}, id } = txMeta
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) || {}
- const { params = [] } = tokenData
- const { value: to } = params[0] || {}
- const { value: tokenAmountInDec } = params[1] || {}
- const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
- const {
- gas: gasLimit,
- gasPrice,
- } = txParams
- dispatch(actions.setSelectedToken(address))
- dispatch(actions.updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount: tokenAmountInHex,
- errors: { to: null, amount: null },
- editingTransactionId: id && id.toString(),
- token: ownProps.token,
- }))
- dispatch(actions.showSendTokenPage())
- },
- showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
- const { id, txParams, lastGasPrice } = txMeta
- const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { params = [] } = tokenData
- const { value: to } = params[0] || {}
- const { value: tokenAmountInDec } = params[1] || {}
- const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- fromDenomination: 'WEI',
- }))
- }
-
- dispatch(actions.updateSend({
- gasLimit: sendGasLimit || txGasLimit,
- gasPrice: sendGasPrice || txGasPrice,
- editingTransactionId: id,
- gasTotal: sendGasTotal,
- to,
- amount: tokenAmountInHex,
- forceGasMin,
- }))
- dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
- },
- updateSendErrors: error => dispatch(updateSendErrors(error)),
- }
-}
-
-inherits(ConfirmSendToken, Component)
-function ConfirmSendToken () {
- Component.call(this)
- this.state = {}
- this.onSubmit = this.onSubmit.bind(this)
-}
-
-ConfirmSendToken.prototype.editTransaction = function (txMeta) {
- const { editTransaction, history } = this.props
- editTransaction(txMeta)
- history.push(SEND_ROUTE)
-}
-
-ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) {
- const {
- balance: oldBalance,
- conversionRate: oldConversionRate,
- } = prevProps
- const {
- updateSendErrors,
- balance,
- conversionRate,
- send: {
- errors: {
- simulationFails,
- },
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
-
- const shouldUpdateBalanceSendErrors = balance && [
- balance !== oldBalance,
- conversionRate !== oldConversionRate,
- ].some(x => Boolean(x))
-
- if (shouldUpdateBalanceSendErrors) {
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- updateSendErrors({
- insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
- })
- }
-
- const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
-
- if (shouldUpdateSimulationSendError) {
- updateSendErrors({
- simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
- })
- }
-}
-
-ConfirmSendToken.prototype.componentWillMount = function () {
- const { tokenContract, selectedAddress } = this.props
- tokenContract && tokenContract
- .balanceOf(selectedAddress)
- .then(usersToken => {
- })
- this.updateComponentSendErrors({})
-}
-
-ConfirmSendToken.prototype.componentDidUpdate = function (prevProps) {
- this.updateComponentSendErrors(prevProps)
-}
-
-ConfirmSendToken.prototype.getAmount = function () {
- const {
- conversionRate,
- tokenExchangeRate,
- token,
- tokenData,
- send: { amount, editingTransactionId },
- } = this.props
- const { params = [] } = tokenData
- let { value } = params[1] || {}
- const { decimals } = token
-
- if (editingTransactionId) {
- value = conversionUtil(amount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
- }
-
- const sendTokenAmount = calcTokenAmount(value, decimals)
-
- return {
- fiat: tokenExchangeRate
- ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2)
- : null,
- token: typeof value === 'undefined'
- ? this.context.t('unknown')
- : +sendTokenAmount.toFixed(decimals),
- }
-
-}
-
-ConfirmSendToken.prototype.getGasFee = function () {
- const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { decimals } = token
-
- const gas = txParams.gas
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasTotal = multiplyCurrencies(gas, gasPrice, {
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-
- const FIAT = conversionUtil(gasTotal, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(gasTotal, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
- const tokenGas = multiplyCurrencies(gas, gasPrice, {
- toNumericBase: 'dec',
- multiplicandBase: 16,
- multiplierBase: 16,
- toCurrency: 'BAT',
- conversionRate: tokenExchangeRate,
- invertConversionRate: true,
- fromDenomination: 'WEI',
- numberOfDecimals: decimals || 4,
- })
-
- return {
- fiat: +Number(FIAT).toFixed(2),
- eth: ETH,
- token: tokenExchangeRate
- ? tokenGas
- : null,
- gasFeeInHex: gasTotal.toString(16),
- }
-}
-
-ConfirmSendToken.prototype.getData = function () {
- const { identities, tokenData } = this.props
- const { params = [] } = tokenData
- const { value } = params[0] || {}
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- return {
- from: {
- address: txParams.from,
- name: identities[txParams.from].name,
- },
- to: {
- address: value,
- name: identities[value] ? identities[value].name : this.context.t('newRecipient'),
- },
- memo: txParams.memo || '',
- }
-}
-
-ConfirmSendToken.prototype.renderHeroAmount = function () {
- const { token: { symbol }, currentCurrency } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { memo = '' } = txParams
-
- const convertedAmountInFiat = this.convertToRenderableCurrency(fiatAmount, currentCurrency)
-
- return fiatAmount
- ? (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', `${convertedAmountInFiat}`),
- h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
- ])
- )
- : (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', tokenAmount),
- h('h3.flex-center.confirm-screen-send-amount-currency', symbol),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderGasFee = function () {
- const {
- currentCurrency: convertedCurrency,
- conversionRate,
- send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
- showCustomizeGasModal,
- } = this.props
- const txMeta = this.gatherTxMeta()
- const { gasFeeInHex } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h(GasFeeDisplay, {
- gasTotal: gasTotal || gasFeeInHex,
- conversionRate,
- convertedCurrency,
- onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
- }),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderTotalPlusGas = function () {
- const { token: { symbol }, currentCurrency, send: { errors } } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
-
- const totalInFIAT = fiatAmount && fiatGas && addCurrencies(fiatAmount, fiatGas)
- const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
-
- return fiatAmount && fiatGas
- ? (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div.confirm-screen-section-column', [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency}`),
- h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`),
- ]),
- ])
- )
- : (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div', {
- className: classnames({
- 'confirm-screen-section-column--with-error': errors['insufficientFunds'],
- 'confirm-screen-section-column': !errors['insufficientFunds'],
- }),
- }, [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
- h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`),
- ]),
-
- this.renderErrorMessage('insufficientFunds'),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderErrorMessage = function (message) {
- const { send: { errors } } = this.props
-
- return errors[message]
- ? h('div.confirm-screen-error', [ errors[message] ])
- : null
-}
-
-ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, currencyCode) {
- const upperCaseCurrencyCode = currencyCode.toUpperCase()
-
- return currencies.find(currency => currency.code === upperCaseCurrencyCode)
- ? currencyFormatter.format(Number(value), {
- code: upperCaseCurrencyCode,
- })
- : value
-}
-
-ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) {
- const windowType = window.METAMASK_UI_TYPE
- const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
- windowType !== ENVIRONMENT_TYPE_POPUP
-
- if (isTxReprice && isFullScreen) {
- return null
- }
-
- return (
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => this.editTransaction(),
- style: {
- visibility: isTxReprice ? 'hidden' : 'initial',
- },
- }, 'Edit'),
- !isFullScreen && h(NetworkDisplay),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderHeader = function (isTxReprice) {
- const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
- const subtitle = isTxReprice
- ? this.context.t('speedUpSubtitle')
- : this.context.t('pleaseReviewTransaction')
-
- return (
- h('.page-container__header', [
- this.renderHeaderRow(isTxReprice),
- h('.page-container__title', title),
- h('.page-container__subtitle', subtitle),
- ])
- )
-}
-
-ConfirmSendToken.prototype.render = function () {
- const txMeta = this.gatherTxMeta()
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- to: {
- address: toAddress,
- name: toName,
- },
- } = this.getData()
-
- const isTxReprice = Boolean(txMeta.lastGasPrice)
-
- return (
- h('div.confirm-screen-container.confirm-send-token', [
- // Main Send token Card
- h('div.page-container', [
- this.renderHeader(isTxReprice),
- h('.page-container__content', [
- h('div.flex-row.flex-center.confirm-screen-identicons', [
- h('div.confirm-screen-account-wrapper', [
- h(
- Identicon,
- {
- address: fromAddress,
- diameter: 60,
- },
- ),
- h('span.confirm-screen-account-name', fromName),
- // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
- ]),
- h('i.fa.fa-arrow-right.fa-lg'),
- h('div.confirm-screen-account-wrapper', [
- h(
- Identicon,
- {
- address: toAddress,
- diameter: 60,
- },
- ),
- h('span.confirm-screen-account-name', toName),
- // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
- ]),
- ]),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
- // ]),
-
- this.renderHeroAmount(),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- toAddress && h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', toName),
- h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
- ]),
- ]),
-
- this.renderGasFee(),
-
- this.renderTotalPlusGas(),
-
- ]),
-
- ]),
-
- h('form#pending-tx-form', {
- className: 'confirm-screen-form',
- onSubmit: this.onSubmit,
- }, [
- this.renderErrorMessage('simulationFails'),
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: (event) => this.cancel(event, txMeta),
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, [this.context.t('confirm')]),
- ]),
- ]),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.onSubmit = function (event) {
- event.preventDefault()
- const { updateSendErrors } = this.props
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams() && balanceIsSufficient) {
- this.props.sendTransaction(txMeta, event)
- } else if (!balanceIsSufficient) {
- updateSendErrors({ insufficientFunds: 'insufficientFunds' })
- } else {
- updateSendErrors({ invalidGasParams: 'invalidGasParams' })
- this.setState({ submitting: false })
- }
-}
-
-ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
- const {
- balance,
- conversionRate,
- } = this.props
- const {
- txParams: {
- gas,
- gasPrice,
- },
- } = txMeta
- const gasTotal = calcGasTotal(gas, gasPrice)
-
- return isBalanceSufficient({
- amount: '0',
- gasTotal,
- balance,
- conversionRate,
- })
-}
-
-
-ConfirmSendToken.prototype.cancel = function (event, txMeta) {
- event.preventDefault()
- const { cancelTransaction } = this.props
-
- cancelTransaction(txMeta)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmSendToken.prototype.checkValidity = function () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
-}
-
-ConfirmSendToken.prototype.getFormEl = function () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
-}
-
-// After a customizable state value has been updated,
-ConfirmSendToken.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
- const {
- lastGasPrice,
- txParams: {
- gasPrice: txGasPrice,
- gas: txGasLimit,
- },
- } = txData
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- }))
- }
-
- txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
- txData.txParams.gas = sendGasLimit || txGasLimit
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
-}
-
-ConfirmSendToken.prototype.verifyGasParams = function () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
-}
-
-ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) {
- return obj !== '' && obj !== '0x0'
-}
-
-ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js
deleted file mode 100644
index 3f8cd8823..000000000
--- a/ui/app/components/pending-tx/index.js
+++ /dev/null
@@ -1,165 +0,0 @@
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const clone = require('clone')
-const abi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(abi)
-const inherits = require('util').inherits
-const actions = require('../../actions')
-const { getSymbolAndDecimals } = require('../../token-util')
-const ConfirmSendEther = require('./confirm-send-ether')
-const ConfirmSendToken = require('./confirm-send-token')
-const ConfirmDeployContract = require('./confirm-deploy-contract')
-const Loading = require('../loading-screen')
-
-const TX_TYPES = {
- DEPLOY_CONTRACT: 'deploy_contract',
- SEND_ETHER: 'send_ether',
- SEND_TOKEN: 'send_token',
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx)
-
-function mapStateToProps (state) {
- const {
- conversionRate,
- identities,
- tokens: existingTokens,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- return {
- conversionRate,
- identities,
- selectedAddress,
- existingTokens,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- }
-}
-
-inherits(PendingTx, Component)
-function PendingTx () {
- Component.call(this)
- this.state = {
- isFetching: true,
- transactionType: '',
- tokenAddress: '',
- tokenSymbol: '',
- tokenDecimals: '',
- }
-}
-
-PendingTx.prototype.componentDidMount = function () {
- this.setTokenData()
-}
-
-PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
- if (prevState.isFetching) {
- this.setTokenData()
- }
-}
-
-PendingTx.prototype.setTokenData = async function () {
- const { existingTokens } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- if (txMeta.loadingDefaults) {
- return
- }
-
- if (!txParams.to) {
- return this.setState({
- transactionType: TX_TYPES.DEPLOY_CONTRACT,
- isFetching: false,
- })
- }
-
- // inspect tx data for supported special confirmation screens
- let isTokenTransaction = false
- if (txParams.data) {
- const tokenData = abiDecoder.decodeMethod(txParams.data)
- const { name: tokenMethodName } = tokenData || {}
- isTokenTransaction = (tokenMethodName === 'transfer')
- }
-
- if (isTokenTransaction) {
- const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
-
- this.setState({
- transactionType: TX_TYPES.SEND_TOKEN,
- tokenAddress: txParams.to,
- tokenSymbol: symbol,
- tokenDecimals: decimals,
- isFetching: false,
- })
- } else {
- this.setState({
- transactionType: TX_TYPES.SEND_ETHER,
- isFetching: false,
- })
- }
-}
-
-PendingTx.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- return txData
-}
-
-PendingTx.prototype.render = function () {
- const {
- isFetching,
- transactionType,
- tokenAddress,
- tokenSymbol,
- tokenDecimals,
- } = this.state
-
- const { sendTransaction } = this.props
-
- if (isFetching) {
- return h(Loading, {
- loadingMessage: this.context.t('generatingTransaction'),
- })
- }
-
- switch (transactionType) {
- case TX_TYPES.SEND_ETHER:
- return h(ConfirmSendEther, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- })
- case TX_TYPES.SEND_TOKEN:
- return h(ConfirmSendToken, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- token: {
- address: tokenAddress,
- symbol: tokenSymbol,
- decimals: tokenDecimals,
- },
- })
- case TX_TYPES.DEPLOY_CONTRACT:
- return h(ConfirmDeployContract, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- })
- default:
- return h(Loading)
- }
-}
-
-PendingTx.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js
index 3386a4196..6c202141e 100644
--- a/ui/app/components/selected-account/selected-account.component.js
+++ b/ui/app/components/selected-account/selected-account.component.js
@@ -1,17 +1,10 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import copyToClipboard from 'copy-to-clipboard'
+import { addressSlicer } from '../../util'
const Tooltip = require('../tooltip-v2.js')
-const addressStripper = (address = '') => {
- if (address.length < 4) {
- return address
- }
-
- return `${address.slice(0, 4)}...${address.slice(-4)}`
-}
-
class SelectedAccount extends Component {
state = {
copied: false,
@@ -48,7 +41,7 @@ class SelectedAccount extends Component {
{ selectedIdentity.name }
</div>
<div className="selected-account__address">
- { addressStripper(selectedAddress) }
+ { addressSlicer(selectedAddress) }
</div>
</div>
</Tooltip>
diff --git a/ui/app/components/send_/README.md b/ui/app/components/send/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/README.md
+++ b/ui/app/components/send/README.md
diff --git a/ui/app/components/send_/account-list-item/account-list-item-README.md b/ui/app/components/send/account-list-item/account-list-item-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/account-list-item/account-list-item-README.md
+++ b/ui/app/components/send/account-list-item/account-list-item-README.md
diff --git a/ui/app/components/send_/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js
index b8407d147..9f4a96e61 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.component.js
+++ b/ui/app/components/send/account-list-item/account-list-item.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { checksumAddress } from '../../../util'
import Identicon from '../../identicon'
-import CurrencyDisplay from '../../send/currency-display'
+import CurrencyDisplay from '../currency-display'
export default class AccountListItem extends Component {
@@ -17,6 +17,10 @@ export default class AccountListItem extends Component {
icon: PropTypes.node,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
account,
@@ -67,8 +71,3 @@ export default class AccountListItem extends Component {
</div>)
}
}
-
-AccountListItem.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/account-list-item/account-list-item.container.js b/ui/app/components/send/account-list-item/account-list-item.container.js
index 3151b1f1d..4b4519288 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.container.js
+++ b/ui/app/components/send/account-list-item/account-list-item.container.js
@@ -1,7 +1,7 @@
import { connect } from 'react-redux'
import {
getConversionRate,
- getConvertedCurrency,
+ getCurrentCurrency,
} from '../send.selectors.js'
import AccountListItem from './account-list-item.component'
@@ -10,6 +10,6 @@ export default connect(mapStateToProps)(AccountListItem)
function mapStateToProps (state) {
return {
conversionRate: getConversionRate(state),
- currentCurrency: getConvertedCurrency(state),
+ currentCurrency: getCurrentCurrency(state),
}
}
diff --git a/ui/app/components/send_/account-list-item/account-list-item.scss b/ui/app/components/send/account-list-item/account-list-item.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.scss
+++ b/ui/app/components/send/account-list-item/account-list-item.scss
diff --git a/ui/app/components/send/account-list-item/index.js b/ui/app/components/send/account-list-item/index.js
new file mode 100644
index 000000000..907485cf7
--- /dev/null
+++ b/ui/app/components/send/account-list-item/index.js
@@ -0,0 +1 @@
+export { default } from './account-list-item.container'
diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
index bb7f3776c..ef152d2e7 100644
--- a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js
+++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
@@ -4,7 +4,7 @@ import { shallow } from 'enzyme'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import Identicon from '../../../identicon'
-import CurrencyDisplay from '../../../send/currency-display'
+import CurrencyDisplay from '../../currency-display'
const utilsMethodStubs = {
checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
index 49da920e6..af0859117 100644
--- a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js
+++ b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
@@ -12,7 +12,7 @@ proxyquire('../account-list-item.container.js', {
},
'../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`,
- getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`,
+ getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`,
},
})
diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display/currency-display.js
index e410bc070..2b8eaa41f 100644
--- a/ui/app/components/send/currency-display.js
+++ b/ui/app/components/send/currency-display/currency-display.js
@@ -1,10 +1,16 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
+const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util')
+const { removeLeadingZeroes } = require('../send.utils')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
const ethUtil = require('ethereumjs-util')
+const PropTypes = require('prop-types')
+
+CurrencyDisplay.contextTypes = {
+ t: PropTypes.func,
+}
module.exports = CurrencyDisplay
@@ -74,6 +80,12 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi
CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) {
const { primaryCurrency, convertedCurrency, conversionRate } = this.props
+ if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) {
+ if (nonFormattedValue !== 0) {
+ return null
+ }
+ }
+
let convertedValue = conversionUtil(nonFormattedValue, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
@@ -81,20 +93,15 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
numberOfDecimals: 2,
conversionRate,
})
- convertedValue = Number(convertedValue).toFixed(2)
+ convertedValue = Number(convertedValue).toFixed(2)
const upperCaseCurrencyCode = convertedCurrency.toUpperCase()
-
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(convertedValue), {
code: upperCaseCurrencyCode,
})
- : convertedValue
-}
-
-function removeLeadingZeroes (str) {
- return str.replace(/^0*(?=\d)/, '')
-}
+ : convertedValue
+ }
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
@@ -108,16 +115,28 @@ CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
+CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) {
+ const {
+ convertedBalanceClassName = 'currency-display__converted-value',
+ convertedCurrency,
+ } = this.props
+ return h('div', {
+ className: convertedBalanceClassName,
+ }, convertedValueToRender == null
+ ? this.context.t('noConversionRateAvailable')
+ : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`
+)
+ }
+
CurrencyDisplay.prototype.render = function () {
const {
className = 'currency-display',
primaryBalanceClassName = 'currency-display__input',
- convertedBalanceClassName = 'currency-display__converted-value',
primaryCurrency,
- convertedCurrency,
readOnly = false,
inError = false,
onBlur,
+ step,
} = this.props
const { valueToRender } = this.state
@@ -152,17 +171,14 @@ CurrencyDisplay.prototype.render = function () {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
+ step,
}),
h('span.currency-display__currency-symbol', primaryCurrency),
]),
- ]),
-
- h('div', {
- className: convertedBalanceClassName,
- }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`),
+ ]), this.onlyRenderConversions(convertedValueToRender),
])
diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js
new file mode 100644
index 000000000..5dc269c5a
--- /dev/null
+++ b/ui/app/components/send/currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './currency-display.js'
diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js
deleted file mode 100644
index 1423aa84d..000000000
--- a/ui/app/components/send/gas-fee-display-v2.js
+++ /dev/null
@@ -1,53 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const CurrencyDisplay = require('./currency-display')
-const connect = require('react-redux').connect
-
-GasFeeDisplay.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(GasFeeDisplay)
-
-
-inherits(GasFeeDisplay, Component)
-function GasFeeDisplay () {
- Component.call(this)
-}
-
-GasFeeDisplay.prototype.render = function () {
- const {
- conversionRate,
- gasTotal,
- onClick,
- primaryCurrency = 'ETH',
- convertedCurrency,
- gasLoadingError,
- } = this.props
-
- return h('div.send-v2__gas-fee-display', [
-
- gasTotal
- ? h(CurrencyDisplay, {
- primaryCurrency,
- convertedCurrency,
- value: gasTotal,
- conversionRate,
- convertedPrefix: '$',
- readOnly: true,
- })
- : gasLoadingError
- ? h('div.currency-display.currency-display--message', this.context.t('setGasPrice'))
- : h('div.currency-display', this.context.t('loading')),
-
- h('button.sliders-icon-container', {
- onClick,
- disabled: !gasTotal && !gasLoadingError,
- }, [
- h('i.fa.fa-sliders.sliders-icon'),
- ]),
-
- ])
-}
diff --git a/ui/app/components/send/index.js b/ui/app/components/send/index.js
new file mode 100644
index 000000000..b5114babc
--- /dev/null
+++ b/ui/app/components/send/index.js
@@ -0,0 +1 @@
+export { default } from './send.container'
diff --git a/ui/app/components/send/send-content/index.js b/ui/app/components/send/send-content/index.js
new file mode 100644
index 000000000..891c17e6a
--- /dev/null
+++ b/ui/app/components/send/send-content/index.js
@@ -0,0 +1 @@
+export { default } from './send-content.component'
diff --git a/ui/app/components/send_/send-content/send-amount-row/README.md b/ui/app/components/send/send-content/send-amount-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-amount-row/README.md
+++ b/ui/app/components/send/send-content/send-amount-row/README.md
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index bdf12b738..4d0d36ab4 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -13,6 +13,10 @@ export default class AmountMaxButton extends Component {
tokenBalance: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
setMaxAmount () {
const {
balance,
@@ -48,7 +52,3 @@ export default class AmountMaxButton extends Component {
}
}
-
-AmountMaxButton.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
index 2d2ec42f7..2d2ec42f7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
index 69fec1994..69fec1994 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
index b490a7fd7..b490a7fd7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js
new file mode 100644
index 000000000..ee8271494
--- /dev/null
+++ b/ui/app/components/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/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
index 86a05ff21..86a05ff21 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
index 2cc00d6d6..2cc00d6d6 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
index 655fe1969..655fe1969 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
index 816df6a12..816df6a12 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
diff --git a/ui/app/components/send/send-content/send-amount-row/index.js b/ui/app/components/send/send-content/send-amount-row/index.js
new file mode 100644
index 000000000..abc6852fe
--- /dev/null
+++ b/ui/app/components/send/send-content/send-amount-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-amount-row.container'
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
index 8da36d3b7..c548a5695 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
import AmountMaxButton from './amount-max-button/'
-import CurrencyDisplay from '../../../send/currency-display'
+import CurrencyDisplay from '../../currency-display'
export default class SendAmountRow extends Component {
@@ -21,10 +21,15 @@ export default class SendAmountRow extends Component {
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 {
@@ -35,6 +40,7 @@ export default class SendAmountRow extends Component {
primaryCurrency,
selectedToken,
tokenBalance,
+ updateGasFeeError,
updateSendAmountError,
} = this.props
@@ -48,6 +54,19 @@ export default class SendAmountRow extends Component {
selectedToken,
tokenBalance,
})
+
+ if (selectedToken) {
+ updateGasFeeError({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ }
}
updateAmount (amount) {
@@ -95,14 +114,10 @@ export default class SendAmountRow extends Component {
primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken}
value={amount}
+ step="any"
/>
</SendRowWrapper>
)
}
}
-
-SendAmountRow.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
index bbbf56971..3504d1b73 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
@@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import {
getAmountConversionRate,
getConversionRate,
- getConvertedCurrency,
+ getCurrentCurrency,
getGasTotal,
getPrimaryCurrency,
getSelectedToken,
@@ -13,7 +13,7 @@ import {
import {
sendAmountIsInError,
} from './send-amount-row.selectors'
-import { getAmountErrorObject } from '../../send.utils'
+import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils'
import {
setMaxModeTo,
updateSendAmount,
@@ -31,7 +31,7 @@ function mapStateToProps (state) {
amountConversionRate: getAmountConversionRate(state),
balance: getSendFromBalance(state),
conversionRate: getConversionRate(state),
- convertedCurrency: getConvertedCurrency(state),
+ convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state),
inError: sendAmountIsInError(state),
primaryCurrency: getPrimaryCurrency(state),
@@ -44,6 +44,9 @@ function mapDispatchToProps (dispatch) {
return {
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
+ updateGasFeeError: (amountDataObject) => {
+ dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject)))
+ },
updateSendAmountError: (amountDataObject) => {
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
},
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js
index fb08c7ed7..fb08c7ed7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
index 579e18585..8425e076e 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
@@ -6,13 +6,14 @@ 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 CurrencyDisplay from '../../../../send/currency-display'
+import CurrencyDisplay from '../../../currency-display'
const propsMethodSpies = {
setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(),
updateSendAmountError: sinon.spy(),
updateGas: sinon.spy(),
+ updateGasFeeError: sinon.spy(),
}
sinon.spy(SendAmountRow.prototype, 'updateAmount')
@@ -36,6 +37,7 @@ describe('SendAmountRow Component', function () {
selectedToken={ { address: 'mockTokenAddress' } }
setMaxModeTo={propsMethodSpies.setMaxModeTo}
tokenBalance={'mockTokenBalance'}
+ updateGasFeeError={propsMethodSpies.updateGasFeeError}
updateSendAmount={propsMethodSpies.updateSendAmount}
updateSendAmountError={propsMethodSpies.updateSendAmountError}
updateGas={propsMethodSpies.updateGas}
@@ -47,6 +49,7 @@ describe('SendAmountRow Component', function () {
propsMethodSpies.setMaxModeTo.resetHistory()
propsMethodSpies.updateSendAmount.resetHistory()
propsMethodSpies.updateSendAmountError.resetHistory()
+ propsMethodSpies.updateGasFeeError.resetHistory()
SendAmountRow.prototype.validateAmount.resetHistory()
SendAmountRow.prototype.updateAmount.resetHistory()
})
@@ -72,6 +75,32 @@ describe('SendAmountRow Component', function () {
)
})
+ it('should call updateGasFeeError if selectedToken is truthy', () => {
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateGasFeeError.getCall(0).args,
+ [{
+ amount: 'someAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 7,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: { address: 'mockTokenAddress' },
+ tokenBalance: 'mockTokenBalance',
+ }]
+ )
+ })
+
+ it('should call not updateGasFeeError if selectedToken is falsey', () => {
+ wrapper.setProps({ selectedToken: null })
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ })
+
})
describe('updateAmount', () => {
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
index e4c913c69..52e351aee 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
@@ -24,7 +24,7 @@ proxyquire('../send-amount-row.container.js', {
'../../send.selectors': {
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
getConversionRate: (s) => `mockConversionRate:${s}`,
- getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
+ getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
getSelectedToken: (s) => `mockSelectedToken:${s}`,
@@ -33,7 +33,10 @@ proxyquire('../send-amount-row.container.js', {
getTokenBalance: (s) => `mockTokenBalance:${s}`,
},
'./send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
- '../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) },
+ '../../send.utils': {
+ getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }),
+ getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }),
+ },
'../../../../actions': actionSpies,
'../../../../ducks/send.duck': duckActionSpies,
})
@@ -66,6 +69,7 @@ describe('send-amount-row container', () => {
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ duckActionSpies.updateSendErrors.resetHistory()
})
describe('setMaxModeTo()', () => {
@@ -92,6 +96,18 @@ describe('send-amount-row container', () => {
})
})
+ 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' })
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
index 4672cb8a7..4672cb8a7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-content-README.md b/ui/app/components/send/send-content/send-content-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-content-README.md
+++ b/ui/app/components/send/send-content/send-content-README.md
diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
index adc114c0e..7a0b1a18e 100644
--- a/ui/app/components/send_/send-content/send-content.component.js
+++ b/ui/app/components/send/send-content/send-content.component.js
@@ -4,6 +4,7 @@ import PageContainerContent from '../../page-container/page-container-content.co
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 {
@@ -20,6 +21,7 @@ export default class SendContent extends Component {
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow />
+ <SendHexDataRow />
</div>
</PageContainerContent>
)
diff --git a/ui/app/components/send_/send-content/send-content.scss b/ui/app/components/send/send-content/send-content.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-content.scss
+++ b/ui/app/components/send/send-content/send-content.scss
diff --git a/ui/app/components/send/send-content/send-dropdown-list/index.js b/ui/app/components/send/send-content/send-dropdown-list/index.js
new file mode 100644
index 000000000..04af6536c
--- /dev/null
+++ b/ui/app/components/send/send-content/send-dropdown-list/index.js
@@ -0,0 +1 @@
+export { default } from './send-dropdown-list.component'
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
index 5c7174ecf..bedac1259 100644
--- a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js
+++ b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
@@ -11,6 +11,10 @@ export default class SendDropdownList extends Component {
activeAddress: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
getListItemIcon (accountAddress, activeAddress) {
return accountAddress === activeAddress
? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
@@ -46,7 +50,3 @@ export default class SendDropdownList extends Component {
}
}
-
-SendDropdownList.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
index b92dd4dfe..b92dd4dfe 100644
--- a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
+++ b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
index 418766cd9..4f43a9d61 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
@@ -14,6 +14,10 @@ export default class FromDropdown extends Component {
selectedAccount: PropTypes.object,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
accounts,
@@ -40,7 +44,3 @@ export default class FromDropdown extends Component {
}
}
-
-FromDropdown.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
new file mode 100644
index 000000000..2314ef4e3
--- /dev/null
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
@@ -0,0 +1 @@
+export { default } from './from-dropdown.component'
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
index 84fcb281e..84fcb281e 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
diff --git a/ui/app/components/send/send-content/send-from-row/index.js b/ui/app/components/send/send-content/send-from-row/index.js
new file mode 100644
index 000000000..0a79726b2
--- /dev/null
+++ b/ui/app/components/send/send-content/send-from-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-from-row.container'
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
index a580aef96..3e0e0de22 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
@@ -17,6 +17,10 @@ export default class SendFromRow extends Component {
setSendTokenBalance: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
async handleFromChange (newFrom) {
const {
updateSendFrom,
@@ -57,7 +61,3 @@ export default class SendFromRow extends Component {
}
}
-
-SendFromRow.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
index 33cb63b43..33cb63b43 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js
index 03ef4806b..03ef4806b 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
index 9ba8d1739..9ba8d1739 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
index e080b2fe3..e080b2fe3 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
index ecb57bbc3..ecb57bbc3 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-gas-row/README.md b/ui/app/components/send/send-content/send-gas-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-gas-row/README.md
+++ b/ui/app/components/send/send-content/send-gas-row/README.md
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
new file mode 100644
index 000000000..bb9a94428
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
@@ -0,0 +1,61 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../../../../send/currency-display'
+
+
+export default class GasFeeDisplay extends Component {
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ primaryCurrency: PropTypes.string,
+ convertedCurrency: PropTypes.string,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ onClick: PropTypes.func,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const {
+ conversionRate,
+ gasTotal,
+ onClick,
+ primaryCurrency = 'ETH',
+ convertedCurrency,
+ gasLoadingError,
+ } = this.props
+
+ return (
+ <div className="send-v2__gas-fee-display">
+ {gasTotal
+ ? <CurrencyDisplay
+ primaryCurrency={primaryCurrency}
+ convertedCurrency={convertedCurrency}
+ value={gasTotal}
+ conversionRate={conversionRate}
+ gasLoadingError={gasLoadingError}
+ convertedPrefix={'$'}
+ readOnly
+ />
+ : gasLoadingError
+ ? <div className="currency-display.currency-display--message">
+ {this.context.t('setGasPrice')}
+ </div>
+ : <div className="currency-display">
+ {this.context.t('loading')}
+ </div>
+ }
+ <button
+ className="sliders-icon-container"
+ onClick={onClick}
+ disabled={!gasTotal && !gasLoadingError}
+ >
+ <i className="fa fa-sliders sliders-icon" />
+ </button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js
new file mode 100644
index 000000000..dba0edb7b
--- /dev/null
+++ b/ui/app/components/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/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
new file mode 100644
index 000000000..7cbe8d0df
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
@@ -0,0 +1,55 @@
+import React from 'react'
+import assert from 'assert'
+import {shallow} from 'enzyme'
+import GasFeeDisplay from '../gas-fee-display.component'
+import CurrencyDisplay from '../../../../../send/currency-display'
+import sinon from 'sinon'
+
+
+const propsMethodSpies = {
+ showCustomizeGasModal: sinon.spy(),
+}
+
+describe('SendGasRow Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasFeeDisplay
+ conversionRate={20}
+ gasTotal={'mockGasTotal'}
+ onClick={propsMethodSpies.showCustomizeGasModal}
+ primaryCurrency={'mockPrimaryCurrency'}
+ convertedCurrency={'mockConvertedCurrency'}
+ />, {context: {t: str => str + '_t'}})
+ })
+
+ afterEach(() => {
+ propsMethodSpies.showCustomizeGasModal.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a CurrencyDisplay component', () => {
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ })
+
+ it('should render the CurrencyDisplay with the correct props', () => {
+ const {
+ conversionRate,
+ convertedCurrency,
+ value,
+ } = wrapper.find(CurrencyDisplay).props()
+ assert.equal(conversionRate, 20)
+ assert.equal(convertedCurrency, 'mockConvertedCurrency')
+ assert.equal(value, 'mockGasTotal')
+ })
+
+ it('should render the Button with the correct props', () => {
+ const {
+ onClick,
+ } = wrapper.find('button').props()
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
+ onClick()
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/components/send/send-content/send-gas-row/index.js b/ui/app/components/send/send-content/send-gas-row/index.js
new file mode 100644
index 000000000..3c7ff1d5f
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-gas-row.container'
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
index c80d8c0bb..91b58cfd0 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
@@ -1,29 +1,39 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
-import GasFeeDisplay from '../../../send/gas-fee-display-v2'
+import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
export default class SendGasRow extends Component {
static propTypes = {
conversionRate: PropTypes.number,
convertedCurrency: PropTypes.string,
+ gasFeeError: PropTypes.bool,
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
conversionRate,
convertedCurrency,
gasLoadingError,
gasTotal,
+ gasFeeError,
showCustomizeGasModal,
} = this.props
return (
- <SendRowWrapper label={`${this.context.t('gasFee')}:`}>
+ <SendRowWrapper
+ label={`${this.context.t('gasFee')}:`}
+ showError={gasFeeError}
+ errorType={'gasFee'}
+ >
<GasFeeDisplay
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
@@ -36,7 +46,3 @@ export default class SendGasRow extends Component {
}
}
-
-SendGasRow.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
index 20d3daa59..8f8e3e4dd 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
@@ -1,10 +1,10 @@
import { connect } from 'react-redux'
import {
getConversionRate,
- getConvertedCurrency,
+ getCurrentCurrency,
getGasTotal,
} from '../../send.selectors.js'
-import { sendGasIsInError } from './send-gas-row.selectors.js'
+import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js'
import { showModal } from '../../../../actions'
import SendGasRow from './send-gas-row.component'
@@ -13,9 +13,10 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
function mapStateToProps (state) {
return {
conversionRate: getConversionRate(state),
- convertedCurrency: getConvertedCurrency(state),
+ convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state),
- gasLoadingError: sendGasIsInError(state),
+ gasFeeError: gasFeeIsInError(state),
+ gasLoadingError: getGasLoadingError(state),
}
}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
new file mode 100644
index 000000000..96f6293c2
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
@@ -0,0 +1,14 @@
+const selectors = {
+ gasFeeIsInError,
+ getGasLoadingError,
+}
+
+module.exports = selectors
+
+function getGasLoadingError (state) {
+ return state.send.errors.gasLoading
+}
+
+function gasFeeIsInError (state) {
+ return Boolean(state.send.errors.gasFee)
+}
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
index e4f05d708..54a92bd2d 100644
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
@@ -5,7 +5,7 @@ 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 '../../../../send/gas-fee-display-v2'
+import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
const propsMethodSpies = {
showCustomizeGasModal: sinon.spy(),
@@ -18,6 +18,7 @@ describe('SendGasRow Component', function () {
wrapper = shallow(<SendGasRow
conversionRate={20}
convertedCurrency={'mockConvertedCurrency'}
+ gasFeeError={'mockGasFeeError'}
gasLoadingError={false}
gasTotal={'mockGasTotal'}
showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
@@ -36,9 +37,13 @@ describe('SendGasRow Component', function () {
it('should pass the correct props to SendRowWrapper', () => {
const {
label,
+ showError,
+ errorType,
} = wrapper.find(SendRowWrapper).props()
assert.equal(label, 'gasFee_t:')
+ assert.equal(showError, 'mockGasFeeError')
+ assert.equal(errorType, 'gasFee')
})
it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => {
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
index 9135524d1..2ce062505 100644
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
@@ -19,10 +19,13 @@ proxyquire('../send-gas-row.container.js', {
},
'../../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`,
- getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
+ getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
},
- './send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` },
+ './send-gas-row.selectors.js': {
+ getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
+ gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
+ },
'../../../../actions': actionSpies,
})
@@ -35,6 +38,7 @@ describe('send-gas-row container', () => {
conversionRate: 'mockConversionRate:mockState',
convertedCurrency: 'mockConvertedCurrency:mockState',
gasTotal: 'mockGasTotal:mockState',
+ gasFeeError: 'mockGasFeeError:mockState',
gasLoadingError: 'mockGasLoadingError:mockState',
})
})
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
new file mode 100644
index 000000000..d46dd9d8b
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
@@ -0,0 +1,49 @@
+import assert from 'assert'
+import {
+ gasFeeIsInError,
+ getGasLoadingError,
+} from '../send-gas-row.selectors.js'
+
+describe('send-gas-row selectors', () => {
+
+ describe('getGasLoadingError()', () => {
+ it('should return send.errors.gasLoading', () => {
+ const state = {
+ send: {
+ errors: {
+ gasLoading: 'abc',
+ },
+ },
+ }
+
+ assert.equal(getGasLoadingError(state), 'abc')
+ })
+ })
+
+ describe('gasFeeIsInError()', () => {
+ it('should return true if send.errors.gasFee is truthy', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: 'def',
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), true)
+ })
+
+ it('should return false send.errors.gasFee is falsely', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: null,
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), false)
+ })
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/send/send-content/send-hex-data-row/index.js
new file mode 100644
index 000000000..08c341067
--- /dev/null
+++ b/ui/app/components/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/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
new file mode 100644
index 000000000..063930db3
--- /dev/null
+++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
@@ -0,0 +1,40 @@
+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,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ onInput = (event) => {
+ const {updateSendHexData} = this.props
+ event.target.value = event.target.value.replace(/\n/g, '')
+ updateSendHexData(event.target.value || null)
+ }
+
+ render () {
+ const {inError} = this.props
+ const {t} = this.context
+
+ return (
+ <SendRowWrapper
+ label={`${t('hexData')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ <textarea
+ onInput={this.onInput}
+ placeholder="Optional"
+ className="send-v2__hex-data__input"
+ />
+ </SendRowWrapper>
+ )
+ }
+}
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
new file mode 100644
index 000000000..df554ca5f
--- /dev/null
+++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import {
+ updateSendHexData,
+} from '../../../../actions'
+import SendHexDataRow from './send-hex-data-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow)
+
+function mapStateToProps (state) {
+ return {
+ data: state.metamask.send.data,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateSendHexData (data) {
+ return dispatch(updateSendHexData(data))
+ },
+ }
+}
diff --git a/ui/app/components/send/send-content/send-row-wrapper/index.js b/ui/app/components/send/send-content/send-row-wrapper/index.js
new file mode 100644
index 000000000..d17545dcc
--- /dev/null
+++ b/ui/app/components/send/send-content/send-row-wrapper/index.js
@@ -0,0 +1 @@
+export { default } from './send-row-wrapper.component'
diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js
new file mode 100644
index 000000000..c00617f83
--- /dev/null
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js
@@ -0,0 +1 @@
+export { default } from './send-row-error-message.container'
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
index 0d314208b..61bc7bab7 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
@@ -8,6 +8,10 @@ export default class SendRowErrorMessage extends Component {
errorType: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const { errors, errorType } = this.props
@@ -21,7 +25,3 @@ export default class SendRowErrorMessage extends Component {
}
}
-
-SendRowErrorMessage.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
index 59622047f..59622047f 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
index 2304a43d2..2304a43d2 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
index eecff165d..eecff165d 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
index f484bd8d9..b7528a15f 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
@@ -11,6 +11,10 @@ export default class SendRowWrapper extends Component {
showError: PropTypes.bool,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
children,
@@ -37,7 +41,3 @@ export default class SendRowWrapper extends Component {
}
}
-
-SendRowWrapper.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
index 30280e1d0..30280e1d0 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
diff --git a/ui/app/components/send/send-content/send-to-row/index.js b/ui/app/components/send/send-content/send-to-row/index.js
new file mode 100644
index 000000000..121f15148
--- /dev/null
+++ b/ui/app/components/send/send-content/send-to-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-to-row.container'
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send/send-content/send-to-row/send-to-row-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row-README.md
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 0a83186a5..892ad5d67 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -19,9 +19,13 @@ export default class SendToRow extends Component {
updateSendToError: PropTypes.func,
};
- handleToChange (to, nickname = '') {
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ handleToChange (to, nickname = '', toError) {
const { updateSendTo, updateSendToError, updateGas } = this.props
- const toErrorObject = getToErrorObject(to)
+ const toErrorObject = getToErrorObject(to, toError)
updateSendTo(to, nickname)
updateSendToError(toErrorObject)
if (toErrorObject.to === null) {
@@ -53,7 +57,7 @@ export default class SendToRow extends Component {
inError={inError}
name={'address'}
network={network}
- onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)}
+ onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)}
openDropdown={() => openToDropdown()}
placeholder={this.context.t('recipientAddress')}
to={to}
@@ -63,8 +67,3 @@ export default class SendToRow extends Component {
}
}
-
-SendToRow.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
index 1c9c9d518..1c9c9d518 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
index 8919014be..8919014be 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
index cea51ee20..6b90a9f09 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
@@ -4,12 +4,10 @@ const {
} = require('../../send.constants')
const { isValidAddress } = require('../../../../util')
-function getToErrorObject (to) {
- let toError = null
-
+function getToErrorObject (to, toError = null) {
if (!to) {
toError = REQUIRED_ERROR
- } else if (!isValidAddress(to)) {
+ } else if (!isValidAddress(to) && !toError) {
toError = INVALID_RECIPIENT_ADDRESS_ERROR
}
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
index 58fe51dcf..781371004 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
@@ -6,8 +6,8 @@ import proxyquire from 'proxyquire'
const SendToRow = proxyquire('../send-to-row.component.js', {
'./send-to-row.utils.js': {
- getToErrorObject: (to) => ({
- to: to === false ? null : `mockToErrorObject:${to}`,
+ getToErrorObject: (to, toError) => ({
+ to: to === false ? null : `mockToErrorObject:${to}${toError}`,
}),
},
}).default
@@ -67,11 +67,11 @@ describe('SendToRow Component', function () {
it('should call updateSendToError', () => {
assert.equal(propsMethodSpies.updateSendToError.callCount, 0)
- instance.handleToChange('mockTo2')
+ instance.handleToChange('mockTo2', '', 'mockToError')
assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendToError.getCall(0).args,
- [{ to: 'mockToErrorObject:mockTo2' }]
+ [{ to: 'mockToErrorObject:mockTo2mockToError' }]
)
})
@@ -138,11 +138,11 @@ describe('SendToRow Component', function () {
openDropdown()
assert.equal(propsMethodSpies.openToDropdown.callCount, 1)
assert.equal(SendToRow.prototype.handleToChange.callCount, 0)
- onChange('mockNewTo', 'mockNewNickname')
+ onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' })
assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
assert.deepEqual(
SendToRow.prototype.handleToChange.getCall(0).args,
- ['mockNewTo', 'mockNewNickname']
+ ['mockNewTo', 'mockNewNickname', 'mockToError']
)
})
})
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
index 92355c00a..92355c00a 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
index 122ad3265..122ad3265 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
index 615c9581b..4d2447c32 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
@@ -40,6 +40,12 @@ describe('send-to-row utils', () => {
to: null,
})
})
+
+ it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => {
+ assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), {
+ to: 'someExplicitError',
+ })
+ })
})
})
diff --git a/ui/app/components/send_/send-content/tests/send-content-component.test.js b/ui/app/components/send/send-content/tests/send-content-component.test.js
index d5bb6693c..d5bb6693c 100644
--- a/ui/app/components/send_/send-content/tests/send-content-component.test.js
+++ b/ui/app/components/send/send-content/tests/send-content-component.test.js
diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send/send-footer/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-footer/README.md
+++ b/ui/app/components/send/send-footer/README.md
diff --git a/ui/app/components/send/send-footer/index.js b/ui/app/components/send/send-footer/index.js
new file mode 100644
index 000000000..58e91d622
--- /dev/null
+++ b/ui/app/components/send/send-footer/index.js
@@ -0,0 +1 @@
+export { default } from './send-footer.container'
diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js
index 6471ae1a3..518cff06e 100644
--- a/ui/app/components/send_/send-footer/send-footer.component.js
+++ b/ui/app/components/send/send-footer/send-footer.component.js
@@ -8,6 +8,7 @@ export default class SendFooter extends Component {
static propTypes = {
addToAddressBookIfNew: PropTypes.func,
amount: PropTypes.string,
+ data: PropTypes.string,
clearSend: PropTypes.func,
disabled: PropTypes.bool,
editingTransactionId: PropTypes.string,
@@ -27,6 +28,10 @@ export default class SendFooter extends Component {
update: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
onCancel () {
this.props.clearSend()
this.props.history.push(DEFAULT_ROUTE)
@@ -37,6 +42,7 @@ export default class SendFooter extends Component {
const {
addToAddressBookIfNew,
amount,
+ data,
editingTransactionId,
from: {address: from},
gasLimit: gas,
@@ -48,6 +54,7 @@ export default class SendFooter extends Component {
// updateTx,
update,
toAccounts,
+ history,
} = this.props
// Should not be needed because submit should be disabled if there are errors.
@@ -60,9 +67,10 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts)
- editingTransactionId
+ const promise = editingTransactionId
? update({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -71,9 +79,10 @@ export default class SendFooter extends Component {
to,
unapprovedTxs,
})
- : sign({ selectedToken, to, amount, from, gas, gasPrice })
+ : sign({ data, selectedToken, to, amount, from, gas, gasPrice })
- this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
+ Promise.resolve(promise)
+ .then(() => history.push(CONFIRM_TRANSACTION_ROUTE))
}
formShouldBeDisabled () {
@@ -93,7 +102,3 @@ export default class SendFooter extends Component {
}
}
-
-SendFooter.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js
index 260ff40bc..60de4d030 100644
--- a/ui/app/components/send_/send-footer/send-footer.container.js
+++ b/ui/app/components/send/send-footer/send-footer.container.js
@@ -18,6 +18,7 @@ import {
getSendFromObject,
getSendTo,
getSendToAccounts,
+ getSendHexData,
getTokenBalance,
getUnapprovedTxs,
} from '../send.selectors'
@@ -35,6 +36,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
function mapStateToProps (state) {
return {
amount: getSendAmount(state),
+ data: getSendHexData(state),
editingTransactionId: getSendEditingTransactionId(state),
from: getSendFromObject(state),
gasLimit: getGasLimit(state),
@@ -52,9 +54,10 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
clearSend: () => dispatch(clearSend()),
- sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => {
+ sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => {
const txParams = constructTxParams({
amount,
+ data,
from,
gas,
gasPrice,
@@ -68,6 +71,7 @@ function mapDispatchToProps (dispatch) {
},
update: ({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -78,6 +82,7 @@ function mapDispatchToProps (dispatch) {
}) => {
const editingTx = constructUpdatedTx({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -87,7 +92,7 @@ function mapDispatchToProps (dispatch) {
unapprovedTxs,
})
- dispatch(updateTransaction(editingTx))
+ return dispatch(updateTransaction(editingTx))
},
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send/send-footer/send-footer.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-footer/send-footer.scss
+++ b/ui/app/components/send/send-footer/send-footer.scss
diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send/send-footer/send-footer.selectors.js
index e20addfdc..e20addfdc 100644
--- a/ui/app/components/send_/send-footer/send-footer.selectors.js
+++ b/ui/app/components/send/send-footer/send-footer.selectors.js
diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send/send-footer/send-footer.utils.js
index 875e7d948..f82ff1e9b 100644
--- a/ui/app/components/send_/send-footer/send-footer.utils.js
+++ b/ui/app/components/send/send-footer/send-footer.utils.js
@@ -8,8 +8,9 @@ function addHexPrefixToObjectValues (obj) {
}, {})
}
-function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) {
+function constructTxParams ({ selectedToken, data, to, amount, from, gas, gasPrice }) {
const txParams = {
+ data,
from,
value: '0',
gas,
@@ -21,13 +22,12 @@ function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice })
txParams.to = to
}
- const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams)
-
- return hexPrefixedTxParams
+ return addHexPrefixToObjectValues(txParams)
}
function constructUpdatedTx ({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -36,9 +36,21 @@ function constructUpdatedTx ({
to,
unapprovedTxs,
}) {
+ const unapprovedTx = unapprovedTxs[editingTransactionId]
+ const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data
const editingTx = {
- ...unapprovedTxs[editingTransactionId],
- txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }),
+ ...unapprovedTx,
+ txParams: Object.assign(
+ unapprovedTx.txParams,
+ addHexPrefixToObjectValues({
+ data: txParamsData,
+ to,
+ from,
+ gas,
+ gasPrice,
+ value: amount,
+ })
+ ),
}
if (selectedToken) {
@@ -52,18 +64,10 @@ function constructUpdatedTx ({
to: selectedToken.address,
data,
}))
- } else {
- const { data } = unapprovedTxs[editingTransactionId].txParams
-
- Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
- value: amount,
- to,
- data,
- }))
+ }
- if (typeof editingTx.txParams.data === 'undefined') {
- delete editingTx.txParams.data
- }
+ if (typeof editingTx.txParams.data === 'undefined') {
+ delete editingTx.txParams.data
}
return editingTx
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
index e071fe54f..65e4bb654 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
@@ -129,6 +129,7 @@ describe('SendFooter Component', function () {
assert.deepEqual(
propsMethodSpies.update.getCall(0).args[0],
{
+ data: undefined,
amount: 'mockAmount',
editingTransactionId: 'mockEditingTransactionId',
from: 'mockAddress',
@@ -152,6 +153,7 @@ describe('SendFooter Component', function () {
assert.deepEqual(
propsMethodSpies.sign.getCall(0).args[0],
{
+ data: undefined,
amount: 'mockAmount',
from: 'mockAddress',
gas: 'mockGasLimit',
@@ -166,10 +168,13 @@ describe('SendFooter Component', function () {
assert.equal(propsMethodSpies.update.callCount, 0)
})
- it('should call history.push', () => {
- wrapper.instance().onSubmit(MOCK_EVENT)
- assert.equal(historySpies.push.callCount, 1)
- assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ it('should call history.push', done => {
+ Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT))
+ .then(() => {
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ done()
+ })
})
})
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
index 39d6a7686..cf4c893ee 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
@@ -38,6 +38,7 @@ proxyquire('../send-footer.container.js', {
getSendTo: (s) => `mockTo:${s}`,
getSendToAccounts: (s) => `mockToAccounts:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getSendHexData: (s) => `mockHexData:${s}`,
getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
},
'./send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
@@ -51,6 +52,7 @@ describe('send-footer container', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
amount: 'mockAmount:mockState',
+ data: 'mockHexData:mockState',
selectedToken: 'mockSelectedToken:mockState',
editingTransactionId: 'mockEditingTransactionId:mockState',
from: 'mockFromObject:mockState',
@@ -100,6 +102,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructTxParams.getCall(0).args[0],
{
+ data: undefined,
selectedToken: {
address: '0xabc',
},
@@ -129,6 +132,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructTxParams.getCall(0).args[0],
{
+ data: undefined,
selectedToken: undefined,
to: 'mockTo',
amount: 'mockAmount',
@@ -160,6 +164,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructUpdatedTx.getCall(0).args[0],
{
+ data: undefined,
to: 'mockTo',
amount: 'mockAmount',
from: 'mockFrom',
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js
index 8de032f57..8de032f57 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js
index 2d3135995..28ff0c891 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js
@@ -65,6 +65,28 @@ describe('send-footer utils', () => {
})
describe('constructTxParams()', () => {
+ it('should return a new txParams object with data if there data is given', () => {
+ assert.deepEqual(
+ constructTxParams({
+ data: 'someData',
+ selectedToken: false,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }),
+ {
+ data: '0xsomeData',
+ to: '0xmockTo',
+ value: '0xmockAmount',
+ from: '0xmockFrom',
+ gas: '0xmockGas',
+ gasPrice: '0xmockGasPrice',
+ }
+ )
+ })
+
it('should return a new txParams object with value and to properties if there is no selectedToken', () => {
assert.deepEqual(
constructTxParams({
@@ -76,6 +98,7 @@ describe('send-footer utils', () => {
gasPrice: 'mockGasPrice',
}),
{
+ data: undefined,
to: '0xmockTo',
value: '0xmockAmount',
from: '0xmockFrom',
@@ -96,6 +119,7 @@ describe('send-footer utils', () => {
gasPrice: 'mockGasPrice',
}),
{
+ data: undefined,
value: '0x0',
from: '0xmockFrom',
gas: '0xmockGas',
diff --git a/ui/app/components/send_/send-header/README.md b/ui/app/components/send/send-header/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-header/README.md
+++ b/ui/app/components/send/send-header/README.md
diff --git a/ui/app/components/send/send-header/index.js b/ui/app/components/send/send-header/index.js
new file mode 100644
index 000000000..0b17f0b7d
--- /dev/null
+++ b/ui/app/components/send/send-header/index.js
@@ -0,0 +1 @@
+export { default } from './send-header.container'
diff --git a/ui/app/components/send_/send-header/send-header.component.js b/ui/app/components/send/send-header/send-header.component.js
index 5f6617fce..efc4bbf27 100644
--- a/ui/app/components/send_/send-header/send-header.component.js
+++ b/ui/app/components/send/send-header/send-header.component.js
@@ -12,6 +12,10 @@ export default class SendHeader extends Component {
subtitleParams: PropTypes.array,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
onClose () {
this.props.clearSend()
this.props.history.push(DEFAULT_ROUTE)
@@ -28,7 +32,3 @@ export default class SendHeader extends Component {
}
}
-
-SendHeader.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-header/send-header.container.js b/ui/app/components/send/send-header/send-header.container.js
index 4bcd0d1b6..4bcd0d1b6 100644
--- a/ui/app/components/send_/send-header/send-header.container.js
+++ b/ui/app/components/send/send-header/send-header.container.js
diff --git a/ui/app/components/send_/send-header/send-header.selectors.js b/ui/app/components/send/send-header/send-header.selectors.js
index d7c9d3766..d7c9d3766 100644
--- a/ui/app/components/send_/send-header/send-header.selectors.js
+++ b/ui/app/components/send/send-header/send-header.selectors.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-component.test.js b/ui/app/components/send/send-header/tests/send-header-component.test.js
index 930bfa387..930bfa387 100644
--- a/ui/app/components/send_/send-header/tests/send-header-component.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-component.test.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-container.test.js b/ui/app/components/send/send-header/tests/send-header-container.test.js
index 41a7e8a89..41a7e8a89 100644
--- a/ui/app/components/send_/send-header/tests/send-header-container.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-container.test.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js b/ui/app/components/send/send-header/tests/send-header-selectors.test.js
index e0c6a3ab3..e0c6a3ab3 100644
--- a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-selectors.test.js
diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send/send.component.js
index 219b362f2..6f1b20c55 100644
--- a/ui/app/components/send_/send.component.js
+++ b/ui/app/components/send/send.component.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import PersistentForm from '../../../lib/persistent-form'
import {
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
doesAmountErrorRequireUpdate,
} from './send.utils'
@@ -39,6 +40,10 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
updateGas ({ to: updatedToAddress, amount: value } = {}) {
const {
amount,
@@ -112,7 +117,19 @@ export default class SendTransactionScreen extends PersistentForm {
selectedToken,
tokenBalance,
})
- updateSendErrors(amountErrorObject)
+ const gasFeeErrorObject = selectedToken
+ ? getGasFeeErrorObject({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ : { gasFee: null }
+ updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject))
}
if (!uninitialized) {
@@ -143,6 +160,10 @@ export default class SendTransactionScreen extends PersistentForm {
this.updateGas()
}
+ componentWillUnmount () {
+ this.props.resetSendState()
+ }
+
render () {
const { history } = this.props
@@ -156,7 +177,3 @@ export default class SendTransactionScreen extends PersistentForm {
}
}
-
-SendTransactionScreen.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send/send.constants.js
index df5dee371..8acdf0641 100644
--- a/ui/app/components/send_/send.constants.js
+++ b/ui/app/components/send/send.constants.js
@@ -36,6 +36,7 @@ const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
}))
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
+const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
module.exports = {
INSUFFICIENT_FUNDS_ERROR,
@@ -52,4 +53,5 @@ module.exports = {
REQUIRED_ERROR,
SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
+ BASE_TOKEN_GAS_COST,
}
diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send/send.container.js
index 185653c5f..44ebd2792 100644
--- a/ui/app/components/send_/send.container.js
+++ b/ui/app/components/send/send.container.js
@@ -28,6 +28,7 @@ import {
setGasTotal,
} from '../../actions'
import {
+ resetSendState,
updateSendErrors,
} from '../../ducks/send.duck'
import {
@@ -87,5 +88,6 @@ function mapDispatchToProps (dispatch) {
}))
},
updateSendErrors: newError => dispatch(updateSendErrors(newError)),
+ resetSendState: () => dispatch(resetSendState()),
}
}
diff --git a/ui/app/components/send_/send.scss b/ui/app/components/send/send.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send.scss
+++ b/ui/app/components/send/send.scss
diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send/send.selectors.js
index 7e7cfe2e9..cf07eafe1 100644
--- a/ui/app/components/send_/send.selectors.js
+++ b/ui/app/components/send/send.selectors.js
@@ -14,7 +14,6 @@ const selectors = {
getAmountConversionRate,
getBlockGasLimit,
getConversionRate,
- getConvertedCurrency,
getCurrentAccountWithSendEtherInfo,
getCurrentCurrency,
getCurrentNetwork,
@@ -34,6 +33,7 @@ const selectors = {
getSelectedTokenExchangeRate,
getSelectedTokenToFiatRate,
getSendAmount,
+ getSendHexData,
getSendEditingTransactionId,
getSendErrors,
getSendFrom,
@@ -98,10 +98,6 @@ function getConversionRate (state) {
return state.metamask.conversionRate
}
-function getConvertedCurrency (state) {
- return state.metamask.currentCurrency
-}
-
function getCurrentAccountWithSendEtherInfo (state) {
const currentAddress = getSelectedAddress(state)
const accounts = accountsWithSendEtherInfoSelector(state)
@@ -215,6 +211,10 @@ function getSendAmount (state) {
return state.metamask.send.amount
}
+function getSendHexData (state) {
+ return state.metamask.send.data
+}
+
function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId
}
diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send/send.utils.js
index dfd459731..aa255c3d4 100644
--- a/ui/app/components/send_/send.utils.js
+++ b/ui/app/components/send/send.utils.js
@@ -10,6 +10,7 @@ const {
calcTokenAmount,
} = require('../../token-util')
const {
+ BASE_TOKEN_GAS_COST,
INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR,
NEGATIVE_ETH_ERROR,
@@ -29,12 +30,14 @@ module.exports = {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
isBalanceSufficient,
isTokenBalanceSufficient,
+ removeLeadingZeroes,
}
-function calcGasTotal (gasLimit, gasPrice) {
+function calcGasTotal (gasLimit = '0', gasPrice = '0') {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
@@ -44,9 +47,9 @@ function calcGasTotal (gasLimit, gasPrice) {
function isBalanceSufficient ({
amount = '0x0',
- amountConversionRate = 0,
- balance,
- conversionRate,
+ amountConversionRate = 1,
+ balance = '0x0',
+ conversionRate = 1,
gasTotal = '0x0',
primaryCurrency,
}) {
@@ -108,9 +111,9 @@ function getAmountErrorObject ({
tokenBalance,
}) {
let insufficientFunds = false
- if (gasTotal && conversionRate) {
+ if (gasTotal && conversionRate && !selectedToken) {
insufficientFunds = !isBalanceSufficient({
- amount: selectedToken ? '0x0' : amount,
+ amount,
amountConversionRate,
balance,
conversionRate,
@@ -147,6 +150,34 @@ function getAmountErrorObject ({
return { amount: amountError }
}
+function getGasFeeErrorObject ({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+}) {
+ let gasFeeError = null
+
+ if (gasTotal && conversionRate) {
+ const insufficientFunds = !isBalanceSufficient({
+ amount: '0x0',
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ })
+
+ if (insufficientFunds) {
+ gasFeeError = INSUFFICIENT_FUNDS_ERROR
+ }
+ }
+
+ return { gasFee: gasFeeError }
+}
+
function calcTokenBalance ({ selectedToken, usersToken }) {
const { decimals } = selectedToken || {}
return calcTokenAmount(usersToken.balance.toString(), decimals) + ''
@@ -183,6 +214,8 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
if (!code || code === '0x') {
return SIMPLE_GAS_COST
}
+ } else if (selectedToken && !to) {
+ return BASE_TOKEN_GAS_COST
}
paramsForGasEstimate.to = selectedToken ? selectedToken.address : to
@@ -273,3 +306,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) {
function getToAddressForGasUpdate (...addresses) {
return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase()
}
+
+function removeLeadingZeroes (str) {
+ return str.replace(/^0*(?=\d)/, '')
+}
diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js
index 4ba9b226d..6194ec508 100644
--- a/ui/app/components/send_/tests/send-component.test.js
+++ b/ui/app/components/send/tests/send-component.test.js
@@ -12,9 +12,11 @@ const propsMethodSpies = {
updateAndSetGasTotal: sinon.spy(),
updateSendErrors: sinon.spy(),
updateSendTokenBalance: sinon.spy(),
+ resetSendState: sinon.spy(),
}
const utilsMethodStubs = {
getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
+ getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }),
doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance),
}
@@ -50,6 +52,7 @@ describe('Send Component', function () {
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
updateSendErrors={propsMethodSpies.updateSendErrors}
updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
+ resetSendState={propsMethodSpies.resetSendState}
/>)
})
@@ -58,6 +61,7 @@ describe('Send Component', function () {
SendTransactionScreen.prototype.updateGas.resetHistory()
utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
utilsMethodStubs.getAmountErrorObject.resetHistory()
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
propsMethodSpies.updateAndSetGasTotal.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
propsMethodSpies.updateSendTokenBalance.resetHistory()
@@ -77,6 +81,15 @@ describe('Send Component', function () {
})
})
+ describe('componentWillUnmount', () => {
+ it('should call this.props.resetSendState', () => {
+ propsMethodSpies.resetSendState.resetHistory()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 0)
+ wrapper.instance().componentWillUnmount()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 1)
+ })
+ })
+
describe('componentDidUpdate', () => {
it('should call doesAmountErrorRequireUpdate with the expected params', () => {
utilsMethodStubs.getAmountErrorObject.resetHistory()
@@ -133,8 +146,66 @@ describe('Send Component', function () {
)
})
- it('should call updateSendErrors with the expected params', () => {
+ it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1)
+ assert.deepEqual(
+ utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 10,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: { address: 'mockAddress', balance: 'mockBalance' },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is falsy', () => {
+ propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendErrors.getCall(0).args[0],
+ { amount: 'mockAmountError', gasFee: null }
+ )
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is truthy', () => {
propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: 'someToken' })
wrapper.instance().componentDidUpdate({
from: {
balance: 'balanceChanged',
@@ -143,7 +214,7 @@ describe('Send Component', function () {
assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendErrors.getCall(0).args[0],
- { amount: 'mockAmountError'}
+ { amount: 'mockAmountError', gasFee: 'mockGasFeeError' }
)
})
diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js
index 91484f4d8..7a9120d24 100644
--- a/ui/app/components/send_/tests/send-container.test.js
+++ b/ui/app/components/send/tests/send-container.test.js
@@ -12,6 +12,7 @@ const actionSpies = {
}
const duckActionSpies = {
updateSendErrors: sinon.spy(),
+ resetSendState: sinon.spy(),
}
proxyquire('../send.container.js', {
@@ -152,6 +153,17 @@ describe('send container', () => {
})
})
+ describe('resetSendState()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.resetSendState()
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ duckActionSpies.resetSendState.getCall(0).args.length,
+ 0
+ )
+ })
+ })
+
})
})
diff --git a/ui/app/components/send_/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js
index 8f9c19314..8f9c19314 100644
--- a/ui/app/components/send_/tests/send-selectors-test-data.js
+++ b/ui/app/components/send/tests/send-selectors-test-data.js
diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js
index 152af8059..218da656b 100644
--- a/ui/app/components/send_/tests/send-selectors.test.js
+++ b/ui/app/components/send/tests/send-selectors.test.js
@@ -8,7 +8,6 @@ const {
getBlockGasLimit,
getAmountConversionRate,
getConversionRate,
- getConvertedCurrency,
getCurrentAccountWithSendEtherInfo,
getCurrentCurrency,
getCurrentNetwork,
@@ -154,15 +153,6 @@ describe('send selectors', () => {
})
})
- describe('getConvertedCurrency()', () => {
- it('should return the currently selected currency', () => {
- assert.equal(
- getConvertedCurrency(mockState),
- 'USD'
- )
- })
- })
-
describe('getCurrentAccountWithSendEtherInfo()', () => {
it('should return the currently selected account with identity info', () => {
assert.deepEqual(
diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js
index f3d5674b7..18dde495a 100644
--- a/ui/app/components/send_/tests/send-utils.test.js
+++ b/ui/app/components/send/tests/send-utils.test.js
@@ -2,6 +2,7 @@ import assert from 'assert'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import {
+ BASE_TOKEN_GAS_COST,
ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST,
} from '../send.constants'
@@ -16,7 +17,11 @@ const {
} = require('../send.constants')
const stubs = {
- addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
+ addCurrencies: sinon.stub().callsFake((a, b, obj) => {
+ if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2))
+ if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2))
+ return a + b
+ }),
conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
@@ -48,10 +53,12 @@ const {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
calcTokenBalance,
isBalanceSufficient,
isTokenBalanceSufficient,
+ removeLeadingZeroes,
} = sendUtils
describe('send utils', () => {
@@ -142,6 +149,18 @@ describe('send utils', () => {
primaryCurrency: 'ABC',
expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
},
+ 'should not return insufficientFunds error if selectedToken is truthy': {
+ amount: '0x0',
+ amountConversionRate: 2,
+ balance: 1,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ selectedToken: { symbole: 'DEF', decimals: 0 },
+ decimals: 0,
+ tokenBalance: 'sometokenbalance',
+ expectedResult: { amount: null },
+ },
'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
amount: '0x10',
amountConversionRate: 2,
@@ -162,6 +181,32 @@ describe('send utils', () => {
})
})
+ describe('getGasFeeErrorObject()', () => {
+ const config = {
+ 'should return insufficientFunds error if isBalanceSufficient returns false': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR },
+ },
+ 'should return null error if isBalanceSufficient returns true': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 15,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: null },
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult)
+ })
+ })
+ })
+
describe('calcTokenBalance()', () => {
it('should return the calculated token blance', () => {
assert.equal(calcTokenBalance({
@@ -221,6 +266,7 @@ describe('send utils', () => {
describe('isTokenBalanceSufficient()', () => {
it('should correctly call conversionUtil and return the result of calling conversionGTE', () => {
stubs.conversionGTE.resetHistory()
+ stubs.conversionUtil.resetHistory()
const result = isTokenBalanceSufficient({
amount: '0x10',
tokenBalance: 123,
@@ -336,6 +382,11 @@ describe('send utils', () => {
assert.notEqual(result, SIMPLE_GAS_COST)
})
+ it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } }))
+ assert.equal(result, BASE_TOKEN_GAS_COST)
+ })
+
it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
const result = await estimateGas(Object.assign({}, baseMockParams, {
to: 'isContract willFailBecauseOf:Transaction execution error.',
@@ -433,4 +484,29 @@ describe('send utils', () => {
assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
})
})
+
+ describe('removeLeadingZeroes()', () => {
+ it('should remove leading zeroes from int when user types', () => {
+ assert.equal(removeLeadingZeroes('0'), '0')
+ assert.equal(removeLeadingZeroes('1'), '1')
+ assert.equal(removeLeadingZeroes('00'), '0')
+ assert.equal(removeLeadingZeroes('01'), '1')
+ })
+
+ it('should remove leading zeroes from int when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('001'), '1')
+ })
+
+ it('should remove leading zeroes from float when user types', () => {
+ assert.equal(removeLeadingZeroes('0.'), '0.')
+ assert.equal(removeLeadingZeroes('0.0'), '0.0')
+ assert.equal(removeLeadingZeroes('0.00'), '0.00')
+ assert.equal(removeLeadingZeroes('0.001'), '0.001')
+ assert.equal(removeLeadingZeroes('0.10'), '0.10')
+ })
+
+ it('should remove leading zeroes from float when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('00.1'), '0.1')
+ })
+ })
})
diff --git a/ui/app/components/send/to-autocomplete.component.js b/ui/app/components/send/to-autocomplete.component.js
new file mode 100644
index 000000000..9e270db75
--- /dev/null
+++ b/ui/app/components/send/to-autocomplete.component.js
@@ -0,0 +1,141 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import AccountListItem from '../send/account-list-item/account-list-item.component'
+
+
+export default class ToAutoComplete extends Component {
+
+ static propTypes = {
+ dropdownOpen: PropTypes.bool,
+ openDropdown: PropTypes.func,
+ closeDropdown: PropTypes.func,
+ onChange: PropTypes.func,
+ to: PropTypes.string,
+ accounts: PropTypes.array,
+ inError: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ accountsToRender: [],
+ }
+
+ getListItemIcon (listItemAddress, toAddress) {
+ return toAddress && listItemAddress === toAddress
+ ? <i className={'fa fa-check fa-lg'}
+ style={{
+ color: '#02c9b1',
+ }}
+ />
+ : null
+ }
+
+ renderDropdown () {
+ const {
+ closeDropdown,
+ onChange,
+ to,
+ } = this.props
+ const {accountsToRender} = this.state
+
+ if (!accountsToRender.length) {
+ return null
+ }
+
+ return (
+ <div>
+ <div className={'send-v2__from-dropdown__close-area'} onClick={closeDropdown} />
+ <div className={'send-v2__from-dropdown__list'}>
+ {accountsToRender.map((account, i) => (
+ <AccountListItem
+ key={i}
+ account={account}
+ className={'account-list-item__dropdown'}
+ handleClick={() => {
+ onChange(account.address)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account.address, to)}
+ displayBalance={false}
+ displayAddress={true}
+ />
+ ))}
+ </div>
+ </div>
+ )
+ }
+
+ handleInputEvent (event = {}, cb) {
+ const {
+ to,
+ accounts,
+ closeDropdown,
+ openDropdown,
+ } = this.props
+
+ const matchingAccounts = accounts.filter(({address}) => address.match(to || ''))
+ const matches = matchingAccounts.length
+
+ if (!matches || matchingAccounts[0].address === to) {
+ this.setState({accountsToRender: []})
+ event.target && event.target.select()
+ closeDropdown()
+ } else {
+ this.setState({accountsToRender: matchingAccounts})
+ openDropdown()
+ }
+ cb && cb(event.target.value)
+ }
+
+ componentDidUpdate (nextProps) {
+ if (this.props.to !== nextProps.to) {
+ this.handleInputEvent()
+ }
+ }
+
+ render () {
+ const {
+ to,
+ dropdownOpen,
+ onChange,
+ inError,
+ } = this.props
+
+ return (
+ <div className={'send-v2__to-autocomplete'}>
+ <input
+ className={classnames('send-v2__to-autocomplete__input', {
+ 'send-v2__error-border': inError,
+ })}
+ placeholder={this.context.t('recipientAddress')}
+ value={to}
+ onChange={event => onChange(event.target.value)}
+ onFocus={event => this.handleInputEvent(event)}
+ style={{
+ borderColor: inError ? 'red' : null,
+ }}
+ />
+ {
+ to
+ ? null
+ : <i className={'fa fa-caret-down fa-lg send-v2__to-autocomplete__down-caret'}
+ onClick={() => this.handleInputEvent()}
+ style={{
+ style: {color: '#dedede'},
+ }}
+ />
+ }
+ {
+ dropdownOpen
+ ? this.renderDropdown()
+ : null
+ }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/send/to-autocomplete/index.js
new file mode 100644
index 000000000..244d301d1
--- /dev/null
+++ b/ui/app/components/send/to-autocomplete/index.js
@@ -0,0 +1 @@
+export { default } from './to-autocomplete.js'
diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js
index df74ef194..80cfa7a85 100644
--- a/ui/app/components/send/to-autocomplete.js
+++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js
@@ -2,7 +2,7 @@ const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
+const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
ToAutoComplete.contextTypes = {
diff --git a/ui/app/components/send_/account-list-item/index.js b/ui/app/components/send_/account-list-item/index.js
deleted file mode 100644
index 1fca540be..000000000
--- a/ui/app/components/send_/account-list-item/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './account-list-item.container' \ No newline at end of file
diff --git a/ui/app/components/send_/index.js b/ui/app/components/send_/index.js
deleted file mode 100644
index 9a4dd5727..000000000
--- a/ui/app/components/send_/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/index.js b/ui/app/components/send_/send-content/index.js
deleted file mode 100644
index 10b3c850e..000000000
--- a/ui/app/components/send_/send-content/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-content.component' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js
deleted file mode 100644
index 548b51f33..000000000
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './amount-max-button.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-amount-row/index.js b/ui/app/components/send_/send-content/send-amount-row/index.js
deleted file mode 100644
index 94a7da56f..000000000
--- a/ui/app/components/send_/send-content/send-amount-row/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-amount-row.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/index.js b/ui/app/components/send_/send-content/send-dropdown-list/index.js
deleted file mode 100644
index ee7736376..000000000
--- a/ui/app/components/send_/send-content/send-dropdown-list/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-dropdown-list.component' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js
deleted file mode 100644
index 6ab9a157a..000000000
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './from-dropdown.component' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-from-row/index.js b/ui/app/components/send_/send-content/send-from-row/index.js
deleted file mode 100644
index 4a0916dba..000000000
--- a/ui/app/components/send_/send-content/send-from-row/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-from-row.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-gas-row/index.js b/ui/app/components/send_/send-content/send-gas-row/index.js
deleted file mode 100644
index 060ed7fd3..000000000
--- a/ui/app/components/send_/send-content/send-gas-row/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-gas-row.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
deleted file mode 100644
index d069ae8c6..000000000
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const selectors = {
- sendGasIsInError,
-}
-
-module.exports = selectors
-
-function sendGasIsInError (state) {
- return state.send.errors.gasLoading
-}
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
deleted file mode 100644
index a5196334e..000000000
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import assert from 'assert'
-import {
- sendGasIsInError,
-} from '../send-gas-row.selectors.js'
-
-describe('send-gas-row selectors', () => {
-
- describe('sendGasIsInError()', () => {
- it('should return send.errors.gasLoading', () => {
- const state = {
- send: {
- errors: {
- gasLoading: 'abc',
- },
- },
- }
-
- assert.equal(sendGasIsInError(state), 'abc')
- })
- })
-
-})
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/index.js b/ui/app/components/send_/send-content/send-row-wrapper/index.js
deleted file mode 100644
index 5715f55c6..000000000
--- a/ui/app/components/send_/send-content/send-row-wrapper/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-row-wrapper.component' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js
deleted file mode 100644
index bf49c55bd..000000000
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-row-error-message.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-to-row/index.js b/ui/app/components/send_/send-content/send-to-row/index.js
deleted file mode 100644
index 4e7aa9747..000000000
--- a/ui/app/components/send_/send-content/send-to-row/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-to-row.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-footer/index.js b/ui/app/components/send_/send-footer/index.js
deleted file mode 100644
index cd1727330..000000000
--- a/ui/app/components/send_/send-footer/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-footer.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-header/index.js b/ui/app/components/send_/send-header/index.js
deleted file mode 100644
index b808eabbf..000000000
--- a/ui/app/components/send_/send-header/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './send-header.container' \ No newline at end of file
diff --git a/ui/app/components/sender-to-recipient.js b/ui/app/components/sender-to-recipient.js
deleted file mode 100644
index 9cef8e401..000000000
--- a/ui/app/components/sender-to-recipient.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const PropTypes = require('prop-types')
-const Identicon = require('./identicon')
-
-class SenderToRecipient extends Component {
- renderRecipientIcon () {
- const { recipientAddress } = this.props
- return (
- recipientAddress
- ? h(Identicon, { address: recipientAddress, diameter: 20 })
- : h('i.fa.fa-file-text-o')
- )
- }
-
- renderRecipient () {
- const { recipientName } = this.props
- return (
- h('.sender-to-recipient__recipient', [
- this.renderRecipientIcon(),
- h(
- '.sender-to-recipient__name.sender-to-recipient__recipient-name',
- recipientName || this.context.t('newContract')
- ),
- ])
- )
- }
-
- render () {
- const { senderName, senderAddress } = this.props
-
- return (
- h('.sender-to-recipient__container', [
- h('.sender-to-recipient__sender', [
- h('.sender-to-recipient__sender-icon', [
- h(Identicon, {
- address: senderAddress,
- diameter: 20,
- }),
- ]),
- h('.sender-to-recipient__name.sender-to-recipient__sender-name', senderName),
- ]),
- h('.sender-to-recipient__arrow-container', [
- h('.sender-to-recipient__arrow-circle', [
- h('img', {
- height: 15,
- width: 15,
- src: './images/arrow-right.svg',
- }),
- ]),
- ]),
- this.renderRecipient(),
- ])
- )
- }
-}
-
-SenderToRecipient.propTypes = {
- senderName: PropTypes.string,
- senderAddress: PropTypes.string,
- recipientName: PropTypes.string,
- recipientAddress: PropTypes.string,
- t: PropTypes.func,
-}
-
-SenderToRecipient.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(SenderToRecipient)
-
diff --git a/ui/app/components/sender-to-recipient/index.js b/ui/app/components/sender-to-recipient/index.js
new file mode 100644
index 000000000..f515c4ac4
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/index.js
@@ -0,0 +1 @@
+export { default } from './sender-to-recipient.component'
diff --git a/ui/app/css/itcss/components/sender-to-recipient.scss b/ui/app/components/sender-to-recipient/index.scss
index f16013cdf..a97393b8f 100644
--- a/ui/app/css/itcss/components/sender-to-recipient.scss
+++ b/ui/app/components/sender-to-recipient/index.scss
@@ -6,6 +6,16 @@
justify-content: center;
border-bottom: 1px solid $geyser;
position: relative;
+ flex: 0 0 auto;
+ height: 42px;
+ }
+
+ &__tooltip-wrapper {
+ min-width: 0;
+ }
+
+ &__tooltip-container {
+ max-width: 100%;
}
&__sender,
@@ -14,7 +24,7 @@
flex-direction: row;
align-items: center;
flex: 1;
- padding: 10px 20px;
+ padding: 0 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -22,11 +32,16 @@
&__sender {
padding-right: 30px;
+ cursor: pointer;
}
&__recipient {
- border-left: 1px solid $geyser;
padding-left: 30px;
+ border-left: 1px solid $geyser;
+
+ &--with-address {
+ cursor: pointer;
+ }
}
&__arrow-container {
@@ -42,17 +57,18 @@
padding: 5px;
border: 1px solid $geyser;
border-radius: 20px;
- height: 30px;
- width: 30px;
+ height: 32px;
+ width: 32px;
display: flex;
justify-content: center;
align-items: center;
}
&__name {
- padding-left: 5px;
+ padding-left: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ font-size: .875rem;
}
}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
new file mode 100644
index 000000000..cae173b56
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Identicon from '../identicon'
+import Tooltip from '../tooltip-v2'
+import copyToClipboard from 'copy-to-clipboard'
+
+export default class SenderToRecipient extends Component {
+ static propTypes = {
+ senderName: PropTypes.string,
+ senderAddress: PropTypes.string,
+ recipientName: PropTypes.string,
+ recipientAddress: PropTypes.string,
+ t: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ senderAddressCopied: false,
+ recipientAddressCopied: false,
+ }
+
+ renderRecipientWithAddress () {
+ const { t } = this.context
+ const { recipientName, recipientAddress } = this.props
+
+ return (
+ <div
+ className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
+ onClick={() => {
+ this.setState({ recipientAddressCopied: true })
+ copyToClipboard(recipientAddress)
+ }}
+ >
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={recipientAddress}
+ diameter={24}
+ />
+ </div>
+ <Tooltip
+ position="bottom"
+ title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ recipientAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ { recipientName || this.context.t('newContract') }
+ </div>
+ </Tooltip>
+ </div>
+ )
+ }
+
+ renderRecipientWithoutAddress () {
+ return (
+ <div className="sender-to-recipient__recipient">
+ <i className="fa fa-file-text-o" />
+ <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ { this.context.t('newContract') }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { senderName, senderAddress, recipientAddress } = this.props
+
+ return (
+ <div className="sender-to-recipient__container">
+ <div
+ className="sender-to-recipient__sender"
+ onClick={() => {
+ this.setState({ senderAddressCopied: true })
+ copyToClipboard(senderAddress)
+ }}
+ >
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={senderAddress}
+ diameter={24}
+ />
+ </div>
+ <Tooltip
+ position="bottom"
+ title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ senderAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name sender-to-recipient__sender-name">
+ { senderName }
+ </div>
+ </Tooltip>
+ </div>
+ <div className="sender-to-recipient__arrow-container">
+ <div className="sender-to-recipient__arrow-circle">
+ <img
+ height={15}
+ width={15}
+ src="./images/arrow-right.svg"
+ />
+ </div>
+ </div>
+ {
+ recipientAddress
+ ? this.renderRecipientWithAddress()
+ : this.renderRecipientWithoutAddress()
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
index 93d2023b5..2c4ba40bf 100644
--- a/ui/app/components/shapeshift-form.js
+++ b/ui/app/components/shapeshift-form.js
@@ -181,7 +181,7 @@ ShapeshiftForm.prototype.render = function () {
return h('div.shapeshift-form-wrapper', [
showQrCode
? this.renderQrCode()
- : h('div.shapeshift-form', [
+ : h('div.modal-shapeshift-form', [
h('div.shapeshift-form__selectors', [
h('div.shapeshift-form__selector', [
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index 2a3e929fe..2e0102d1a 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -22,6 +22,8 @@ const {
conversionRateSelector,
} = require('../selectors.js')
+import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
+
const { DEFAULT_ROUTE } = require('../routes')
function mapStateToProps (state) {
@@ -39,6 +41,7 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(actions.goHome()),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
}
}
@@ -204,6 +207,9 @@ SignatureRequest.prototype.renderBody = function () {
h('div.request-signature__rows', [
...rows.map(({ name, value }) => {
+ if (typeof value === 'boolean') {
+ value = value.toString()
+ }
return h('div.request-signature__row', [
h('div.request-signature__row-title', [`${name}:`]),
h('div.request-signature__row-value', value),
@@ -244,12 +250,18 @@ SignatureRequest.prototype.renderFooter = function () {
return h('div.request-signature__footer', [
h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
onClick: event => {
- cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE))
+ cancel(event).then(() => {
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
},
}, this.context.t('cancel')),
h('button.btn-primary.btn--large', {
onClick: event => {
- sign(event).then(() => this.props.history.push(DEFAULT_ROUTE))
+ sign(event).then(() => {
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
},
}, this.context.t('sign')),
])
diff --git a/ui/app/components/tabs/index.js b/ui/app/components/tabs/index.js
new file mode 100644
index 000000000..3a8d18248
--- /dev/null
+++ b/ui/app/components/tabs/index.js
@@ -0,0 +1,3 @@
+import Tabs from './tabs.component'
+import Tab from './tab'
+export { Tabs, Tab }
diff --git a/ui/app/components/tabs/index.scss b/ui/app/components/tabs/index.scss
new file mode 100644
index 000000000..a3b42f8e3
--- /dev/null
+++ b/ui/app/components/tabs/index.scss
@@ -0,0 +1,11 @@
+@import './tab/index';
+
+.tabs {
+ &__list {
+ display: flex;
+ justify-content: flex-start;
+ background-color: #f9fafa;
+ border-bottom: 1px solid $geyser;
+ padding: 0 16px;
+ }
+}
diff --git a/ui/app/components/tabs/tab/index.js b/ui/app/components/tabs/tab/index.js
new file mode 100644
index 000000000..fbc309e8e
--- /dev/null
+++ b/ui/app/components/tabs/tab/index.js
@@ -0,0 +1,2 @@
+import Tab from './tab.component'
+module.exports = Tab
diff --git a/ui/app/components/tabs/tab/index.scss b/ui/app/components/tabs/tab/index.scss
new file mode 100644
index 000000000..1de6ffa0e
--- /dev/null
+++ b/ui/app/components/tabs/tab/index.scss
@@ -0,0 +1,15 @@
+.tab {
+ color: #8C8E94;
+ font-size: .75rem;
+ text-transform: uppercase;
+ cursor: pointer;
+ padding: 8px 0;
+ margin: 0 8px;
+ min-width: 50px;
+ text-align: center;
+
+ &--active {
+ color: $black;
+ border-bottom: 2px solid $curious-blue;
+ }
+}
diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/tabs/tab/tab.component.js
new file mode 100644
index 000000000..a59da8904
--- /dev/null
+++ b/ui/app/components/tabs/tab/tab.component.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const Tab = props => {
+ const { name, onClick, isActive, tabIndex } = props
+
+ return (
+ <li
+ className={classnames(
+ 'tab',
+ isActive && 'tab--active',
+ )}
+ onClick={event => {
+ event.preventDefault()
+ onClick(tabIndex)
+ }}
+ >
+ { name }
+ </li>
+ )
+}
+
+Tab.propTypes = {
+ name: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ isActive: PropTypes.bool,
+ tabIndex: PropTypes.number,
+}
+
+export default Tab
diff --git a/ui/app/components/tabs/tabs.component.js b/ui/app/components/tabs/tabs.component.js
new file mode 100644
index 000000000..d26dcff2f
--- /dev/null
+++ b/ui/app/components/tabs/tabs.component.js
@@ -0,0 +1,62 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class Tabs extends Component {
+ static propTypes = {
+ defaultActiveTabIndex: PropTypes.number,
+ children: PropTypes.node,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ activeTabIndex: props.defaultActiveTabIndex || 0,
+ }
+ }
+
+ handleTabClick (tabIndex) {
+ const { activeTabIndex } = this.state
+
+ if (tabIndex !== activeTabIndex) {
+ this.setState({
+ activeTabIndex: tabIndex,
+ })
+ }
+ }
+
+ renderTabs () {
+ const numberOfTabs = React.Children.count(this.props.children)
+
+ return React.Children.map(this.props.children, (child, index) => {
+ return child && React.cloneElement(child, {
+ onClick: index => this.handleTabClick(index),
+ tabIndex: index,
+ isActive: numberOfTabs > 1 && index === this.state.activeTabIndex,
+ key: index,
+ })
+ })
+ }
+
+ renderActiveTabContent () {
+ const { children } = this.props
+ const { activeTabIndex } = this.state
+
+ return children[activeTabIndex]
+ ? children[activeTabIndex].props.children
+ : children.props.children
+ }
+
+ render () {
+ return (
+ <div className="tabs">
+ <ul className="tabs__list">
+ { this.renderTabs() }
+ </ul>
+ <div className="tabs__content">
+ { this.renderActiveTabContent() }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js
index 1900ccec7..99ca7335c 100644
--- a/ui/app/components/token-balance.js
+++ b/ui/app/components/token-balance.js
@@ -34,7 +34,7 @@ TokenBalance.prototype.render = function () {
return isLoading
? h('span', '')
: h('span.token-balance', [
- h('span.token-balance__amount', string),
+ h('span.hide-text-overflow.token-balance__amount', string),
!balanceOnly && h('span.token-balance__symbol', symbol),
])
}
@@ -98,6 +98,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) {
}
TokenBalance.prototype.updateBalance = function (tokens = []) {
+ if (!this.tracker.running) {
+ return
+ }
+
const [{ string, symbol }] = tokens
this.setState({
@@ -110,5 +114,7 @@ TokenBalance.prototype.updateBalance = function (tokens = []) {
TokenBalance.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
}
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 4189cf801..42351cf89 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -158,12 +158,17 @@ TokenList.prototype.componentDidUpdate = function (nextProps) {
}
TokenList.prototype.updateBalances = function (tokens) {
+ if (!this.tracker.running) {
+ return
+ }
this.setState({ tokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
}
// function uniqueMergeTokens (tokensA, tokensB = []) {
diff --git a/ui/app/components/tooltip-v2.js b/ui/app/components/tooltip-v2.js
index 133a0f16a..05a5efc80 100644
--- a/ui/app/components/tooltip-v2.js
+++ b/ui/app/components/tooltip-v2.js
@@ -12,7 +12,7 @@ function Tooltip () {
Tooltip.prototype.render = function () {
const props = this.props
- const { position, title, children, wrapperClassName } = props
+ const { position, title, children, wrapperClassName, containerClassName, onHidden } = props
return h('div', {
className: wrapperClassName,
@@ -25,6 +25,8 @@ Tooltip.prototype.render = function () {
hideOnClick: false,
size: 'small',
arrow: true,
+ className: containerClassName,
+ onHidden,
}, children),
])
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
index 9a2fb5311..7513ba267 100644
--- a/ui/app/components/tx-list-item.js
+++ b/ui/app/components/tx-list-item.js
@@ -54,6 +54,8 @@ function TxListItem () {
fiatTotal: null,
isTokenTx: null,
}
+
+ this.unmounted = false
}
TxListItem.prototype.componentDidMount = async function () {
@@ -67,9 +69,16 @@ TxListItem.prototype.componentDidMount = async function () {
? await this.getSendTokenTotal()
: this.getSendEtherTotal()
+ if (this.unmounted) {
+ return
+ }
this.setState({ total, fiatTotal, isTokenTx })
}
+TxListItem.prototype.componentWillUnmount = function () {
+ this.unmounted = true
+}
+
TxListItem.prototype.getAddressText = function () {
const {
address,
@@ -204,14 +213,23 @@ TxListItem.prototype.showRetryButton = function () {
if (!txParams) {
return false
}
+ let currentTxSharesEarliestNonce = false
const currentNonce = txParams.nonce
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
+ const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId
+ if (currentSubmittedTxs.length > 0) {
+ const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
+ if (tx1.submittedTime < tx2.submittedTime) return tx1
+ return tx2
+ })
+ currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
+ }
- return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
+ return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
}
TxListItem.prototype.setSelectedToken = function (tokenAddress) {
@@ -298,20 +316,16 @@ TxListItem.prototype.render = function () {
]),
]),
- this.showRetryButton() && h('div.tx-list-item-retry-container', [
-
- h('span.tx-list-item-retry-copy', 'Taking too long?'),
-
- h('span.tx-list-item-retry-link', {
- onClick: (event) => {
- event.stopPropagation()
- if (isTokenTx) {
- this.setSelectedToken(txParams.to)
- }
- this.resubmit()
- },
- }, 'Increase the gas price on your transaction'),
-
+ this.showRetryButton() && h('.tx-list-item-retry-container', {
+ onClick: (event) => {
+ event.stopPropagation()
+ if (isTokenTx) {
+ this.setSelectedToken(txParams.to)
+ }
+ this.resubmit()
+ },
+ }, [
+ h('span', 'Taking too long? Increase the gas price on your transaction'),
]),
]), // holding on icon from design
diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js
index 014497fcd..654090da6 100644
--- a/ui/app/components/tx-view.js
+++ b/ui/app/components/tx-view.js
@@ -11,6 +11,7 @@ const { SEND_ROUTE } = require('../routes')
const { checksumAddress: toChecksumAddress } = require('../util')
const BalanceComponent = require('./balance-component')
+const Tooltip = require('./tooltip')
const TxList = require('./tx-list')
const SelectedAccount = require('./selected-account')
@@ -103,7 +104,8 @@ TxView.prototype.renderButtons = function () {
}
TxView.prototype.render = function () {
- const { isMascara } = this.props
+ const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
+ const { t } = this.context
return h('div.tx-view.flex-column', {
style: {},
@@ -120,21 +122,30 @@ TxView.prototype.render = function () {
},
}, [
- h('div.fa.fa-bars', {
- style: {
- fontSize: '1.3em',
- cursor: 'pointer',
- padding: '10px',
- },
- onClick: () => this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar(),
- }),
+ h(Tooltip, {
+ title: t('menu'),
+ position: 'bottom',
+ }, [
+ h('div.fa.fa-bars', {
+ style: {
+ fontSize: '1.3em',
+ cursor: 'pointer',
+ padding: '10px',
+ },
+ onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
+ }),
+ ]),
h(SelectedAccount),
- !isMascara && h('div.open-in-browser', {
- onClick: () => global.platform.openExtensionInBrowser(),
- }, [h('img', { src: 'images/popout.svg' })]),
-
+ !isMascara && h(Tooltip, {
+ title: t('openInTab'),
+ position: 'bottom',
+ }, [
+ h('div.open-in-browser', {
+ onClick: () => global.platform.openExtensionInBrowser(),
+ }, [h('img', { src: 'images/popout.svg' })]),
+ ]),
]),
this.renderHeroBalance(),
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index da142fad8..20c2be0f1 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -175,7 +175,7 @@ WalletView.prototype.render = function () {
this.setState({ copyToClipboardPressed: false })
},
}, [
- `${checksummedAddress.slice(0, 4)}...${checksummedAddress.slice(-4)}`,
+ `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`,
h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }),
]),
]),
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 461587cb1..112ea6bca 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -9,11 +9,7 @@ const txHelper = require('../lib/tx-helper')
const log = require('loglevel')
const R = require('ramda')
-const PendingTx = require('./components/pending-tx')
const SignatureRequest = require('./components/signature-request')
-// const PendingMsg = require('./components/pending-msg')
-// const PendingPersonalMsg = require('./components/pending-personal-msg')
-// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes')
@@ -105,7 +101,7 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
- if (prevTx.status === 'dropped') {
+ if (prevTx && prevTx.status === 'dropped') {
this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
onHide: () => history.push(DEFAULT_ROUTE),
@@ -151,102 +147,32 @@ ConfirmTxScreen.prototype.render = function () {
currentCurrency,
conversionRate,
blockGasLimit,
- // provider,
- // computedBalances,
} = props
var txData = this.getTxData() || {}
- var txParams = txData.params || {}
-
- // var isNotification = isPopupOrNotification() === 'notification'
- /*
- Client is using the flag above to render the following in conf screen
- // subtitle and nav
- h('.section-title.flex-row.flex-center', [
- !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.goHome.bind(this),
- }) : null,
- h('h2.page-subtitle', 'Confirm Transaction'),
- isNotification ? h(NetworkIndicator, {
- network: network,
- provider: provider,
- }) : null,
- ]),
- */
-
-
- return currentTxView({
- // Properties
- txData: txData,
- key: txData.id,
- selectedAddress: props.selectedAddress,
- accounts: props.accounts,
- identities: props.identities,
- conversionRate,
- currentCurrency,
- blockGasLimit,
- // Actions
- buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
- sendTransaction: this.sendTransaction.bind(this),
- cancelTransaction: this.cancelTransaction.bind(this, txData),
- signMessage: this.signMessage.bind(this, txData),
- signPersonalMessage: this.signPersonalMessage.bind(this, txData),
- signTypedMessage: this.signTypedMessage.bind(this, txData),
- cancelMessage: this.cancelMessage.bind(this, txData),
- cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
- cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
- })
-}
-
-function currentTxView (opts) {
- log.info('rendering current tx view')
- const { txData } = opts
- const { txParams, msgParams } = txData
-
- if (txParams) {
- log.debug('txParams detected, rendering pending tx')
- return h(PendingTx, opts)
- } else if (msgParams) {
- log.debug('msgParams detected, rendering pending msg')
-
- return h(SignatureRequest, opts)
-
- // if (type === 'eth_sign') {
- // log.debug('rendering eth_sign message')
- // return h(PendingMsg, opts)
- // } else if (type === 'personal_sign') {
- // log.debug('rendering personal_sign message')
- // return h(PendingPersonalMsg, opts)
- // } else if (type === 'eth_signTypedData') {
- // log.debug('rendering eth_signTypedData message')
- // return h(PendingTypedMsg, opts)
- // }
- }
-
- return h(Loading)
-}
-
-ConfirmTxScreen.prototype.buyEth = function (address, event) {
- event.preventDefault()
- this.props.dispatch(actions.buyEthView(address))
-}
-
-ConfirmTxScreen.prototype.sendTransaction = function (txData, event) {
- this.stopPropagation(event)
- this.props.dispatch(actions.updateAndApproveTx(txData))
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
- this.stopPropagation(event)
- event.preventDefault()
- this.props.dispatch(actions.cancelTx(txData))
-}
-
-ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
- this.stopPropagation(event)
- event.preventDefault()
- this.props.dispatch(actions.cancelAllTx(unconfTxList))
+ const { msgParams } = txData
+ log.debug('msgParams detected, rendering pending msg')
+
+ return msgParams
+ ? h(SignatureRequest, {
+ // Properties
+ txData: txData,
+ key: txData.id,
+ selectedAddress: props.selectedAddress,
+ accounts: props.accounts,
+ identities: props.identities,
+ conversionRate,
+ currentCurrency,
+ blockGasLimit,
+ // Actions
+ signMessage: this.signMessage.bind(this, txData),
+ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
+ cancelMessage: this.cancelMessage.bind(this, txData),
+ cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
+ })
+ : h(Loading)
}
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
@@ -296,20 +222,3 @@ ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
this.stopPropagation(event)
return this.props.dispatch(actions.cancelTypedMsg(msgData))
}
-
-ConfirmTxScreen.prototype.goHome = function (event) {
- this.stopPropagation(event)
- this.props.dispatch(actions.goHome())
-}
-
-// function warningIfExists (warning) {
-// if (warning &&
-// // Do not display user rejections on this screen:
-// warning.indexOf('User denied transaction signature') === -1) {
-// return h('.error', {
-// style: {
-// margin: 'auto',
-// },
-// }, warning)
-// }
-// }
diff --git a/ui/app/constants/error-keys.js b/ui/app/constants/error-keys.js
new file mode 100644
index 000000000..1b89be62e
--- /dev/null
+++ b/ui/app/constants/error-keys.js
@@ -0,0 +1,3 @@
+export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
+export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
+export const TRANSACTION_ERROR = 'transactionError'
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
index 337763067..a7a226cc5 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/conversion-util.js
@@ -140,7 +140,7 @@ const addCurrencies = (a, b, options = {}) => {
bBase,
...conversionOptions
} = options
- const value = (new BigNumber(a, aBase)).add(b, bBase)
+ const value = (new BigNumber(a.toString(), aBase)).add(b.toString(), bBase)
return converter({
value,
diff --git a/ui/app/conversion-util.test.js b/ui/app/conversion-util.test.js
new file mode 100644
index 000000000..368ce3bba
--- /dev/null
+++ b/ui/app/conversion-util.test.js
@@ -0,0 +1,22 @@
+import assert from 'assert'
+import {addCurrencies} from './conversion-util'
+
+
+describe('conversion utils', () => {
+ describe('addCurrencies()', () => {
+ it('add whole numbers', () => {
+ const result = addCurrencies(3, 9)
+ assert.equal(result.toNumber(), 12)
+ })
+
+ it('add decimals', () => {
+ const result = addCurrencies(1.3, 1.9)
+ assert.equal(result.toNumber(), 3.2)
+ })
+
+ it('add repeating decimals', () => {
+ const result = addCurrencies(1 / 3, 1 / 9)
+ assert.equal(result.toNumber(), 0.4444444444444444)
+ })
+ })
+})
diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss
index 96fba890c..b14753e23 100644
--- a/ui/app/css/itcss/components/account-menu.scss
+++ b/ui/app/css/itcss/components/account-menu.scss
@@ -72,6 +72,7 @@
background-color: $dusty-gray;
color: $black;
font-weight: normal;
+ letter-spacing: .5px;
}
}
@@ -84,6 +85,23 @@
@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 {
diff --git a/ui/app/css/itcss/components/alert.scss b/ui/app/css/itcss/components/alert.scss
new file mode 100644
index 000000000..930fc3f54
--- /dev/null
+++ b/ui/app/css/itcss/components/alert.scss
@@ -0,0 +1,57 @@
+.global-alert {
+ position: relative;
+ width: 100%;
+ background-color: #33A4E7;
+
+ .msg {
+ width: 100%;
+ display: block;
+ color: white;
+ font-size: 12px;
+ text-align: center;
+ }
+}
+
+.global-alert.hidden {
+ animation: alertHidden .5s ease forwards;
+}
+
+.global-alert.visible {
+ animation: alert .5s ease forwards;
+}
+
+/* Animation */
+@keyframes alert {
+ 0% {
+ opacity: 0;
+ top: -50px;
+ padding: 0px;
+ line-height: 12px;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ top: 0;
+ padding: 8px;
+ line-height: 12px;
+ }
+}
+
+@keyframes alertHidden {
+ 0% {
+ top: 0;
+ opacity: 1;
+ padding: 8px;
+ line-height: 12px;
+ }
+
+ 100% {
+ opacity: 0;
+ top: -50px;
+ padding: 0px;
+ line-height: 0px;
+ }
+}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index f93daec04..34565767f 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -4,7 +4,8 @@
.btn-default,
.btn-primary,
-.btn-secondary {
+.btn-secondary,
+.btn-confirm {
height: 44px;
background: $white;
display: flex;
@@ -13,13 +14,14 @@
box-sizing: border-box;
border-radius: 4px;
font-size: 14px;
- font-weight: 500;
+ font-weight: 400;
transition: border-color .3s ease;
padding: 0 16px;
min-width: 140px;
width: 100%;
text-transform: uppercase;
outline: none;
+ font-family: Roboto;
&--disabled,
&[disabled] {
@@ -71,6 +73,12 @@
}
}
+.btn-confirm {
+ color: $white;
+ border: 2px solid $curious-blue;
+ background-color: $curious-blue;
+}
+
.btn--large {
height: 54px;
}
@@ -119,19 +127,6 @@
}
}
-.btn-confirm {
- background-color: $caribbean-green; // TODO: reusable color in colors.css
- text-align: center;
- padding: .8rem 1rem;
- color: $white;
- border: 2px solid $caribbean-green;
- border-radius: 4px;
- font-size: .85rem;
- font-weight: 400;
- transition: border-color .3s ease;
- width: 100%;
-}
-
// No longer used in flat design, remove when modal buttons done
// div.wallet-btn {
// border: 1px solid rgb(91, 93, 103);
diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss
index 3560b0b0c..b1a74dce2 100644
--- a/ui/app/css/itcss/components/currency-display.scss
+++ b/ui/app/css/itcss/components/currency-display.scss
@@ -1,6 +1,5 @@
.currency-display {
height: 54px;
- width: 100%ß;
border: 1px solid $alto;
border-radius: 4px;
background-color: $white;
@@ -21,7 +20,7 @@
line-height: 22px;
border: none;
outline: 0 !important;
- max-width: 100%;
+ max-width: 22ch;
}
&__primary-currency {
@@ -47,14 +46,22 @@
&__input-wrapper {
position: relative;
display: flex;
+ flex: 1;
+ max-width: 100%;
+
+ input[type="number"] {
+ -moz-appearance: textfield;
+ }
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
+ -moz-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
+ -moz-appearance: none;
display: none;
}
}
@@ -67,12 +74,14 @@
.react-numeric-input {
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;
}
}
-} \ No newline at end of file
+}
diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss
index 09d66aedd..eba93ecb4 100644
--- a/ui/app/css/itcss/components/hero-balance.scss
+++ b/ui/app/css/itcss/components/hero-balance.scss
@@ -27,25 +27,37 @@
@media screen and (max-width: $break-small) {
flex-direction: column;
flex: 0 0 auto;
+ max-width: 100%;
}
@media screen and (min-width: $break-large) {
flex-direction: row;
flex-grow: 3;
+ min-width: 0;
}
}
.balance-display {
.token-amount {
color: $black;
+ max-width: 100%;
+
+ .token-balance {
+ display: flex;
+ }
}
@media screen and (max-width: $break-small) {
+ max-width: 100%;
text-align: center;
.token-amount {
font-size: 1.75rem;
margin-top: 1rem;
+
+ .token-balance {
+ flex-direction: column;
+ }
}
.fiat-amount {
@@ -56,9 +68,10 @@
}
@media screen and (min-width: $break-large) {
- margin-left: .8em;
+ margin: 0 .8em;
justify-content: flex-start;
align-items: flex-start;
+ min-width: 0;
.token-amount {
font-size: 1.5rem;
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 1d87b8004..96ad5fe64 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -8,6 +8,8 @@
@import './modal.scss';
+@import './alert.scss';
+
@import './newui-sections.scss';
@import './account-dropdown.scss';
@@ -58,6 +60,4 @@
@import './welcome-screen.scss';
-@import './sender-to-recipient.scss';
-
@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss
index 74658f656..42ef7ae0a 100644
--- a/ui/app/css/itcss/components/modal.scss
+++ b/ui/app/css/itcss/components/modal.scss
@@ -642,10 +642,31 @@
display: flex;
flex-flow: column nowrap;
flex: 1;
+ align-items: center;
@media screen and (max-width: 575px) {
height: 0;
}
+
+ .shapeshift-form-wrapper {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ align-items: center;
+ flex: 1 0 auto;
+
+ .shapeshift-form, .modal-shapeshift-form {
+ border-radius: 8px;
+ background-color: rgba(0, 0, 0, .05);
+ padding: 17px 15px;
+ margin-bottom: 10px;
+
+ &__caret {
+ width: auto;
+ flex: 1;
+ }
+ }
+ }
}
&__logo {
@@ -773,17 +794,15 @@
margin-top: 28px;
flex: 1 0 auto;
- .shapeshift-form {
- width: auto;
+ .shapeshift-form, .modal-shapeshift-form {
+ border-radius: 8px;
+ background-color: rgba(0, 0, 0, .05);
+ padding: 17px 15px;
&__caret {
width: auto;
flex: 1;
}
-
- @media screen and (max-width: 575px) {
- width: auto;
- }
}
}
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
index 6c8be0b6d..b23876d01 100644
--- a/ui/app/css/itcss/components/network.scss
+++ b/ui/app/css/itcss/components/network.scss
@@ -76,7 +76,6 @@
}
.network-name-item {
- font-weight: 100;
flex: 1;
color: $dusty-gray;
text-overflow: ellipsis;
@@ -159,15 +158,3 @@
.network-caret {
margin: 0 8px 2px;
}
-
-.network-display {
- &__container {
- display: flex;
- align-items: center;
- justify-content: flex-start;
-
- @media screen and (min-width: 576px) {
- display: none;
- }
- }
-}
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index 293579058..b12afb124 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -1,9 +1,8 @@
.new-account {
- width: 376px;
+ width: 375px;
background-color: #FFFFFF;
box-shadow: 0 0 7px 0 rgba(0,0,0,0.08);
z-index: 25;
- padding-bottom: 31px;
&__header {
display: flex;
@@ -28,7 +27,6 @@
&__tab {
height: 54px;
- width: 75px;
padding: 15px 10px;
color: $dusty-gray;
font-family: Roboto;
@@ -38,10 +36,6 @@
cursor: pointer;
}
- &__tab:first-of-type {
- margin-right: 20px;
- }
-
&__tab:hover {
color: $black;
border-bottom: none;
@@ -69,7 +63,7 @@
display: flex;
flex-flow: column;
align-items: center;
- padding: 0 30px;
+ padding: 0 30px 30px;
&__select-section {
display: flex;
@@ -158,11 +152,296 @@
}
}
+.hw-tutorial {
+ width: 375px;
+ border-top: 1px solid #D2D8DD;
+ border-bottom: 1px solid #D2D8DD;
+ overflow: visible;
+ display: block;
+ padding: 15px 30px;
+}
+
+.hw-connect {
+ &__header {
+ &__title {
+ margin-top: 5px;
+ margin-bottom: 15px;
+ font-size: 22px;
+ text-align: center;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 10px;
+ margin-bottom: 0px;
+ }
+ }
+
+ &__learn-more {
+ margin-top: 15px;
+ font-size: 14px;
+ color: #5B5D67;
+ line-height: 19px;
+ text-align: center;
+ cursor: pointer;
+
+ &__arrow {
+ transform: rotate(90deg);
+ display: block;
+ text-align: center;
+ height: 30px;
+ margin: 0px auto 10px;
+ }
+ }
+
+ &__title {
+ padding-top: 10px;
+ font-weight: 400;
+ font-size: 18px;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 10px;
+ margin-bottom: 15px;
+ }
+
+ &__link {
+ color: #2f9ae0;
+ }
+
+ &__footer {
+ width: 100%;
+
+ &__title {
+ padding-top: 15px;
+ padding-bottom: 12px;
+ font-weight: 400;
+ font-size: 18px;
+ text-align: center;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 12px;
+ margin-bottom: 27px;
+ }
+
+ &__link {
+ color: #2f9ae0;
+ margin-left: 5px;
+ }
+ }
+
+ &__get-trezor {
+ width: 100%;
+ padding-bottom: 20px;
+ padding-top: 20px;
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ }
+
+ &__link {
+ font-size: 14px;
+ text-align: center;
+ color: #2f9ae0;
+ cursor: pointer;
+ }
+ }
+
+ &__step-asset {
+ margin: 0px auto 20px;
+ display: flex;
+ }
+}
+
+.hw-account-list {
+ display: flex;
+ flex: 1;
+ flex-flow: column;
+ width: 100%;
+
+ &__title_wrapper {
+ display: flex;
+ flex-direction: row;
+ flex: 1;
+ }
+
+ &__title {
+ margin-bottom: 23px;
+ align-self: flex-start;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: bold;
+ display: flex;
+ flex: 1;
+ }
+
+ &__device {
+ margin-bottom: 23px;
+ align-self: flex-end;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: normal;
+ display: flex;
+ }
+
+ &__item {
+ font-size: 15px;
+ flex-direction: row;
+ display: flex;
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+
+ &__item:nth-of-type(even) {
+ background-color: #fbfbfb;
+ }
+
+ &__item:nth-of-type(odd) {
+ background: rgba(0, 0, 0, 0.03);
+ }
+
+ &__item:hover {
+ background-color: rgba(0, 0, 0, 0.06);
+ }
+
+ &__item__index {
+ display: flex;
+ width: 24px;
+ }
+
+ &__item__radio {
+ display: flex;
+ flex: 1;
+
+ input {
+ padding: 10px;
+ margin-top: 13px;
+ }
+ }
+
+ &__item__label {
+ display: flex;
+ flex: 1;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+
+ &__item__balance {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ }
+
+ &__item__link {
+ display: flex;
+ margin-top: 13px;
+ }
+
+ &__item__link img {
+ width: 15px;
+ height: 15px;
+ }
+}
+
+.hw-list-pagination {
+ display: flex;
+ align-self: flex-end;
+ margin-top: 10px;
+
+ &__button {
+ height: 19px;
+ display: flex;
+ color: #33a4e7;
+ font-size: 14px;
+ line-height: 19px;
+ border: none;
+ min-width: 46px;
+ margin-right: 0px;
+ margin-left: 16px;
+ padding: 0px;
+ text-transform: uppercase;
+ font-family: Roboto;
+ }
+}
+
+.new-account-connect-form {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ padding: 15px 30px 0;
+ height: 710px;
+ overflow: auto;
+
+ &.unsupported-browser {
+ height: 210px;
+ }
+
+ &.account-list {
+ height: auto;
+ }
+
+ &__buttons {
+ margin-top: 39px;
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ &__button {
+ width: 150px;
+ min-width: initial;
+ }
+
+ .btn-primary {
+ background-color: #259DE5;
+ color: #FFFFFF;
+ border: none;
+ width: 100%;
+ min-height: 54px;
+ font-weight: 300;
+ font-size: 14px;
+ }
+
+ &__button.unlock {
+ width: 50%;
+ }
+
+ &__button.btn-primary--disabled {
+ cursor: not-allowed;
+ opacity: .5;
+ }
+}
+
+.hw-forget-device-container {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ padding: 22px;
+
+ a {
+ color: #2f9ae0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+}
+
.new-account-create-form {
display: flex;
flex-flow: column;
align-items: center;
- padding: 30px 30px 0;
+ padding: 30px;
&__input-label {
color: $scorpion;
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index bbe0ee661..bbfd85c90 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -26,14 +26,16 @@ $wallet-view-bg: $alabaster;
//Account and transaction details
.account-and-transaction-details {
display: flex;
- flex: 1 0 auto;
+ flex: 1 1 auto;
+ min-width: 0;
}
// tx view
.tx-view {
- flex: 63.5 0 66.5%;
+ flex: 1 1 66.5%;
background: $tx-view-bg;
+ min-width: 0;
// No title on mobile
@media screen and (max-width: 575px) {
@@ -286,7 +288,7 @@ $wallet-view-bg: $alabaster;
}
.token-balance__amount {
- padding-right: 6px;
+ padding: 0 6px;
}
// first time
@@ -330,3 +332,12 @@ $wallet-view-bg: $alabaster;
align-items: center;
flex: 1 0 auto;
}
+
+.first-view-main-wrapper {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ padding: 0 10px;
+ background: white;
+}
diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss
index 4707ff60e..b607aded3 100644
--- a/ui/app/css/itcss/components/request-signature.scss
+++ b/ui/app/css/itcss/components/request-signature.scss
@@ -181,6 +181,7 @@
overflow-wrap: break-word;
border-bottom: 1px solid #d2d8dd;
padding: 6px 18px 15px;
+ white-space: pre-line;
}
&__help-link {
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index c168242cf..e9c872ea7 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -628,7 +628,7 @@
}
}
- &__to-autocomplete, &__memo-text-area {
+ &__to-autocomplete, &__memo-text-area, &__hex-data {
&__input {
height: 54px;
width: 100%;
@@ -899,4 +899,4 @@
.sliders-icon {
color: $curious-blue;
-} \ No newline at end of file
+}
diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss
index 4b706abce..49d0c290e 100644
--- a/ui/app/css/itcss/components/token-list.scss
+++ b/ui/app/css/itcss/components/token-list.scss
@@ -34,6 +34,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
&__fiat-amount {
margin-top: .25%;
font-size: 105%;
+ width: 100%;
text-transform: uppercase;
@media #{$wallet-balance-breakpoint-range} {
diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss
index d03faf486..1d45ff13b 100644
--- a/ui/app/css/itcss/components/transaction-list.scss
+++ b/ui/app/css/itcss/components/transaction-list.scss
@@ -129,12 +129,14 @@
.tx-list-item-retry-container {
background: #d1edff;
width: 100%;
- border-radius: 4px;
- font-size: 0.8em;
+ border-radius: 12px;
+ font-size: .75rem;
display: flex;
justify-content: center;
margin-left: 44px;
width: calc(100% - 44px);
+ padding: 4px;
+ cursor: pointer;
@media screen and (min-width: 576px) and (max-width: 679px) {
flex-flow: column;
@@ -151,10 +153,6 @@
}
}
-.tx-list-item-retry-copy {
- font-family: Roboto;
-}
-
.tx-list-item-retry-link {
text-decoration: underline;
margin-left: 6px;
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index 3525d2003..d1c65afed 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -73,195 +73,6 @@ input.large-input {
text-transform: uppercase;
}
-.page-container {
- width: 408px;
- background-color: $white;
- box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
- z-index: 25;
- display: flex;
- flex-flow: column;
- border-radius: 8px;
-
- &__header {
- display: flex;
- flex-flow: column;
- border-bottom: 1px solid $geyser;
- padding: 16px;
- flex: 0 0 auto;
- position: relative;
-
- &--no-padding-bottom {
- padding-bottom: 0;
- }
- }
-
- &__header-close {
- color: $tundora;
- position: absolute;
- top: 16px;
- right: 16px;
- cursor: pointer;
- overflow: hidden;
-
- &::after {
- content: '\00D7';
- font-size: 40px;
- line-height: 20px;
- }
- }
-
- &__header-row {
- padding-bottom: 10px;
- display: flex;
- justify-content: space-between;
- }
-
- &__footer {
- display: flex;
- flex-flow: row;
- justify-content: center;
- border-top: 1px solid $geyser;
- padding: 16px;
- flex: 0 0 auto;
-
- .btn-clear,
- .btn-cancel,
- .btn-confirm {
- font-size: 1rem;
- }
- }
-
- &__footer-button {
- height: 55px;
- font-size: 1rem;
- text-transform: uppercase;
- margin-right: 16px;
- border-radius: 2px;
-
- &:last-of-type {
- margin-right: 0;
- }
- }
-
- &__back-button {
- color: #2f9ae0;
- font-size: 1rem;
- cursor: pointer;
- font-weight: 400;
- }
-
- &__title {
- color: $black;
- font-size: 2rem;
- font-weight: 500;
- line-height: 2rem;
- }
-
- &__subtitle {
- padding-top: .5rem;
- line-height: initial;
- font-size: .9rem;
- color: $gray;
- }
-
- &__tabs {
- display: flex;
- margin-top: 16px;
- }
-
- &__tab {
- min-width: 5rem;
- padding: 8px;
- color: $dusty-gray;
- font-family: Roboto;
- font-size: 1rem;
- text-align: center;
- cursor: pointer;
- border-bottom: none;
- margin-right: 16px;
-
- &:last-of-type {
- margin-right: 0;
- }
-
- &--selected {
- color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
- }
- }
-
- &--full-width {
- width: 100% !important;
- }
-
- &--full-height {
- height: 100% !important;
- max-height: initial !important;
- min-height: initial !important;
- }
-
- &__content {
- overflow-y: auto;
- flex: 1;
- }
-
- &__warning-container {
- background: $linen;
- padding: 20px;
- display: flex;
- align-items: start;
- }
-
- &__warning-message {
- padding-left: 15px;
- }
-
- &__warning-title {
- font-weight: 500;
- }
-
- &__warning-icon {
- padding-top: 5px;
- }
-}
-
-@media screen and (max-width: 250px) {
- .page-container {
- &__footer {
- flex-flow: column-reverse;
- }
-
- &__footer-button {
- width: 100%;
- margin-bottom: 1rem;
- margin-right: 0;
-
- &:first-of-type {
- margin-bottom: 0;
- }
- }
- }
-}
-
-@media screen and (max-width: 575px) {
- .page-container {
- height: 100%;
- width: 100%;
- overflow-y: auto;
- background-color: $white;
- border-radius: 0;
- flex: 1;
- }
-}
-
-@media screen and (min-width: 576px) {
- .page-container {
- max-height: 82vh;
- min-height: 570px;
- flex: 0 0 auto;
- }
-}
-
.input-label {
padding-bottom: 10px;
font-weight: 400;
diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss
index 814d7a382..f90c8edc3 100644
--- a/ui/app/css/itcss/settings/variables.scss
+++ b/ui/app/css/itcss/settings/variables.scss
@@ -55,6 +55,7 @@ $dodger-blue: #3099f2;
$zumthor: #edf7ff;
$ecstasy: #f7861c;
$linen: #fdf4f4;
+$oslo-gray: #8C8E94;
/*
Z-Indicies
diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss
index ee867640d..209614c6b 100644
--- a/ui/app/css/itcss/tools/utilities.scss
+++ b/ui/app/css/itcss/tools/utilities.scss
@@ -165,7 +165,7 @@
}
.bold {
- font-weight: 700;
+ font-weight: 500;
}
.text-transform-uppercase {
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
new file mode 100644
index 000000000..1885e12d1
--- /dev/null
+++ b/ui/app/ducks/confirm-transaction.duck.js
@@ -0,0 +1,386 @@
+import {
+ conversionRateSelector,
+ currentCurrencySelector,
+ unconfirmedTransactionsHashSelector,
+} from '../selectors/confirm-transaction'
+
+import {
+ getTokenData,
+ getMethodData,
+ getTransactionAmount,
+ getTransactionFee,
+ getHexGasTotal,
+ addFiat,
+ addEth,
+ increaseLastGasPrice,
+ hexGreaterThan,
+} from '../helpers/confirm-transaction/util'
+
+import { getSymbolAndDecimals } from '../token-util'
+import { conversionUtil } from '../conversion-util'
+
+// Actions
+const createActionType = action => `metamask/confirm-transaction/${action}`
+
+const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA')
+const CLEAR_TX_DATA = createActionType('CLEAR_TX_DATA')
+const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA')
+const CLEAR_TOKEN_DATA = createActionType('CLEAR_TOKEN_DATA')
+const UPDATE_METHOD_DATA = createActionType('UPDATE_METHOD_DATA')
+const CLEAR_METHOD_DATA = createActionType('CLEAR_METHOD_DATA')
+const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
+const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
+const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
+const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
+const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
+const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
+const UPDATE_NONCE = createActionType('UPDATE_NONCE')
+const FETCH_METHOD_DATA_START = createActionType('FETCH_METHOD_DATA_START')
+const FETCH_METHOD_DATA_END = createActionType('FETCH_METHOD_DATA_END')
+
+// Initial state
+const initState = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexGasTotal: '',
+ nonce: '',
+ fetchingMethodData: false,
+}
+
+// Reducer
+export default function reducer ({ confirmTransaction: confirmState = initState }, action = {}) {
+ switch (action.type) {
+ case UPDATE_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {},
+ }
+ case UPDATE_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {},
+ }
+ case UPDATE_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {},
+ }
+ case UPDATE_TRANSACTION_AMOUNTS:
+ const { fiatTransactionAmount, ethTransactionAmount } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
+ ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
+ }
+ case UPDATE_TRANSACTION_FEES:
+ const { fiatTransactionFee, ethTransactionFee } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
+ ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
+ }
+ case UPDATE_TRANSACTION_TOTALS:
+ const { fiatTransactionTotal, ethTransactionTotal } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
+ ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
+ }
+ case UPDATE_HEX_GAS_TOTAL:
+ return {
+ ...confirmState,
+ hexGasTotal: action.payload,
+ }
+ case UPDATE_TOKEN_PROPS:
+ const { tokenSymbol = '', tokenDecimals = '' } = action.payload
+ return {
+ ...confirmState,
+ tokenProps: {
+ ...confirmState.tokenProps,
+ tokenSymbol,
+ tokenDecimals,
+ },
+ }
+ case UPDATE_NONCE:
+ return {
+ ...confirmState,
+ nonce: action.payload,
+ }
+ case FETCH_METHOD_DATA_START:
+ return {
+ ...confirmState,
+ fetchingMethodData: true,
+ }
+ case FETCH_METHOD_DATA_END:
+ return {
+ ...confirmState,
+ fetchingMethodData: false,
+ }
+ case CLEAR_CONFIRM_TRANSACTION:
+ return initState
+ default:
+ return confirmState
+ }
+}
+
+// Action Creators
+export function updateTxData (txData) {
+ return {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+}
+
+export function clearTxData () {
+ return {
+ type: CLEAR_TX_DATA,
+ }
+}
+
+export function updateTokenData (tokenData) {
+ return {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+}
+
+export function clearTokenData () {
+ return {
+ type: CLEAR_TOKEN_DATA,
+ }
+}
+
+export function updateMethodData (methodData) {
+ return {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+}
+
+export function clearMethodData () {
+ return {
+ type: CLEAR_METHOD_DATA,
+ }
+}
+
+export function updateTransactionAmounts (amounts) {
+ return {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: amounts,
+ }
+}
+
+export function updateTransactionFees (fees) {
+ return {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: fees,
+ }
+}
+
+export function updateTransactionTotals (totals) {
+ return {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: totals,
+ }
+}
+
+export function updateHexGasTotal (hexGasTotal) {
+ return {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: hexGasTotal,
+ }
+}
+
+export function updateTokenProps (tokenProps) {
+ return {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+}
+
+export function updateNonce (nonce) {
+ return {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+}
+
+export function setFetchingMethodData (isFetching) {
+ return {
+ type: isFetching ? FETCH_METHOD_DATA_START : FETCH_METHOD_DATA_END,
+ }
+}
+
+export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
+ return (dispatch, getState) => {
+ const { confirmTransaction: { txData } } = getState()
+ const newTxData = {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gas: gasLimit,
+ gasPrice,
+ },
+ }
+
+ dispatch(updateTxDataAndCalculate(newTxData))
+ }
+}
+
+function increaseFromLastGasPrice (txData) {
+ const { lastGasPrice, txParams: { gasPrice: previousGasPrice } = {} } = txData
+
+ // Set the minimum to a 10% increase from the lastGasPrice.
+ const minimumGasPrice = increaseLastGasPrice(lastGasPrice)
+ const gasPriceBelowMinimum = hexGreaterThan(minimumGasPrice, previousGasPrice)
+ const gasPrice = (!previousGasPrice || gasPriceBelowMinimum) ? minimumGasPrice : previousGasPrice
+
+ return {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gasPrice,
+ },
+ }
+}
+
+export function updateTxDataAndCalculate (txData) {
+ return (dispatch, getState) => {
+ const state = getState()
+ const currentCurrency = currentCurrencySelector(state)
+ const conversionRate = conversionRateSelector(state)
+
+ dispatch(updateTxData(txData))
+
+ const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
+
+ const fiatTransactionAmount = getTransactionAmount({
+ value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
+ })
+ const ethTransactionAmount = getTransactionAmount({
+ value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
+ })
+
+ dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount }))
+
+ const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
+
+ dispatch(updateHexGasTotal(hexGasTotal))
+
+ const fiatTransactionFee = getTransactionFee({
+ value: hexGasTotal,
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ethTransactionFee = getTransactionFee({
+ value: hexGasTotal,
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee }))
+
+ const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
+ const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
+
+ dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal }))
+ }
+}
+
+export function setTransactionToConfirm (transactionId) {
+ return async (dispatch, getState) => {
+ const state = getState()
+ const unconfirmedTransactionsHash = unconfirmedTransactionsHashSelector(state)
+ const transaction = unconfirmedTransactionsHash[transactionId]
+
+ if (!transaction) {
+ console.error(`Transaction with id ${transactionId} not found`)
+ return
+ }
+
+ if (transaction.txParams) {
+ const { lastGasPrice } = transaction
+ const txData = lastGasPrice ? increaseFromLastGasPrice(transaction) : transaction
+ dispatch(updateTxDataAndCalculate(txData))
+
+ const { txParams } = transaction
+
+ if (txParams.data) {
+ const { tokens: existingTokens } = state
+ const { data, to: tokenAddress } = txParams
+
+ try {
+ dispatch(setFetchingMethodData(true))
+ const methodData = await getMethodData(data)
+ dispatch(updateMethodData(methodData))
+ dispatch(setFetchingMethodData(false))
+ } catch (error) {
+ dispatch(updateMethodData({}))
+ dispatch(setFetchingMethodData(false))
+ }
+
+ const tokenData = getTokenData(data)
+ dispatch(updateTokenData(tokenData))
+
+ try {
+ const tokenSymbolData = await getSymbolAndDecimals(tokenAddress, existingTokens) || {}
+ const { symbol: tokenSymbol = '', decimals: tokenDecimals = '' } = tokenSymbolData
+ dispatch(updateTokenProps({ tokenSymbol, tokenDecimals }))
+ } catch (error) {
+ dispatch(updateTokenProps({ tokenSymbol: '', tokenDecimals: '' }))
+ }
+ }
+
+ if (txParams.nonce) {
+ const nonce = conversionUtil(txParams.nonce, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+
+ dispatch(updateNonce(nonce))
+ }
+ } else {
+ dispatch(updateTxData(transaction))
+ }
+ }
+}
+
+export function clearConfirmTransaction () {
+ return {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+}
diff --git a/ui/app/ducks/send.duck.js b/ui/app/ducks/send.duck.js
index 055cc05c1..db01bbaa9 100644
--- a/ui/app/ducks/send.duck.js
+++ b/ui/app/ducks/send.duck.js
@@ -6,6 +6,7 @@ const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
+const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
@@ -42,6 +43,8 @@ export default function reducer ({ send: sendState = initState }, action = {}) {
...action.value,
},
})
+ case RESET_SEND_STATE:
+ return extend({}, initState)
default:
return newState
}
@@ -70,3 +73,7 @@ export function updateSendErrors (errorObject) {
value: errorObject,
}
}
+
+export function resetSendState () {
+ return { type: RESET_SEND_STATE }
+}
diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js
new file mode 100644
index 000000000..111674e33
--- /dev/null
+++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js
@@ -0,0 +1,675 @@
+import assert from 'assert'
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+
+import ConfirmTransactionReducer, * as actions from '../confirm-transaction.duck.js'
+
+const initialState = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexGasTotal: '',
+ nonce: '',
+ fetchingMethodData: false,
+}
+
+const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
+const CLEAR_TX_DATA = 'metamask/confirm-transaction/CLEAR_TX_DATA'
+const UPDATE_TOKEN_DATA = 'metamask/confirm-transaction/UPDATE_TOKEN_DATA'
+const CLEAR_TOKEN_DATA = 'metamask/confirm-transaction/CLEAR_TOKEN_DATA'
+const UPDATE_METHOD_DATA = 'metamask/confirm-transaction/UPDATE_METHOD_DATA'
+const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA'
+const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS'
+const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES'
+const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS'
+const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL'
+const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
+const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
+const FETCH_METHOD_DATA_START = 'metamask/confirm-transaction/FETCH_METHOD_DATA_START'
+const FETCH_METHOD_DATA_END = 'metamask/confirm-transaction/FETCH_METHOD_DATA_END'
+const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION'
+
+describe('Confirm Transaction Duck', () => {
+ describe('State changes', () => {
+ const mockState = {
+ confirmTransaction: {
+ txData: {
+ id: 1,
+ },
+ tokenData: {
+ name: 'abcToken',
+ },
+ methodData: {
+ name: 'approve',
+ },
+ tokenProps: {
+ tokenDecimals: '3',
+ tokenSymbol: 'ABC',
+ },
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '1.000021',
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ nonce: '0x0',
+ fetchingMethodData: false,
+ },
+ }
+
+ it('should initialize state', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({}),
+ initialState
+ )
+ })
+
+ it('should return state unchanged if it does not match a dispatched actions type', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: 'someOtherAction',
+ value: 'someValue',
+ }),
+ { ...mockState.confirmTransaction },
+ )
+ })
+
+ it('should set txData when receiving a UPDATE_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TX_DATA,
+ payload: {
+ id: 2,
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {
+ ...mockState.confirmTransaction.txData,
+ id: 2,
+ },
+ }
+ )
+ })
+
+ it('should clear txData when receiving a CLEAR_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TX_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {},
+ }
+ )
+ })
+
+ it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_DATA,
+ payload: {
+ name: 'defToken',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {
+ ...mockState.confirmTransaction.tokenData,
+ name: 'defToken',
+ },
+ }
+ )
+ })
+
+ it('should clear tokenData when receiving a CLEAR_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TOKEN_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {},
+ }
+ )
+ })
+
+ it('should set methodData when receiving a UPDATE_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_METHOD_DATA,
+ payload: {
+ name: 'transferFrom',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {
+ ...mockState.confirmTransaction.methodData,
+ name: 'transferFrom',
+ },
+ }
+ )
+ })
+
+ it('should clear methodData when receiving a CLEAR_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_METHOD_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {},
+ }
+ )
+ })
+
+ it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: {
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ }
+ )
+ })
+
+ it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: {
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ }
+ )
+ })
+
+ it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: {
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ }
+ )
+ })
+
+ it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: '0x0',
+ }),
+ {
+ ...mockState.confirmTransaction,
+ hexGasTotal: '0x0',
+ }
+ )
+ })
+
+ it('should update tokenProps when receiving an UPDATE_TOKEN_PROPS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_PROPS,
+ payload: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenProps: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }
+ )
+ })
+
+ it('should update nonce when receiving an UPDATE_NONCE action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_NONCE,
+ payload: '0x1',
+ }),
+ {
+ ...mockState.confirmTransaction,
+ nonce: '0x1',
+ }
+ )
+ })
+
+ it('should set fetchingMethodData to true when receiving a FETCH_METHOD_DATA_START action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: FETCH_METHOD_DATA_START,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fetchingMethodData: true,
+ }
+ )
+ })
+
+ it('should set fetchingMethodData to false when receiving a FETCH_METHOD_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({ confirmTransaction: { fetchingMethodData: true } }, {
+ type: FETCH_METHOD_DATA_END,
+ }),
+ {
+ fetchingMethodData: false,
+ }
+ )
+ })
+
+ it('should clear confirmTransaction when receiving a FETCH_METHOD_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }),
+ {
+ ...initialState,
+ }
+ )
+ })
+ })
+
+ describe('Single actions', () => {
+ it('should create an action to update txData', () => {
+ const txData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+
+ assert.deepEqual(
+ actions.updateTxData(txData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear txData', () => {
+ const expectedAction = {
+ type: CLEAR_TX_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTxData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenData', () => {
+ const tokenData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenData(tokenData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear tokenData', () => {
+ const expectedAction = {
+ type: CLEAR_TOKEN_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTokenData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update methodData', () => {
+ const methodData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+
+ assert.deepEqual(
+ actions.updateMethodData(methodData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear methodData', () => {
+ const expectedAction = {
+ type: CLEAR_METHOD_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearMethodData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction amounts', () => {
+ const transactionAmounts = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: transactionAmounts,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionAmounts(transactionAmounts),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction fees', () => {
+ const transactionFees = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: transactionFees,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionFees(transactionFees),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction totals', () => {
+ const transactionTotals = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: transactionTotals,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionTotals(transactionTotals),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update hexGasTotal', () => {
+ const hexGasTotal = '0x0'
+ const expectedAction = {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: hexGasTotal,
+ }
+
+ assert.deepEqual(
+ actions.updateHexGasTotal(hexGasTotal),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenProps', () => {
+ const tokenProps = {
+ tokenDecimals: '1',
+ tokenSymbol: 'abc',
+ }
+ const expectedAction = {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenProps(tokenProps),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update nonce', () => {
+ const nonce = '0x1'
+ const expectedAction = {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+
+ assert.deepEqual(
+ actions.updateNonce(nonce),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingMethodData to true', () => {
+ const expectedAction = {
+ type: FETCH_METHOD_DATA_START,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingMethodData(true),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingMethodData to false', () => {
+ const expectedAction = {
+ type: FETCH_METHOD_DATA_END,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingMethodData(false),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear confirmTransaction', () => {
+ const expectedAction = {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+
+ assert.deepEqual(
+ actions.clearConfirmTransaction(),
+ expectedAction
+ )
+ })
+ })
+
+ describe('Thunk actions', done => {
+ it('updates txData and gas on an existing transaction in confirmTransaction', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingMethodData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateGasAndCalculate({ gasLimit: '0x2', gasPrice: '0x25' }))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates txData and updates gas values in confirmTransaction', () => {
+ const txData = {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ }
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingMethodData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ },
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateTxDataAndCalculate(txData))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates confirmTransaction transaction', done => {
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ network: '3',
+ unapprovedTxs: {
+ 2603411941761054: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ },
+ },
+ },
+ confirmTransaction: {},
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.setTransactionToConfirm(2603411941761054))
+ .then(() => {
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ done()
+ })
+ })
+ })
+})
diff --git a/ui/app/ducks/tests/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js
index c06cf55d2..c101132d9 100644
--- a/ui/app/ducks/tests/send-duck.test.js
+++ b/ui/app/ducks/tests/send-duck.test.js
@@ -24,6 +24,7 @@ describe('Send Duck', () => {
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
+ const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
describe('SendReducer()', () => {
it('should initialize state', () => {
@@ -105,6 +106,15 @@ describe('Send Duck', () => {
})
)
})
+
+ it('should return the initial state in response to a RESET_SEND_STATE action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: RESET_SEND_STATE,
+ }),
+ Object.assign({}, initState)
+ )
+ })
})
describe('openFromDropdown', () => {
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
new file mode 100644
index 000000000..a37778c19
--- /dev/null
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -0,0 +1,148 @@
+import currencyFormatter from 'currency-formatter'
+import currencies from 'currency-formatter/currencies'
+import abi from 'human-standard-token-abi'
+import abiDecoder from 'abi-decoder'
+import ethUtil from 'ethereumjs-util'
+import BigNumber from 'bignumber.js'
+
+abiDecoder.addABI(abi)
+
+import MethodRegistry from 'eth-method-registry'
+const registry = new MethodRegistry({ provider: global.ethereumProvider })
+
+import {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} from '../../conversion-util'
+
+import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
+
+export function getTokenData (data = {}) {
+ return abiDecoder.decodeMethod(data)
+}
+
+export async function getMethodData (data = {}) {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ const sig = await registry.lookup(fourBytePrefix)
+ const parsedResult = registry.parse(sig)
+
+ return {
+ name: parsedResult.name,
+ params: parsedResult.args,
+ }
+}
+
+export function increaseLastGasPrice (lastGasPrice) {
+ return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ toNumericBase: 'hex',
+ }))
+}
+
+export function hexGreaterThan (a, b) {
+ return conversionGreaterThan(
+ { value: a, fromNumericBase: 'hex' },
+ { value: b, fromNumericBase: 'hex' },
+ )
+}
+
+export function getHexGasTotal ({ gasLimit, gasPrice }) {
+ return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ }))
+}
+
+export function addEth (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 6,
+ })
+ })
+}
+
+export function addFiat (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 2,
+ })
+ })
+}
+
+export function getTransactionAmount ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: 'WEI',
+ conversionRate,
+ })
+}
+
+export function getTransactionFee ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency,
+ numberOfDecimals,
+ conversionRate,
+ })
+}
+
+export function formatCurrency (value, currencyCode) {
+ const upperCaseCurrencyCode = currencyCode.toUpperCase()
+
+ return currencies.find(currency => currency.code === upperCaseCurrencyCode)
+ ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode })
+ : value
+}
+
+export function convertTokenToFiat ({
+ value,
+ toCurrency,
+ conversionRate,
+ contractExchangeRate,
+}) {
+ const totalExchangeRate = conversionRate * contractExchangeRate
+
+ return conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'dec',
+ toCurrency,
+ numberOfDecimals: 2,
+ conversionRate: totalExchangeRate,
+ })
+}
+
+export function hasUnconfirmedTransactions (state) {
+ return unconfirmedTransactionsCountSelector(state) > 0
+}
+
+export function roundExponential (value) {
+ const PRECISION = 4
+ const bigNumberValue = new BigNumber(value)
+
+ // In JS, numbers with exponentials greater than 20 get displayed as an exponential.
+ return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
+}
diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js
new file mode 100644
index 000000000..a9c8fae34
--- /dev/null
+++ b/ui/app/helpers/confirm-transaction/util.test.js
@@ -0,0 +1,137 @@
+import * as utils from './util'
+import assert from 'assert'
+
+describe('Confirm Transaction utils', () => {
+ describe('increaseLastGasPrice', () => {
+ it('should increase the gasPrice by 10%', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('0xa')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+
+ it('should prefix the result with 0x', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('a')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+ })
+
+ describe('hexGreaterThan', () => {
+ it('should return true if the first value is greater than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', '0xa'),
+ true
+ )
+ })
+
+ it('should return false if the first value is less than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xb'),
+ false
+ )
+ })
+
+ it('should return false if the first value is equal to the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xa'),
+ false
+ )
+ })
+
+ it('should correctly compare prefixed and non-prefixed hex values', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', 'a'),
+ true
+ )
+ })
+ })
+
+ describe('getHexGasTotal', () => {
+ it('should multiply the hex gasLimit and hex gasPrice values together', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+
+ it('should prefix the result with 0x', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+ })
+
+ describe('addEth', () => {
+ it('should add two values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.12345678', '0'),
+ '0.123457'
+ )
+ })
+
+ it('should add any number of values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.123457'
+ )
+ })
+ })
+
+ describe('addFiat', () => {
+ it('should add two values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.12345678', '0'),
+ '0.12'
+ )
+ })
+
+ it('should add any number of values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.12'
+ )
+ })
+ })
+
+ describe('getTransactionAmount', () => {
+ it('should get the transaction amount in ETH', () => {
+ const ethTransactionAmount = utils.getTransactionAmount({
+ value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionAmount, '1')
+ })
+
+ it('should get the transaction amount in fiat', () => {
+ const fiatTransactionAmount = utils.getTransactionAmount({
+ value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionAmount, '468.58')
+ })
+ })
+
+ describe('getTransactionFee', () => {
+ it('should get the transaction fee in ETH', () => {
+ const ethTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionFee, '0.000021')
+ })
+
+ it('should get the transaction fee in fiat', () => {
+ const fiatTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionFee, '0.01')
+ })
+ })
+
+ describe('formatCurrency', () => {
+ it('should format USD values', () => {
+ const value = utils.formatCurrency('123.45', 'usd')
+ assert.equal(value, '$123.45')
+ })
+ })
+})
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js
index e24517c18..8608b15f4 100644
--- a/ui/app/helpers/with-token-tracker.js
+++ b/ui/app/helpers/with-token-tracker.js
@@ -75,6 +75,9 @@ const withTokenTracker = WrappedComponent => {
}
updateBalance (tokens = []) {
+ if (!this.tracker.running) {
+ return
+ }
const [{ string, symbol }] = tokens
this.setState({ string, symbol, error: null })
}
diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js
index 2856e0ed6..d46911f7c 100644
--- a/ui/app/i18n-provider.js
+++ b/ui/app/i18n-provider.js
@@ -8,8 +8,11 @@ const t = require('../i18n-helper').getMessage
class I18nProvider extends Component {
getChildContext () {
const { localeMessages } = this.props
+ const { current, en } = localeMessages
return {
- t: t.bind(null, localeMessages),
+ t (key, ...args) {
+ return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
+ },
}
}
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
index b49a52363..8a0708025 100644
--- a/ui/app/main-container.js
+++ b/ui/app/main-container.js
@@ -3,9 +3,10 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details')
const Settings = require('./components/pages/settings')
-const UnlockScreen = require('./components/pages/unlock-page')
const log = require('loglevel')
+import UnlockScreen from './components/pages/unlock-page'
+
module.exports = MainContainer
inherits(MainContainer, Component)
diff --git a/ui/app/metamask-connect.js b/ui/app/metamask-connect.js
deleted file mode 100644
index 81fa7e403..000000000
--- a/ui/app/metamask-connect.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const connect = require('react-redux').connect
-const t = require('../i18n-helper').getMessage
-
-const metamaskConnect = (mapStateToProps, mapDispatchToProps) => {
- return connect(
- _higherOrderMapStateToProps(mapStateToProps),
- mapDispatchToProps
- )
-}
-
-const _higherOrderMapStateToProps = (mapStateToProps) => {
- let _t
- let currentLocale
- return (state, ownProps = {}) => {
- const stateProps = mapStateToProps
- ? mapStateToProps(state, ownProps)
- : ownProps
- if (currentLocale !== state.metamask.currentLocale) {
- currentLocale = state.metamask.currentLocale
- _t = t.bind(null, state.localeMessages)
- }
- stateProps.t = _t
- return stateProps
- }
-}
-
-module.exports = metamaskConnect
diff --git a/ui/app/reducers.js b/ui/app/reducers.js
index 0b158a778..80e76d570 100644
--- a/ui/app/reducers.js
+++ b/ui/app/reducers.js
@@ -8,6 +8,7 @@ const reduceMetamask = require('./reducers/metamask')
const reduceApp = require('./reducers/app')
const reduceLocale = require('./reducers/locale')
const reduceSend = require('./ducks/send.duck').default
+import reduceConfirmTransaction from './ducks/confirm-transaction.duck'
window.METAMASK_CACHED_LOG_STATE = null
@@ -45,6 +46,8 @@ function rootReducer (state, action) {
state.send = reduceSend(state, action)
+ state.confirmTransaction = reduceConfirmTransaction(state, action)
+
window.METAMASK_CACHED_LOG_STATE = state
return state
}
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 4e9d0848c..50d8bcba7 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -49,6 +49,8 @@ function reduceApp (state, action) {
},
},
sidebarOpen: false,
+ alertOpen: false,
+ alertMessage: null,
networkDropdownOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
@@ -62,6 +64,7 @@ function reduceApp (state, action) {
warning: null,
buyView: {},
isMouseUser: false,
+ gasIsLoading: false,
}, state.appState)
switch (action.type) {
@@ -87,6 +90,19 @@ function reduceApp (state, action) {
sidebarOpen: false,
})
+ // sidebar methods
+ case actions.ALERT_OPEN:
+ return extend(appState, {
+ alertOpen: true,
+ alertMessage: action.value,
+ })
+
+ case actions.ALERT_CLOSE:
+ return extend(appState, {
+ alertOpen: false,
+ alertMessage: null,
+ })
+
// modal methods:
case actions.MODAL_OPEN:
const { name, ...modalProps } = action.payload
@@ -675,6 +691,16 @@ function reduceApp (state, action) {
isMouseUser: action.value,
})
+ case actions.GAS_LOADING_STARTED:
+ return extend(appState, {
+ gasIsLoading: true,
+ })
+
+ case actions.GAS_LOADING_FINISHED:
+ return extend(appState, {
+ gasIsLoading: false,
+ })
+
default:
return appState
}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 6c8ac9ed7..3f1d3394f 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -222,6 +222,14 @@ function reduceMetamask (state, action) {
},
})
+ case actions.UPDATE_SEND_HEX_DATA:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ data: action.value,
+ },
+ })
+
case actions.UPDATE_SEND_FROM:
return extend(metamaskState, {
send: {
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 0ff3f644d..f6b2a7a55 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -9,9 +9,8 @@ const ADD_TOKEN_ROUTE = '/add-token'
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
+const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
const SEND_ROUTE = '/send'
-const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
-const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request'
const NOTICE_ROUTE = '/notice'
const WELCOME_ROUTE = '/welcome'
const INITIALIZE_ROUTE = '/initialize'
@@ -23,6 +22,15 @@ const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
const INITIALIZE_BACKUP_PHRASE_ROUTE = '/initialize/backup-phrase'
const INITIALIZE_CONFIRM_SEED_ROUTE = '/initialize/confirm-phrase'
+const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
+const CONFIRM_SEND_ETHER_PATH = '/send-ether'
+const CONFIRM_SEND_TOKEN_PATH = '/send-token'
+const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'
+const CONFIRM_APPROVE_PATH = '/approve'
+const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
+const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
+const SIGNATURE_REQUEST_PATH = '/signature-request'
+
module.exports = {
DEFAULT_ROUTE,
UNLOCK_ROUTE,
@@ -35,10 +43,9 @@ module.exports = {
CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
SEND_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
- SIGNATURE_REQUEST_ROUTE,
WELCOME_ROUTE,
INITIALIZE_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
@@ -48,4 +55,12 @@ module.exports = {
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
INITIALIZE_CONFIRM_SEED_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index a29294b86..d86462275 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -16,8 +16,7 @@ const selectors = {
transactionsSelector,
accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo,
- getGasPrice,
- getGasLimit,
+ getGasIsLoading,
getForceGasMin,
getAddressBook,
getSendFrom,
@@ -28,6 +27,7 @@ const selectors = {
autoAddToBetaUI,
getSendMaxModeState,
getCurrentViewContext,
+ getTotalUnapprovedCount,
}
module.exports = selectors
@@ -117,12 +117,8 @@ function transactionsSelector (state) {
.sort((a, b) => b.time - a.time)
}
-function getGasPrice (state) {
- return state.metamask.send.gasPrice
-}
-
-function getGasLimit (state) {
- return state.metamask.send.gasLimit
+function getGasIsLoading (state) {
+ return state.appState.gasIsLoading
}
function getForceGasMin (state) {
@@ -186,3 +182,15 @@ function getCurrentViewContext (state) {
const { currentView = {} } = state.appState
return currentView.context
}
+
+function getTotalUnapprovedCount ({ metamask }) {
+ const {
+ unapprovedTxs = {},
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+
+ return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
+ unapprovedTypedMessagesCount
+}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
new file mode 100644
index 000000000..9548cf75e
--- /dev/null
+++ b/ui/app/selectors/confirm-transaction.js
@@ -0,0 +1,196 @@
+import { createSelector } from 'reselect'
+import txHelper from '../../lib/tx-helper'
+import { calcTokenAmount } from '../token-util'
+import { roundExponential } from '../helpers/confirm-transaction/util'
+
+const unapprovedTxsSelector = state => state.metamask.unapprovedTxs
+const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
+const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs
+const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages
+const networkSelector = state => state.metamask.network
+
+export const unconfirmedTransactionsListSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgsSelector,
+ unapprovedPersonalMsgsSelector,
+ unapprovedTypedMessagesSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgs = {},
+ unapprovedPersonalMsgs = {},
+ unapprovedTypedMessages = {},
+ network
+ ) => txHelper(
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ network
+ ) || []
+)
+
+export const unconfirmedTransactionsHashSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgsSelector,
+ unapprovedPersonalMsgsSelector,
+ unapprovedTypedMessagesSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgs = {},
+ unapprovedPersonalMsgs = {},
+ unapprovedTypedMessages = {},
+ network
+ ) => {
+ const filteredUnapprovedTxs = Object.keys(unapprovedTxs).reduce((acc, address) => {
+ const { metamaskNetworkId } = unapprovedTxs[address]
+ const transactions = { ...acc }
+
+ if (metamaskNetworkId === network) {
+ transactions[address] = unapprovedTxs[address]
+ }
+
+ return transactions
+ }, {})
+
+ return {
+ ...filteredUnapprovedTxs,
+ ...unapprovedMsgs,
+ ...unapprovedPersonalMsgs,
+ ...unapprovedTypedMessages,
+ }
+ }
+)
+
+const unapprovedMsgCountSelector = state => state.metamask.unapprovedMsgCount
+const unapprovedPersonalMsgCountSelector = state => state.metamask.unapprovedPersonalMsgCount
+const unapprovedTypedMessagesCountSelector = state => state.metamask.unapprovedTypedMessagesCount
+
+export const unconfirmedTransactionsCountSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgCountSelector,
+ unapprovedPersonalMsgCountSelector,
+ unapprovedTypedMessagesCountSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgCount = 0,
+ unapprovedPersonalMsgCount = 0,
+ unapprovedTypedMessagesCount = 0,
+ network
+ ) => {
+ const filteredUnapprovedTxIds = Object.keys(unapprovedTxs).filter(txId => {
+ const { metamaskNetworkId } = unapprovedTxs[txId]
+ return metamaskNetworkId === network
+ })
+
+ return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount +
+ unapprovedPersonalMsgCount
+ }
+)
+
+
+export const currentCurrencySelector = state => state.metamask.currentCurrency
+export const conversionRateSelector = state => state.metamask.conversionRate
+
+const txDataSelector = state => state.confirmTransaction.txData
+const tokenDataSelector = state => state.confirmTransaction.tokenData
+const tokenPropsSelector = state => state.confirmTransaction.tokenProps
+
+const contractExchangeRatesSelector = state => state.metamask.contractExchangeRates
+
+const tokenDecimalsSelector = createSelector(
+ tokenPropsSelector,
+ tokenProps => tokenProps && tokenProps.tokenDecimals
+)
+
+const tokenDataParamsSelector = createSelector(
+ tokenDataSelector,
+ tokenData => tokenData && tokenData.params || []
+)
+
+const txParamsSelector = createSelector(
+ txDataSelector,
+ txData => txData && txData.txParams || {}
+)
+
+export const tokenAddressSelector = createSelector(
+ txParamsSelector,
+ txParams => txParams && txParams.to
+)
+
+const TOKEN_PARAM_SPENDER = '_spender'
+const TOKEN_PARAM_TO = '_to'
+const TOKEN_PARAM_VALUE = '_value'
+
+export const tokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ params => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ const toParam = params.find(param => param.name === TOKEN_PARAM_TO)
+ const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE)
+ toAddress = toParam ? toParam.value : params[0].value
+ const value = valueParam ? Number(valueParam.value) : Number(params[1].value)
+ tokenAmount = roundExponential(value)
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const approveTokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ params => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
+ const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
+ tokenAmount = roundExponential(value)
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const sendTokenTokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ tokenDecimalsSelector,
+ (params, tokenDecimals) => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value
+ let value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
+
+ if (tokenDecimals) {
+ value = calcTokenAmount(value, tokenDecimals)
+ }
+
+ tokenAmount = roundExponential(value)
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const contractExchangeRateSelector = createSelector(
+ contractExchangeRatesSelector,
+ tokenAddressSelector,
+ (contractExchangeRates, tokenAddress) => contractExchangeRates[tokenAddress]
+)
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index 8c5b37d7b..0d4233766 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -1,5 +1,6 @@
const log = require('loglevel')
const util = require('./util')
+const BigNumber = require('bignumber.js')
function tokenInfoGetter () {
const tokens = {}
@@ -20,7 +21,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
if (existingToken) {
return existingToken
}
-
+
let result = []
try {
const token = util.getContractAtAddress(tokenAddress)
@@ -43,9 +44,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
- const amount = Number(value / multiplier)
-
- return amount
+ return new BigNumber(value).div(multiplier).toNumber()
}
diff --git a/ui/app/util.js b/ui/app/util.js
index 1ccd17ba7..8b194e0c7 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -36,6 +36,7 @@ module.exports = {
miniAddressSummary: miniAddressSummary,
isAllOneCase: isAllOneCase,
isValidAddress: isValidAddress,
+ isValidENSAddress,
numericBalance: numericBalance,
parseBalance: parseBalance,
formatBalance: formatBalance,
@@ -58,6 +59,7 @@ module.exports = {
allNull,
getTokenAddressFromTokenObject,
checksumAddress,
+ addressSlicer,
}
function valuesFor (obj) {
@@ -87,6 +89,10 @@ function isValidAddress (address) {
return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
}
+function isValidENSAddress (address) {
+ return address.match(/^.{7,}\.(eth|test)$/)
+}
+
function isInvalidChecksumAddress (address) {
var prefixed = ethUtil.addHexPrefix(address)
if (address === '0x0000000000000000000000000000000000000000') return false
@@ -298,3 +304,11 @@ function getTokenAddressFromTokenObject (token) {
function checksumAddress (address) {
return address ? ethUtil.toChecksumAddress(address) : ''
}
+
+function addressSlicer (address = '') {
+ if (address.length < 11) {
+ return address
+ }
+
+ return `${address.slice(0, 6)}...${address.slice(-4)}`
+}
diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js
index 79aa93116..bc927ee65 100644
--- a/ui/i18n-helper.js
+++ b/ui/i18n-helper.js
@@ -1,20 +1,22 @@
// cross-browser connection to extension i18n API
const log = require('loglevel')
+/**
+ * Returns a localized message for the given key
+ * @param {object} locale The locale
+ * @param {string} key The message key
+ * @param {string[]} substitutions A list of message substitution replacements
+ * @return {null|string} The localized message
+ */
const getMessage = (locale, key, substitutions) => {
- // check locale is loaded
if (!locale) {
- // throw new Error('Translator - has not loaded a locale yet.')
- return ''
+ return null
}
- // check entry is present
- const { current, en } = locale
- const entry = current[key] || en[key]
- if (!entry) {
- // throw new Error(`Translator - Unable to find value for "${key}"`)
- log.error(`Translator - Unable to find value for "${key}"`)
- return `[${key}]`
+ if (!locale[key]) {
+ log.error(`Translator - Unable to find value for key "${key}"`)
+ return null
}
+ const entry = locale[key]
let phrase = entry.message
// perform substitutions
if (substitutions && substitutions.length) {
@@ -29,8 +31,7 @@ const getMessage = (locale, key, substitutions) => {
async function fetchLocale (localeName) {
try {
const response = await fetch(`./_locales/${localeName}/messages.json`)
- const locale = await response.json()
- return locale
+ return await response.json()
} catch (error) {
log.error(`failed to fetch ${localeName} locale because of ${error}`)
return {}