aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml62
-rwxr-xr-x.circleci/scripts/firefox-download.sh17
-rwxr-xr-x.circleci/scripts/firefox-install.sh4
-rw-r--r--CHANGELOG.md2
-rw-r--r--CONTRIBUTING.md31
-rw-r--r--README.md2
-rw-r--r--app/_locales/en/messages.json39
-rw-r--r--app/images/alert-red.svg14
-rw-r--r--app/images/alert.svg19
-rw-r--r--app/images/caret-left.svg18
-rw-r--r--app/scripts/contentscript.js1
-rw-r--r--app/scripts/controllers/network/enums.js3
-rw-r--r--app/scripts/controllers/transactions/nonce-tracker.js13
-rw-r--r--app/scripts/controllers/transactions/tx-state-manager.js6
-rw-r--r--app/scripts/metamask-controller.js1
-rw-r--r--app/scripts/migrations/027.js35
-rw-r--r--development/backGroundConnectionModifiers.js2
-rw-r--r--development/states/confirm-new-ui.json18
-rw-r--r--development/states/confirm-sig-requests.json37
-rw-r--r--development/states/currency-localization.json20
-rw-r--r--development/states/first-time.json20
-rw-r--r--development/states/send-edit.json18
-rw-r--r--development/states/send-new-ui.json18
-rw-r--r--old-ui/app/keychains/hd/restore-vault.js3
-rw-r--r--package-lock.json134
-rw-r--r--package.json3
-rw-r--r--test/e2e/beta/contract-test/contract.js114
-rw-r--r--test/e2e/beta/contract-test/index.html33
-rw-r--r--test/e2e/beta/helpers.js61
-rw-r--r--test/e2e/beta/metamask-beta-ui.spec.js378
-rw-r--r--test/e2e/metamask.spec.js2
-rw-r--r--test/integration/lib/confirm-sig-requests.js12
-rw-r--r--test/integration/lib/currency-localization.js1
-rw-r--r--test/integration/lib/send-new-ui.js27
-rw-r--r--test/unit/actions/tx_test.js102
-rw-r--r--test/unit/app/controllers/transactions/tx-controller-test.js13
-rw-r--r--test/unit/app/controllers/transactions/tx-state-manager-test.js19
-rw-r--r--test/unit/migrations/027-test.js50
-rw-r--r--ui/app/actions.js104
-rw-r--r--ui/app/app.js12
-rw-r--r--ui/app/components/app-header/app-header.component.js3
-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.scss43
-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/dropdowns/components/network-dropdown-icon.js3
-rw-r--r--ui/app/components/index.scss10
-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.scss2
-rw-r--r--ui/app/components/modals/modal.js28
-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.js30
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.container.js28
-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.js39
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.container.js72
-rw-r--r--ui/app/components/pages/confirm-send-token/index.js1
-rw-r--r--ui/app/components/pages/confirm-send-token/index.scss19
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js320
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js169
-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.js77
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js2
-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.js150
-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/home.js96
-rw-r--r--ui/app/components/pages/index.scss2
-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/send_/send-content/send-amount-row/send-amount-row.component.js15
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js5
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js29
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js18
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js8
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js5
-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-component.test.js5
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js6
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js33
-rw-r--r--ui/app/components/send_/send-footer/send-footer.component.js6
-rw-r--r--ui/app/components/send_/send-footer/send-footer.container.js2
-rw-r--r--ui/app/components/send_/send-footer/tests/send-footer-component.test.js11
-rw-r--r--ui/app/components/send_/send.component.js19
-rw-r--r--ui/app/components/send_/send.container.js2
-rw-r--r--ui/app/components/send_/send.utils.js41
-rw-r--r--ui/app/components/send_/tests/send-component.test.js75
-rw-r--r--ui/app/components/send_/tests/send-container.test.js12
-rw-r--r--ui/app/components/send_/tests/send-utils.test.js46
-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/signature-request.js13
-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.js6
-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.js9
-rw-r--r--ui/app/components/tx-view.js37
-rw-r--r--ui/app/conf-tx.js3
-rw-r--r--ui/app/constants/error-keys.js3
-rw-r--r--ui/app/css/itcss/components/buttons.scss25
-rw-r--r--ui/app/css/itcss/components/index.scss2
-rw-r--r--ui/app/css/itcss/components/network.scss12
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss9
-rw-r--r--ui/app/css/itcss/components/request-signature.scss1
-rw-r--r--ui/app/css/itcss/components/token-list.scss1
-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.js116
-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/routes.js19
-rw-r--r--ui/app/selectors.js13
-rw-r--r--ui/app/selectors/confirm-transaction.js65
-rw-r--r--ui/i18n-helper.js25
171 files changed, 5967 insertions, 1143 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0ad5cdf53..7063f3113 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -101,29 +101,31 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ keys:
+ - dependency-cache-{{ checksum "package-lock.json" }}
+ # fallback to using the latest cache if no exact match is found
+ - dependency-cache-
- run:
- name: Install deps via npm
- command: npm install
+ name: Install npm 6 + deps via npm
+ command: |
+ sudo npm install -g npm@6.1.0 && npm install
- save_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- node_modules
- - save_cache:
- key: dependency-cache-{{ .Revision }}
- paths:
- - node_modules
prep-deps-firefox:
docker:
- image: circleci/node:8.11.3-browsers
steps:
- checkout
+ - restore_cache:
+ key: dependency-cache-firefox-
- run:
- name: Download Firefox
+ name: Download Firefox If needed
command: ./.circleci/scripts/firefox-download.sh
- save_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: dependency-cache-firefox-
paths:
- firefox
@@ -133,7 +135,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: build:dist
command: npm run dist
@@ -152,7 +154,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: build:dist
command: npm run doc
@@ -167,7 +169,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -186,7 +188,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Test
command: npm run lint
@@ -197,7 +199,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Test
command: npx nsp check
@@ -208,7 +210,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -224,12 +226,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -245,7 +247,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -261,12 +263,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -282,7 +284,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -299,7 +301,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
@@ -326,7 +328,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
@@ -349,7 +351,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: test:coverage
command: npm run test:coverage
@@ -362,12 +364,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -386,7 +388,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -405,12 +407,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -429,7 +431,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
diff --git a/.circleci/scripts/firefox-download.sh b/.circleci/scripts/firefox-download.sh
index c63e8c3df..64f0c74e3 100755
--- a/.circleci/scripts/firefox-download.sh
+++ b/.circleci/scripts/firefox-download.sh
@@ -1,6 +1,13 @@
#!/usr/bin/env bash
-
-echo "Downloading firefox..."
-wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 \
-&& tar xjf firefox-58.0.tar.bz2
-echo "firefox download complete"
+echo "Checking if firefox was already downloaded"
+if [ -d "firefox" ]
+then
+ echo "Firefox found. No need to download"
+else
+ FIREFOX_VERSION="61.0.1"
+ FIREFOX_BINARY="firefox-$FIREFOX_VERSION.tar.bz2"
+ echo "Downloading firefox..."
+ wget "https://ftp.mozilla.org/pub/firefox/releases/$FIREFOX_VERSION/linux-x86_64/en-US/$FIREFOX_BINARY" \
+ && tar xjf "$FIREFOX_BINARY"
+ echo "firefox download complete"
+fi
diff --git a/.circleci/scripts/firefox-install.sh b/.circleci/scripts/firefox-install.sh
index 589bcbbb5..1c60f4de9 100755
--- a/.circleci/scripts/firefox-install.sh
+++ b/.circleci/scripts/firefox-install.sh
@@ -2,7 +2,7 @@
echo "Installing firefox..."
sudo rm -r /opt/firefox
-sudo mv firefox /opt/firefox58
+sudo mv firefox /opt/firefox61
sudo mv /usr/bin/firefox /usr/bin/firefox-old
-sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
+sudo ln -s /opt/firefox61/firefox /usr/bin/firefox
echo "Firefox installed."
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d86fcd713..4ad52b795 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Current Master
+ - Remove rejected transactions from transaction history
+
## 4.8.0 Thur Jun 14 2018
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8517eed70..63d19c633 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,31 +2,26 @@
If you're submitting code to MetaMask, there are some simple things we'd appreciate you doing to help us stay organized!
-## Submitting pull requests
+### Finding the right project
Before taking the time to code and implement something, feel free to open an issue and discuss it! There may even be an issue already open, and together we may come up with a specific strategy before you take your precious time to write code.
-### Tests
+There are also plenty of open issues we'd love help with. Search the [`good first issue`](https://github.com/MetaMask/metamask-extension/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) label, or head to Gitcoin and earn ETH for completing projects we've posted bounties on.
-For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder.
+If you're picking up a bounty or an existing issue, feel free to ask clarifying questions on the issue as you go about your work.
-### PR Format
+### Submitting a pull request
+When you're done with your project / bugfix / feature and ready to submit a PR, there are a couple guidelines we ask you to follow:
-If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`.
+- [ ] **Test it**: For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder.
+- [ ] **Add to the CHANGELOG**: Help us keep track of all the moving pieces by adding an entry to the [`CHANGELOG.md`](https://github.com/MetaMask/metamask-extension/blob/develop/CHANGELOG.md) with a link to your PR.
+- [ ] **Meet the spec**: Make sure the PR adds functionality that matches the issue you're closing. This is especially important for bounties: sometimes design or implementation details are included in the conversation, so read carefully!
+- [ ] **Close the issue**: If this PR closes an open issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`. If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
+- [ ] **Keep it simple**: Try not to include multiple features in a single PR, and don't make extraneous changes outside the scope of your contribution. All those touched files make things harder to review ;)
+- [ ] **PR against `develop`**: Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production. If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
+- [ ] **Get reviewed by a core contributor**: Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from a user with a `Member` badge before merging.
-If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
-
-Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production.
-
-If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
-
-## Before Merging
-
-Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.
-
-## Before Closing Issues
-
-Make sure the relevant code has been reviewed and merged.
+And that's it! Thanks for helping out.
### Developing inside a node_modules folder
diff --git a/README.md b/README.md
index 70faa8856..513a1d1cb 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ If you're a user seeking support, [here is our support site](https://metamask.he
[Mission Statement](./MISSION.md)
-[Internal documentation](./docs/jsdocs)
+[Internal documentation](./docs#documentation)
## Developing Compatible Dapps
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 46fbdc1a7..35e28c087 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -40,6 +40,9 @@
"message": "MetaMask",
"description": "The name of the application"
},
+ "approve": {
+ "message": "Approve"
+ },
"approved": {
"message": "Approved"
},
@@ -89,6 +92,9 @@
"buyCoinbaseExplainer": {
"message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin."
},
+ "bytes": {
+ "message": "Bytes"
+ },
"ok": {
"message": "Ok"
},
@@ -149,6 +155,9 @@
"copyContractAddress": {
"message": "Copy Contract Address"
},
+ "copyAddress": {
+ "message": "Copy address to clipboard"
+ },
"copyToClipboard": {
"message": "Copy to clipboard"
},
@@ -277,6 +286,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
+ "parameters": {
+ "message": "Parameters"
+ },
"passwordNotLongEnough": {
"message": "Password not long enough"
},
@@ -318,6 +330,9 @@
"fromShapeShift": {
"message": "From ShapeShift"
},
+ "functionType": {
+ "message": "Function Type"
+ },
"gas": {
"message": "Gas",
"description": "Short indication of gas cost"
@@ -370,6 +385,9 @@
"hereList": {
"message": "Here's a list!!!!"
},
+ "hexData": {
+ "message": "Hex Data"
+ },
"hide": {
"message": "Hide"
},
@@ -497,6 +515,9 @@
"mainnet": {
"message": "Main Ethereum Network"
},
+ "menu": {
+ "message": "Menu"
+ },
"message": {
"message": "Message"
},
@@ -578,10 +599,16 @@
"oldUIMessage": {
"message": "You have returned to the old UI. You can switch back to the New UI through the option in the top right dropdown menu."
},
+ "openInTab": {
+ "message": "Open in tab"
+ },
"or": {
"message": "or",
"description": "choice between creating or importing a new account"
},
+ "origin": {
+ "message": "Origin"
+ },
"password": {
"message": "Password"
},
@@ -664,6 +691,9 @@
"restoreVault": {
"message": "Restore Vault"
},
+ "restoreAccountWithSeed": {
+ "message": "Restore your Account with Seed Phrase"
+ },
"required": {
"message": "Required"
},
@@ -673,6 +703,9 @@
"walletSeed": {
"message": "Wallet Seed"
},
+ "restore": {
+ "message": "Restore"
+ },
"revealSeedWords": {
"message": "Reveal Seed Words"
},
@@ -777,6 +810,9 @@
"sendTokens": {
"message": "Send Tokens"
},
+ "separateEachWord": {
+ "message": "Separate each word with a single space"
+ },
"onlySendToEtherAddress": {
"message": "Only send ETH to an Ethereum address."
},
@@ -902,6 +938,9 @@
"transactionNumber": {
"message": "Transaction Number"
},
+ "transfer": {
+ "message": "Transfer"
+ },
"transfers": {
"message": "Transfers"
},
diff --git a/app/images/alert-red.svg b/app/images/alert-red.svg
new file mode 100644
index 000000000..ac5b30e27
--- /dev/null
+++ b/app/images/alert-red.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>Artboard Copy</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Artboard-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group-48">
+ <circle id="Oval" fill="#D0021B" cx="8" cy="8" r="8"></circle>
+ <rect id="Rectangle-41" fill="#FFFFFF" x="7" y="3" width="2" height="7" rx="1"></rect>
+ <rect id="Rectangle-41" fill="#FFFFFF" x="7" y="11" width="2" height="2" rx="1"></rect>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/alert.svg b/app/images/alert.svg
new file mode 100644
index 000000000..534eda194
--- /dev/null
+++ b/app/images/alert.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="29px" height="29px" viewBox="0 0 29 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>7414FFD8-B28A-4593-9D7E-19E73D687B50</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Approve---insufficient-amount" transform="translate(-69.000000, -166.000000)">
+ <g id="Group-7" transform="translate(53.000000, 51.000000)">
+ <g id="Group-34" transform="translate(0.000000, 91.000000)">
+ <g id="alert" transform="translate(16.000000, 24.000000)">
+ <circle id="Oval" fill="#605A1C" cx="14.5" cy="14.5" r="14.5"></circle>
+ <path d="M16,16.8282967 L14,16.8282967 L14,7 L16,7 L16,16.8282967 Z M16,21 L14,21 L14,19 L16,19 L16,21 Z" id="!" fill="#FFFCDB"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/caret-left.svg b/app/images/caret-left.svg
new file mode 100644
index 000000000..0ea266161
--- /dev/null
+++ b/app/images/caret-left.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="9px" height="15px" viewBox="0 0 9 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>8439120D-5704-4273-B416-FEE134322584</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Approve---insufficient-amount" transform="translate(-75.000000, -69.000000)" stroke="#3099F2" stroke-width="2">
+ <g id="Group-7" transform="translate(53.000000, 51.000000)">
+ <g id="cancel" transform="translate(24.000000, 14.000000)">
+ <g id="Group">
+ <polyline id="Path-8" points="6.1263881 18.0633906 0 11.6306831 6.31493631 5"></polyline>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index b35a70dd2..04dd51b01 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -177,6 +177,7 @@ function blacklistedDomainCheck () {
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
'adyen.com',
'gravityforms.com',
+ 'harbourair.com',
]
var currentUrl = window.location.href
var currentRegex
diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js
index 9da7f309c..3190eb37c 100644
--- a/app/scripts/controllers/network/enums.js
+++ b/app/scripts/controllers/network/enums.js
@@ -4,6 +4,7 @@ const KOVAN = 'kovan'
const MAINNET = 'mainnet'
const LOCALHOST = 'localhost'
+const MAINNET_CODE = 1
const ROPSTEN_CODE = 3
const RINKEYBY_CODE = 4
const KOVAN_CODE = 42
@@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby'
const KOVAN_DISPLAY_NAME = 'Kovan'
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
-
module.exports = {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
LOCALHOST,
+ MAINNET_CODE,
ROPSTEN_CODE,
RINKEYBY_CODE,
KOVAN_CODE,
diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js
index 35ca08d6c..06f336eaa 100644
--- a/app/scripts/controllers/transactions/nonce-tracker.js
+++ b/app/scripts/controllers/transactions/nonce-tracker.js
@@ -129,19 +129,6 @@ class NonceTracker {
return Number.isInteger(highest) ? highest + 1 : 0
}
- _reduceTxListToUniqueNonces (txList) {
- const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
- if (!index) return [txMeta]
- const nonceMatches = txList.filter((txData) => {
- return txMeta.txParams.nonce === txData.txParams.nonce
- })
- if (nonceMatches.length > 1) return reducedList
- reducedList.push(txMeta)
- return reducedList
- }, [])
- return reducedTxList
- }
-
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js
index 0aae4774b..28a18ca2e 100644
--- a/app/scripts/controllers/transactions/tx-state-manager.js
+++ b/app/scripts/controllers/transactions/tx-state-manager.js
@@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter {
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
+ this._removeTx(txId)
}
/**
@@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter {
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
+
+ _removeTx (txId) {
+ const transactionList = this.getFullTxList()
+ this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId))
+ }
}
module.exports = TransactionStateManager
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index b4d39031a..450113acf 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -338,6 +338,7 @@ module.exports = class MetamaskController extends EventEmitter {
markAccountsFound: this.markAccountsFound.bind(this),
markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
+ getGasPrice: (cb) => cb(null, this.getGasPrice()),
// coinbase
buyEth: this.buyEth.bind(this),
diff --git a/app/scripts/migrations/027.js b/app/scripts/migrations/027.js
new file mode 100644
index 000000000..d6ebef580
--- /dev/null
+++ b/app/scripts/migrations/027.js
@@ -0,0 +1,35 @@
+// next version number
+const version = 27
+
+/*
+
+normalizes txParams on unconfirmed txs
+
+*/
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: async function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ return versionedData
+ },
+}
+
+function transformState (state) {
+ const newState = state
+
+ if (newState.TransactionController) {
+ if (newState.TransactionController.transactions) {
+ const transactions = newState.TransactionController.transactions
+ newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected')
+ }
+ }
+
+ return newState
+}
diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js
index 665f72898..aee68854b 100644
--- a/development/backGroundConnectionModifiers.js
+++ b/development/backGroundConnectionModifiers.js
@@ -10,7 +10,7 @@ module.exports = {
signPersonalMessage: (msgData, cb) => {
const stateUpdate = {
unapprovedPersonalMsgs: {},
- unapprovedPersonalMsgsCount: 0,
+ unapprovedPersonalMsgCount: 0,
}
return cb(null, stateUpdate)
},
diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json
index 71ccbd96c..2c2e17704 100644
--- a/development/states/confirm-new-ui.json
+++ b/development/states/confirm-new-ui.json
@@ -156,5 +156,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json
index 3c9caafb0..829f513a8 100644
--- a/development/states/confirm-sig-requests.json
+++ b/development/states/confirm-sig-requests.json
@@ -73,7 +73,7 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
},
"status": "unapproved",
- "time": 1537889069339,
+ "time": 1537889070000,
"type": "eth_sign"
}
},
@@ -86,11 +86,11 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
},
"status": "unapproved",
- "time": 1517889069339,
+ "time": 1537889065000,
"type": "personal_sign"
}
},
- "unapprovedPersonalMsgCount": 0,
+ "unapprovedPersonalMsgCount": 1 ,
"unapprovedTypedMessages": {
"8997167822566869": {
"id": 8997167822566869,
@@ -102,7 +102,7 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
},
"status": "unapproved",
- "time": 1617889069339,
+ "time": 1537889060000,
"type": "eth_signTypedData"
}
},
@@ -172,5 +172,32 @@
"scrollToBottom": false,
"forgottenPassword": null
},
- "identities": {}
+ "identities": {},
+ "confirmTransaction": {
+ "txData": {
+ "id": 8927167822566864,
+ "msgParams": {
+ "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0",
+ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
+ },
+ "status": "unapproved",
+ "time": 1537889069339,
+ "type": "eth_sign"
+ },
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json
index 8c8b18a91..6848c0840 100644
--- a/development/states/currency-localization.json
+++ b/development/states/currency-localization.json
@@ -130,5 +130,23 @@
"scrollToBottom": false,
"forgottenPassword": null
},
- "identities": {}
+ "identities": {},
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/first-time.json b/development/states/first-time.json
index f10eefd35..e88ec6d65 100644
--- a/development/states/first-time.json
+++ b/development/states/first-time.json
@@ -58,5 +58,23 @@
}
},
"identities": {},
- "computedBalances": {}
+ "computedBalances": {},
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/send-edit.json b/development/states/send-edit.json
index b05acbf3b..8e5c25a82 100644
--- a/development/states/send-edit.json
+++ b/development/states/send-edit.json
@@ -156,5 +156,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json
index b457749ee..ad2ff3d6e 100644
--- a/development/states/send-new-ui.json
+++ b/development/states/send-new-ui.json
@@ -135,5 +135,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/old-ui/app/keychains/hd/restore-vault.js b/old-ui/app/keychains/hd/restore-vault.js
index d334d8e5f..e9486a28d 100644
--- a/old-ui/app/keychains/hd/restore-vault.js
+++ b/old-ui/app/keychains/hd/restore-vault.js
@@ -42,9 +42,6 @@ RestoreVaultScreen.prototype.render = function () {
// wallet seed entry
h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase.letter-spacey', {
- dataset: {
- persistentFormId: 'wallet-seed',
- },
placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
}),
diff --git a/package-lock.json b/package-lock.json
index d2852f29a..300685fc3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8526,6 +8526,108 @@
"xhr-request-promise": "^0.1.2"
}
},
+ "eth-method-registry": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eth-method-registry/-/eth-method-registry-1.0.0.tgz",
+ "integrity": "sha1-8Ij3Wdad6f3BK3EEm83GiKMoOLY=",
+ "requires": {
+ "ethjs": "^0.3.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.6",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
+ "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU="
+ },
+ "ethjs": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.9.tgz",
+ "integrity": "sha512-gOQzA3tDUjoLpNONSOALJ/rUFtHi5tXl2mholHasF1cvXhoddqi06yU4OJFJu9AGd6n9v9ywzHlYeIKg1t1hdw==",
+ "requires": {
+ "bn.js": "4.11.6",
+ "ethjs-abi": "0.2.1",
+ "ethjs-contract": "0.2.2",
+ "ethjs-filter": "0.1.8",
+ "ethjs-provider-http": "0.1.6",
+ "ethjs-query": "0.3.7",
+ "ethjs-unit": "0.1.6",
+ "ethjs-util": "0.1.3",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ },
+ "ethjs-abi": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz",
+ "integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=",
+ "requires": {
+ "bn.js": "4.11.6",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ },
+ "ethjs-contract": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.2.tgz",
+ "integrity": "sha512-xxPqEjsULQ/QNWuvX6Ako0PGs5RxALA8N/H3+boLvnaXDFZVGpD7H63H1gBCRTZyYqCldPpVlVHuw/rD45vazw==",
+ "requires": {
+ "ethjs-abi": "0.2.0",
+ "ethjs-filter": "0.1.8",
+ "ethjs-util": "0.1.3",
+ "js-sha3": "0.5.5"
+ },
+ "dependencies": {
+ "ethjs-abi": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz",
+ "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=",
+ "requires": {
+ "bn.js": "4.11.6",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ }
+ }
+ },
+ "ethjs-filter": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz",
+ "integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA=="
+ },
+ "ethjs-query": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.7.tgz",
+ "integrity": "sha512-TZnKUwfkWjy0SowFdPLtmsytCorHi0i4vvkQn7Jg8rZt33cRzKhuzOwKr/G3vdigCc+ePXOhUGMcJSAPlOG44A==",
+ "requires": {
+ "ethjs-format": "0.2.7",
+ "ethjs-rpc": "0.2.0",
+ "promise-to-callback": "^1.0.0"
+ }
+ },
+ "ethjs-rpc": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.2.0.tgz",
+ "integrity": "sha512-RINulkNZTKnj4R/cjYYtYMnFFaBcVALzbtEJEONrrka8IeoarNB9Jbzn+2rT00Cv8y/CxAI+GgY1d0/i2iQeOg==",
+ "requires": {
+ "promise-to-callback": "^1.0.0"
+ }
+ },
+ "ethjs-util": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz",
+ "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=",
+ "requires": {
+ "is-hex-prefixed": "1.0.0",
+ "strip-hex-prefix": "1.0.0"
+ }
+ },
+ "js-sha3": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz",
+ "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko="
+ }
+ }
+ },
"eth-phishing-detect": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/eth-phishing-detect/-/eth-phishing-detect-1.1.12.tgz",
@@ -8548,12 +8650,13 @@
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz",
"integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=",
"requires": {
+ "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf",
- "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
+ "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
@@ -26484,6 +26587,15 @@
"deep-diff": "^0.3.5"
}
},
+ "redux-mock-store": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz",
+ "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==",
+ "dev": true,
+ "requires": {
+ "lodash.isplainobject": "^4.0.6"
+ }
+ },
"redux-test-utils": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/redux-test-utils/-/redux-test-utils-0.2.2.tgz",
@@ -26885,6 +26997,11 @@
}
}
},
+ "reselect": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
+ "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+ },
"resolve": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
@@ -30647,6 +30764,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
@@ -31670,6 +31788,7 @@
"resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz",
"integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=",
"requires": {
+ "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"crypto-js": "^3.1.4",
"utf8": "^2.1.1",
"xhr2": "*",
@@ -31678,7 +31797,7 @@
"dependencies": {
"bignumber.js": {
"version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
- "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934"
+ "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git"
}
}
},
@@ -32177,7 +32296,8 @@
"dev": true,
"requires": {
"underscore": "1.8.3",
- "web3-core-helpers": "1.0.0-beta.34"
+ "web3-core-helpers": "1.0.0-beta.34",
+ "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
},
"dependencies": {
"underscore": {
@@ -32188,7 +32308,8 @@
},
"websocket": {
"version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
- "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
+ "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible",
+ "dev": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
@@ -33544,7 +33665,8 @@
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
- "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
+ "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
+ "dev": true
},
"yallist": {
"version": "2.1.2",
diff --git a/package.json b/package.json
index 6a5e63992..a31262174 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0",
+ "eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
@@ -179,6 +180,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"request-promise": "^4.2.1",
+ "reselect": "^3.0.1",
"sandwich-expando": "^1.1.3",
"semaphore": "^1.0.5",
"semver": "^5.4.1",
@@ -285,6 +287,7 @@
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
+ "redux-mock-store": "^1.5.3",
"redux-test-utils": "^0.2.2",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2",
diff --git a/test/e2e/beta/contract-test/contract.js b/test/e2e/beta/contract-test/contract.js
index 18c866f21..8af008dce 100644
--- a/test/e2e/beta/contract-test/contract.js
+++ b/test/e2e/beta/contract-test/contract.js
@@ -32,35 +32,103 @@ var piggybankContract = web3.eth.contract([{'constant': false, 'inputs': [{'name
const deployButton = document.getElementById('deployButton')
const depositButton = document.getElementById('depositButton')
const withdrawButton = document.getElementById('withdrawButton')
+const sendButton = document.getElementById('sendButton')
+const createToken = document.getElementById('createToken')
+const transferTokens = document.getElementById('transferTokens')
+const approveTokens = document.getElementById('approveTokens')
deployButton.addEventListener('click', async function (event) {
+ var piggybank = await piggybankContract.new(
+ {
+ from: web3.eth.accounts[0],
+ data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
+ gas: '4700000',
+ }, function (e, contract) {
+ console.log(e, contract)
+ if (typeof contract.address !== 'undefined') {
+ console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
- var piggybank = await piggybankContract.new(
- {
- from: web3.eth.accounts[0],
- data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
- gas: '4700000',
- }, function (e, contract) {
- console.log(e, contract)
- if (typeof contract.address !== 'undefined') {
- console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
-
- console.log(`contract`, contract)
-
- depositButton.addEventListener('click', function (event) {
- contract.deposit({ from: web3.eth.accounts[0], value: '0x29a2241af62c0000' }, function (result) {
- console.log(result)
- })
+ console.log(`contract`, contract)
+
+ depositButton.addEventListener('click', function (event) {
+ contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) {
+ console.log(result)
+ })
+ })
+
+ withdrawButton.addEventListener('click', function (event) {
+ contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
+ console.log(result)
})
+ })
+ }
+ })
+
+ console.log(piggybank)
+})
+
+sendButton.addEventListener('click', function (event) {
+ web3.eth.sendTransaction({
+ from: web3.eth.accounts[0],
+ to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
+ value: '0x29a2241af62c0000',
+ gas: 21000,
+ gasPrice: 20000000000,
+ }, (result) => {
+ console.log(result)
+ })
+})
+
+
+createToken.addEventListener('click', async function (event) {
+ var _initialAmount = 100
+ var _tokenName = 'TST'
+ var _decimalUnits = 0
+ var _tokenSymbol = 'TST'
+ var humanstandardtokenContract = web3.eth.contract([{'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'approve', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_from', 'type': 'address'}, {'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transferFrom', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{'name': '', 'type': 'uint8'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}, {'name': '_extraData', 'type': 'bytes'}], 'name': 'approveAndCall', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}, {'name': '_spender', 'type': 'address'}], 'name': 'allowance', 'outputs': [{'name': 'remaining', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initialAmount', 'type': 'uint256'}, {'name': '_tokenName', 'type': 'string'}, {'name': '_decimalUnits', 'type': 'uint8'}, {'name': '_tokenSymbol', 'type': 'string'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'payable': false, 'stateMutability': 'nonpayable', 'type': 'fallback'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_from', 'type': 'address'}, {'indexed': true, 'name': '_to', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_owner', 'type': 'address'}, {'indexed': true, 'name': '_spender', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Approval', 'type': 'event'}])
+ return humanstandardtokenContract.new(
+ _initialAmount,
+ _tokenName,
+ _decimalUnits,
+ _tokenSymbol,
+ {
+ from: web3.eth.accounts[0],
+ data: '0x60806040526040805190810160405280600481526020017f48302e3100000000000000000000000000000000000000000000000000000000815250600690805190602001906200005192919062000143565b503480156200005f57600080fd5b50604051620011f3380380620011f383398101806040528101908080519060200190929190805182019291906020018051906020019092919080518201929190505050836000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508360028190555082600390805190602001906200010492919062000143565b5081600460006101000a81548160ff021916908360ff16021790555080600590805190602001906200013892919062000143565b5050505050620001f2565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200018657805160ff1916838001178555620001b7565b82800160010185558215620001b7579182015b82811115620001b657825182559160200191906001019062000199565b5b509050620001c69190620001ca565b5090565b620001ef91905b80821115620001eb576000816000905550600101620001d1565b5090565b90565b610ff180620002026000396000f3006080604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100c1578063095ea7b31461015157806318160ddd146101b657806323b872dd146101e1578063313ce5671461026657806354fd4d501461029757806370a082311461032757806395d89b411461037e578063a9059cbb1461040e578063cae9ca5114610473578063dd62ed3e1461051e575b3480156100bb57600080fd5b50600080fd5b3480156100cd57600080fd5b506100d6610595565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101165780820151818401526020810190506100fb565b50505050905090810190601f1680156101435780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015d57600080fd5b5061019c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610633565b604051808215151515815260200191505060405180910390f35b3480156101c257600080fd5b506101cb610725565b6040518082815260200191505060405180910390f35b3480156101ed57600080fd5b5061024c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061072b565b604051808215151515815260200191505060405180910390f35b34801561027257600080fd5b5061027b6109a4565b604051808260ff1660ff16815260200191505060405180910390f35b3480156102a357600080fd5b506102ac6109b7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102ec5780820151818401526020810190506102d1565b50505050905090810190601f1680156103195780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561033357600080fd5b50610368600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610a55565b6040518082815260200191505060405180910390f35b34801561038a57600080fd5b50610393610a9d565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103d35780820151818401526020810190506103b8565b50505050905090810190601f1680156104005780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561041a57600080fd5b50610459600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610b3b565b604051808215151515815260200191505060405180910390f35b34801561047f57600080fd5b50610504600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509192919290505050610ca1565b604051808215151515815260200191505060405180910390f35b34801561052a57600080fd5b5061057f600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610f3e565b6040518082815260200191505060405180910390f35b60038054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561062b5780601f106106005761010080835404028352916020019161062b565b820191906000526020600020905b81548152906001019060200180831161060e57829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60025481565b6000816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101580156107f7575081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410155b80156108035750600082115b1561099857816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a36001905061099d565b600090505b9392505050565b600460009054906101000a900460ff1681565b60068054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610a4d5780601f10610a2257610100808354040283529160200191610a4d565b820191906000526020600020905b815481529060010190602001808311610a3057829003601f168201915b505050505081565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60058054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610b335780601f10610b0857610100808354040283529160200191610b33565b820191906000526020600020905b815481529060010190602001808311610b1657829003601f168201915b505050505081565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610b8b5750600082115b15610c9657816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610c9b565b600090505b92915050565b600082600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a38373ffffffffffffffffffffffffffffffffffffffff1660405180807f72656365697665417070726f76616c28616464726573732c75696e743235362c81526020017f616464726573732c627974657329000000000000000000000000000000000000815250602e01905060405180910390207c01000000000000000000000000000000000000000000000000000000009004338530866040518563ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018481526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828051906020019080838360005b83811015610ee2578082015181840152602081019050610ec7565b50505050905090810190601f168015610f0f5780820380516001836020036101000a031916815260200191505b509450505050506000604051808303816000875af1925050501515610f3357600080fd5b600190509392505050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050929150505600a165627a7a723058200052919e0bc22b5adcd3d320be977df3a1dcc35d1a0160287383ba371900a1c50029',
+ gas: '4700000',
+ gasPrice: '20000000000',
+ }, function (e, contract) {
+ console.log(e, contract)
+ if (typeof contract.address !== 'undefined') {
+ console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
+
+ document.getElementById('tokenAddress').innerHTML = contract.address
- withdrawButton.addEventListener('click', function (event) {
- contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
- console.log(result)
- })
+ transferTokens.addEventListener('click', function (event) {
+ console.log(`event`, event)
+ contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', {
+ from: web3.eth.accounts[0],
+ to: contract.address,
+ data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
+ gas: 60000,
+ gasPrice: '20000000000',
+ }, function (result) {
+ console.log('result', result)
})
- }
- })
+ })
- console.log(piggybank)
+ approveTokens.addEventListener('click', function (event) {
+ contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', {
+ from: web3.eth.accounts[0],
+ to: contract.address,
+ data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005',
+ gas: 60000,
+ gasPrice: '20000000000',
+ }, function (result) {
+ console.log(result)
+ })
+ })
+ }
+ })
})
+
diff --git a/test/e2e/beta/contract-test/index.html b/test/e2e/beta/contract-test/index.html
index 0868633f7..0d63fd940 100644
--- a/test/e2e/beta/contract-test/index.html
+++ b/test/e2e/beta/contract-test/index.html
@@ -1,8 +1,33 @@
<html>
+<head>
+ <title>E2E Test Dapp</title>
+</head>
<body>
- <button id="deployButton">Deploy Contract</button>
- <button id="depositButton">Deposit</button>
- <button id="withdrawButton">Withdraw</button>
-</body>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Contract</div>
+ <div style="display: flex;">
+ <button id="deployButton">Deploy Contract</button>
+ <button id="depositButton">Deposit</button>
+ <button id="withdrawButton">Withdraw</button>
+ </div>
+ </div>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Send eth</div>
+ <div style="display: flex;">
+ <button id="sendButton">Send</button>
+ </div>
+ </div>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Send tokens</div>
+ <div id="tokenAddress"></div>
+ <div style="display: flex;">
+ <button id="createToken">Create Token</button>
+ <button id="transferTokens">Transfer Tokens</button>
+ <button id="approveTokens">Approve Tokens</button>
+ </div>
+ </div>
+
<script src="contract.js"></script>
+</body>
+
</html> \ No newline at end of file
diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js
index fcc3e96d6..828f87db7 100644
--- a/test/e2e/beta/helpers.js
+++ b/test/e2e/beta/helpers.js
@@ -1,16 +1,21 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const pify = require('pify')
+const assert = require('assert')
const {until} = require('selenium-webdriver')
const { delay } = require('../func')
module.exports = {
+ assertElementNotPresent,
checkBrowserForConsoleErrors,
- loadExtension,
- verboseReportOnFailure,
+ closeAllWindowHandlesExcept,
findElement,
findElements,
+ loadExtension,
openNewPage,
+ switchToWindowWithTitle,
+ verboseReportOnFailure,
+ waitUntilXWindowHandles,
}
async function loadExtension (driver, extensionId) {
@@ -72,9 +77,57 @@ async function openNewPage (driver, url) {
await delay(1000)
const handles = await driver.getAllWindowHandles()
- const secondHandle = handles[1]
- await driver.switchTo().window(secondHandle)
+ const lastHandle = handles[handles.length - 1]
+ await driver.switchTo().window(lastHandle)
await driver.get(url)
await delay(1000)
}
+
+async function waitUntilXWindowHandles (driver, x) {
+ const windowHandles = await driver.getAllWindowHandles()
+ if (windowHandles.length === x) return
+ await delay(1000)
+ return await waitUntilXWindowHandles(driver, x)
+}
+
+async function switchToWindowWithTitle (driver, title, windowHandles) {
+ if (!windowHandles) {
+ windowHandles = await driver.getAllWindowHandles()
+ } else if (windowHandles.length === 0) {
+ throw new Error('No window with title: ' + title)
+ }
+ const firstHandle = windowHandles[0]
+ await driver.switchTo().window(firstHandle)
+ const handleTitle = await driver.getTitle()
+
+ if (handleTitle === title) {
+ return firstHandle
+ } else {
+ return await switchToWindowWithTitle(driver, title, windowHandles.slice(1))
+ }
+}
+
+async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) {
+ exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions
+ windowHandles = windowHandles || await driver.getAllWindowHandles()
+ const lastWindowHandle = windowHandles.pop()
+ if (!exceptions.includes(lastWindowHandle)) {
+ await driver.switchTo().window(lastWindowHandle)
+ await delay(1000)
+ await driver.close()
+ await delay(1000)
+ }
+ return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles)
+}
+
+async function assertElementNotPresent (webdriver, driver, by) {
+ try {
+ const dataTab = await findElement(driver, by, 4000)
+ if (dataTab) {
+ assert(false, 'Data tab should not be present')
+ }
+ } catch (err) {
+ assert(err instanceof webdriver.error.NoSuchElementError)
+ }
+}
diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js
index 5f270b52b..caa49d450 100644
--- a/test/e2e/beta/metamask-beta-ui.spec.js
+++ b/test/e2e/beta/metamask-beta-ui.spec.js
@@ -11,12 +11,16 @@ const {
getExtensionIdFirefox,
} = require('../func')
const {
+ assertElementNotPresent,
+ checkBrowserForConsoleErrors,
+ closeAllWindowHandlesExcept,
findElement,
findElements,
- checkBrowserForConsoleErrors,
loadExtension,
- verboseReportOnFailure,
openNewPage,
+ switchToWindowWithTitle,
+ verboseReportOnFailure,
+ waitUntilXWindowHandles,
} = require('./helpers')
describe('MetaMask', function () {
@@ -25,7 +29,7 @@ describe('MetaMask', function () {
let tokenAddress
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
- const tinyDelayMs = 1000
+ const tinyDelayMs = 200
const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2
@@ -80,20 +84,29 @@ describe('MetaMask', function () {
networkSelector = await findElement(driver, By.css('#network_component'))
} catch (e) {
await loadExtension(driver, extensionId)
+ await delay(largeDelayMs * 2)
+ networkSelector = await findElement(driver, By.css('#network_component'))
}
await delay(regularDelayMs)
})
- it('use the local network', async function () {
+ it('uses the local network', async function () {
await networkSelector.click()
await delay(regularDelayMs)
- const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
+ const networks = await findElements(driver, By.css('.dropdown-menu-item'))
+ const localhost = networks[4]
+ await driver.wait(until.elementTextMatches(localhost, /Localhost/))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
+ try {
+ const overlay = await findElement(driver, By.css('.full-flex-height'))
+ await driver.wait(until.stalenessOf(overlay))
+ } catch (e) {}
+
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click()
await delay(regularDelayMs)
@@ -188,8 +201,20 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
- async function retypeSeedPhrase (words) {
+ async function retypeSeedPhrase (words, wasReloaded) {
try {
+ if (wasReloaded) {
+ const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
+ await driver.wait(until.elementLocated(byRevealButton, 10000))
+ const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
+ await revealSeedPhraseButton.click()
+ await delay(regularDelayMs)
+
+ const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
+ await nextScreen.click()
+ await delay(regularDelayMs)
+ }
+
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
await word0.click()
@@ -250,7 +275,7 @@ describe('MetaMask', function () {
await delay(tinyDelayMs)
} catch (e) {
await loadExtension(driver, extensionId)
- await retypeSeedPhrase(words)
+ await retypeSeedPhrase(words, true)
}
}
@@ -303,7 +328,7 @@ describe('MetaMask', function () {
it('accepts the account password after lock', async () => {
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple')
await driver.findElement(By.id('password')).sendKeys(Key.ENTER)
- await delay(regularDelayMs * 4)
+ await delay(largeDelayMs * 4)
})
})
@@ -324,10 +349,10 @@ describe('MetaMask', function () {
const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`))
await create.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
})
- it('should correct account name', async () => {
+ it('should display correct account name', async () => {
const accountName = await findElement(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs)
@@ -355,16 +380,18 @@ describe('MetaMask', function () {
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
- await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple')
- await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple')
- await driver.findElement(By.css('button:nth-child(2)')).click()
+ const passwordInputs = await driver.findElements(By.css('input'))
+ await delay(regularDelayMs)
+
+ passwordInputs[0].sendKeys('correct horse battery staple')
+ passwordInputs[1].sendKeys('correct horse battery staple')
+ await driver.findElement(By.css('.first-time-flow__button')).click()
await delay(regularDelayMs)
})
it('balance renders', async () => {
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
- const tokenAmount = await balance.getText()
- assert.equal(tokenAmount, '100.000 ETH')
+ await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
await delay(regularDelayMs)
})
})
@@ -380,6 +407,9 @@ describe('MetaMask', function () {
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
+ const inputValue = await inputAmount.getAttribute('value')
+ assert.equal(inputValue, '1')
+
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
@@ -401,59 +431,71 @@ describe('MetaMask', function () {
it('confirms the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1)
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
+ if (process.env.SELENIUM_BROWSER !== 'firefox') {
+ const txValues = await findElement(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
+ }
})
})
- describe('Send ETH from Faucet', () => {
- it('starts a send transaction inside Faucet', async () => {
- await openNewPage(driver, 'https://faucet.metamask.io')
+ describe('Send ETH from dapp', () => {
+ it('starts a send transaction inside the dapp', async () => {
+ await openNewPage(driver, 'http://127.0.0.1:8080/')
+ await delay(regularDelayMs)
- const [extension, faucet] = await driver.getAllWindowHandles()
- await driver.switchTo().window(faucet)
+ await waitUntilXWindowHandles(driver, 2)
+ let windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = windowHandles[1]
- const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
- await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
- const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
- await send1eth.click()
+ const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
+ await send3eth.click()
await delay(regularDelayMs)
- await driver.switchTo().window(extension)
- await loadExtension(driver, extensionId)
+ windowHandles = await driver.getAllWindowHandles()
+ await driver.switchTo().window(windowHandles[2])
await delay(regularDelayMs)
- const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
+ assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
+
+ const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
await confirmButton.click()
await delay(regularDelayMs)
- await driver.switchTo().window(faucet)
- await delay(regularDelayMs)
- await driver.close()
- await delay(regularDelayMs)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- await loadExtension(driver, extensionId)
- await delay(regularDelayMs)
+ })
+
+ it('finds the transaction in the transactions list', async function () {
+ const transactions = await findElements(driver, By.css('.tx-list-item'))
+ assert.equal(transactions.length, 2)
+
+ const txValues = await findElement(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
})
})
describe('Deploy contract and call contract methods', () => {
let extension
- let contractTestPage
- it('confirms a deploy contract transaction', async () => {
- await openNewPage(driver, 'http://127.0.0.1:8080/');
+ let dapp
+ it('creates a deploy contract transaction', async () => {
+ const windowHandles = await driver.getAllWindowHandles()
+ extension = windowHandles[0]
+ dapp = windowHandles[1]
+ await delay(tinyDelayMs)
- [extension, contractTestPage] = await driver.getAllWindowHandles()
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const deployContractButton = await findElement(driver, By.css('#deployButton'))
@@ -463,10 +505,28 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const txListItem = await findElement(driver, By.css('.tx-list-item'))
+ const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
await txListItem.click()
await delay(regularDelayMs)
+ })
+
+ it('displays the contract creation data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await (regularDelayMs)
+
+ await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await (regularDelayMs)
+ })
+
+ it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
@@ -476,10 +536,11 @@ describe('MetaMask', function () {
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
+ await delay(regularDelayMs)
})
it('calls and confirms a contract method where ETH is sent', async () => {
- await driver.switchTo().window(contractTestPage)
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const depositButton = await findElement(driver, By.css('#depositButton'))
@@ -489,17 +550,19 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const txListItem = await findElement(driver, By.css('.tx-list-item'))
- await txListItem.click()
+ await findElements(driver, By.css('.tx-list-pending-item-container'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
+ await txListValue.click()
await delay(regularDelayMs)
// Set the gas limit
- const configureGas = await findElement(driver, By.css('.sliders-icon-container'))
+ const configureGas = await findElement(driver, By.css('.confirm-detail-row__header-text--edit'))
await configureGas.click()
await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
- await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
@@ -521,7 +584,7 @@ describe('MetaMask', function () {
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
+ await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText()
@@ -529,7 +592,7 @@ describe('MetaMask', function () {
})
it('calls and confirms a contract method where ETH is received', async () => {
- await driver.switchTo().window(contractTestPage)
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const withdrawButton = await findElement(driver, By.css('#withdrawButton'))
@@ -553,38 +616,31 @@ describe('MetaMask', function () {
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
- await driver.switchTo().window(contractTestPage)
- await driver.close()
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
- await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
- const tokenAmount = await balance.getText()
- assert.ok(/^86.*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
+ if (process.env.SELENIUM_BROWSER !== 'firefox') {
+ await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
+ const tokenAmount = await balance.getText()
+ assert.ok(/^92.*ETH.*$/.test(tokenAmount))
+ await delay(regularDelayMs)
+ }
})
})
- describe('Add a custom token from TokenFactory', () => {
+ describe('Add a custom token from a dapp', () => {
it('creates a new token', async () => {
- openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = windowHandles[1]
+ await delay(regularDelayMs * 2)
- await delay(regularDelayMs * 10)
- const [extension, tokenFactory] = await driver.getAllWindowHandles()
-
- const [
- totalSupply,
- tokenName,
- tokenDecimal,
- tokenSymbol,
- ] = await findElements(driver, By.css('.form-control'))
-
- await totalSupply.sendKeys('100')
- await tokenName.sendKeys('Test')
- await tokenDecimal.sendKeys('0')
- await tokenSymbol.sendKeys('TST')
+ await driver.switchTo().window(dapp)
+ await delay(regularDelayMs)
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
@@ -598,15 +654,16 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
- await driver.switchTo().window(tokenFactory)
- await delay(regularDelayMs)
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
- const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
- tokenAddress = await tokenContactAddress.getText()
+ const tokenContractAddress = await driver.findElement(By.css('#tokenAddress'))
+ await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/))
+ tokenAddress = await tokenContractAddress.getText()
- await driver.close()
- await driver.switchTo().window(extension)
- await loadExtension(driver, extensionId)
+ await delay(regularDelayMs)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
@@ -665,7 +722,7 @@ describe('MetaMask', function () {
gasModal = await driver.findElement(By.css('span .modal'))
})
- it('customizes gas', async () => {
+ it('opens customizes gas modal', async () => {
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
@@ -681,6 +738,24 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
+ it('displays the token transfer data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await (regularDelayMs)
+
+ const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
+ const functionTypeText = await functionType.getText()
+ assert.equal(functionTypeText, 'Transfer')
+
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await (regularDelayMs)
+ })
+
it('submits the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
@@ -706,37 +781,33 @@ describe('MetaMask', function () {
})
})
- describe('Send a custom token from TokenFactory', () => {
+ describe('Send a custom token from dapp', () => {
let gasModal
it('sends an already created token', async () => {
- openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`)
-
- const [extension] = await driver.getAllWindowHandles()
-
- const [
- transferToAddress,
- transferToAmount,
- ] = await findElements(driver, By.css('.form-control'))
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
- await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
- await transferToAmount.sendKeys('26')
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
- const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
- await transferAmountButton.click()
- await delay(regularDelayMs)
+ const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`))
+ await transferTokens.click()
- const [,, popup] = await driver.getAllWindowHandles()
- await driver.switchTo().window(popup)
- await driver.close()
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
- const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
- await txListItem.click()
+ await findElements(driver, By.css('.tx-list-pending-item-container'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
+ await txListValue.click()
await delay(regularDelayMs)
// Set the gas limit
- const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button')))
+ const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')), 10000)
await configureGas.click()
await delay(regularDelayMs)
@@ -744,7 +815,7 @@ describe('MetaMask', function () {
})
it('customizes gas', async () => {
- await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
@@ -763,12 +834,12 @@ describe('MetaMask', function () {
await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
- const save = await findElement(driver, By.css('.send-v2__customize-gas__save'))
+ const save = await findElement(driver, By.css('.customize-gas__save'))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
- const gasFeeInput = await findElement(driver, By.css('.currency-display__input'))
- assert.equal(await gasFeeInput.getAttribute('value'), 0.0006)
+ const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
+ assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
})
it('submits the transaction', async function () {
@@ -782,7 +853,7 @@ describe('MetaMask', function () {
assert.equal(transactions.length, 2)
const txValues = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/))
+ await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
@@ -796,8 +867,107 @@ describe('MetaMask', function () {
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
- assert.equal(await tokenBalanceAmount.getText(), '24')
+ assert.equal(await tokenBalanceAmount.getText(), '43')
+ }
+ })
+ })
+
+ describe('Approves a custom token from dapp', () => {
+ let gasModal
+ it('approves an already created token', async () => {
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
+
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
+
+ const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
+ await transferTokens.click()
+
+ await closeAllWindowHandlesExcept(driver, extension)
+ await driver.switchTo().window(extension)
+ await delay(regularDelayMs)
+
+ const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
+ await txListItem.click()
+ await delay(regularDelayMs)
+ })
+
+ it('displays the token approval data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await (regularDelayMs)
+
+ const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
+ const functionTypeText = await functionType.getText()
+ assert.equal(functionTypeText, 'Approve')
+
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await (regularDelayMs)
+
+ const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning'))
+ const approvalWarningText = await approvalWarning.getText()
+ assert(approvalWarningText.match(/By approving this/))
+ await (regularDelayMs)
+ })
+
+ it('opens the gas edit modal', async () => {
+ const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')))
+ await configureGas.click()
+ await delay(regularDelayMs)
+
+ gasModal = await driver.findElement(By.css('span .modal'))
+ })
+
+ it('customizes gas', async () => {
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
+
+ const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
+ await gasPriceInput.clear()
+ await delay(tinyDelayMs)
+ await gasPriceInput.sendKeys('10')
+ await delay(tinyDelayMs)
+ await gasLimitInput.clear()
+ await delay(tinyDelayMs)
+ await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
+ await gasLimitInput.sendKeys('60000')
+ await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
+
+ // Needed for different behaviour of input in different versions of firefox
+ const gasLimitInputValue = await gasLimitInput.getAttribute('value')
+ if (gasLimitInputValue === '600001') {
+ await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
+
+ const save = await findElement(driver, By.css('.customize-gas__save'))
+ await save.click()
+ await driver.wait(until.stalenessOf(gasModal))
+
+ const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
+ assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
+ })
+
+ it('submits the transaction', async function () {
+ const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
+ await confirmButton.click()
+ await delay(regularDelayMs)
+ })
+
+ it('finds the transaction in the transactions list', async function () {
+ const txValues = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
+ const txStatuses = await findElements(driver, By.css('.tx-list-status'))
+ await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
})
})
diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js
index a32b924b8..b6efae5b3 100644
--- a/test/e2e/metamask.spec.js
+++ b/test/e2e/metamask.spec.js
@@ -234,7 +234,7 @@ describe('Metamask popup page', function () {
submitButton.click()
- await delay(500)
+ await delay(1500)
})
it('finds the transaction in the transactions list', async function () {
diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js
index 5613c0dcb..dcc25c493 100644
--- a/test/integration/lib/confirm-sig-requests.js
+++ b/test/integration/lib/confirm-sig-requests.js
@@ -1,5 +1,6 @@
const reactTriggerChange = require('react-trigger-change')
const {
+ timeout,
queryAsync,
} = require('../../lib/util')
@@ -18,14 +19,14 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
- // await timeout(1000000)
-
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()
}
+ await timeout(1000)
+
let confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -37,7 +38,7 @@ async function runConfirmSigRequestsTest (assert, done) {
let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
-
+ await timeout(1000)
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -46,7 +47,7 @@ async function runConfirmSigRequestsTest (assert, done) {
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
-
+ await timeout(1000)
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -57,6 +58,5 @@ async function runConfirmSigRequestsTest (assert, done) {
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
- const txView = await queryAsync($, '.tx-view')
- assert.ok(txView[0], 'Should return to the account details screen after confirming')
+ await timeout(2000)
}
diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js
index 3ad1a23e5..d42b7495d 100644
--- a/test/integration/lib/currency-localization.js
+++ b/test/integration/lib/currency-localization.js
@@ -19,6 +19,7 @@ async function runCurrencyLocalizationTest (assert, done) {
console.log('*** start runCurrencyLocalizationTest')
const selectState = await queryAsync($, 'select')
selectState.val('currency localization')
+ await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)
const txView = await queryAsync($, '.tx-view')
diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js
index 241358135..406863ca6 100644
--- a/test/integration/lib/send-new-ui.js
+++ b/test/integration/lib/send-new-ui.js
@@ -112,19 +112,8 @@ async function runSendFlowTest (assert, done) {
errorMessage = $('.send-v2__error')
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected')
- const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
- assert.equal(
- sendGasField.find('.currency-display__input-wrapper > input').val(),
- '0.000021',
- 'send gas field should show estimated gas total'
- )
- assert.equal(
- sendGasField.find('.currency-display__converted-value')[0].textContent,
- '$0.03 USD',
- 'send gas field should show estimated gas total converted to USD'
- )
-
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
+ await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
@@ -136,18 +125,18 @@ async function runSendFlowTest (assert, done) {
reactTriggerChange(selectState[0])
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
- assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name')
+ assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
- const confirmScreenRows = await queryAsync($, '.confirm-screen-rows')
- const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0]
- assert.equal(confirmScreenGas.textContent, '$3.60 USD', 'confirm screen should show correct gas')
- const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2]
- assert.equal(confirmScreenTotal.textContent, '$2,405.36 USD', 'confirm screen should show correct total')
+ const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
+ const confirmScreenGas = confirmScreenRowFiats[0]
+ assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas')
+ const confirmScreenTotal = confirmScreenRowFiats[1]
+ assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total')
- const confirmScreenBackButton = await queryAsync($, '.page-container__back-button')
+ const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button')
confirmScreenBackButton[0].click()
const sendFromFieldItemInEdit = await queryAsync($, '.account-list-item')
diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js
index c110f71fc..160cd4552 100644
--- a/test/unit/actions/tx_test.js
+++ b/test/unit/actions/tx_test.js
@@ -1,74 +1,54 @@
-// var jsdom = require('mocha-jsdom')
var assert = require('assert')
-var freeze = require('deep-freeze-strict')
var path = require('path')
-var sinon = require('sinon')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
-describe('tx confirmation screen', function () {
- beforeEach(function () {
- this.sinon = sinon.createSandbox()
- })
-
- afterEach(function () {
- this.sinon.restore()
- })
+const actions = require(path.join(__dirname, '../../../ui/app/actions.js'))
- var initialState, result
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
- describe('when there is only one tx', function () {
- var firstTxId = 1457634084250832
-
- beforeEach(function () {
- initialState = {
- appState: {
- currentView: {
- name: 'confTx',
- },
- },
- metamask: {
- unapprovedTxs: {
- '1457634084250832': {
- id: 1457634084250832,
- status: 'unconfirmed',
- time: 1457634084250,
- },
- },
+describe('tx confirmation screen', function () {
+ const txId = 1457634084250832
+ const initialState = {
+ appState: {
+ currentView: {
+ name: 'confTx',
+ },
+ },
+ metamask: {
+ unapprovedTxs: {
+ [txId]: {
+ id: txId,
+ status: 'unconfirmed',
+ time: 1457634084250,
},
- }
- freeze(initialState)
+ },
+ },
+ }
+
+ const store = mockStore(initialState)
+
+ describe('cancelTx', function () {
+ before(function (done) {
+ actions._setBackgroundConnection({
+ approveTransaction (txId, cb) { cb('An error!') },
+ cancelTransaction (txId, cb) { cb() },
+ clearSeedWordCache (cb) { cb() },
+ getState (cb) { cb() },
+ })
+ done()
})
- describe('cancelTx', function () {
- before(function (done) {
- actions._setBackgroundConnection({
- approveTransaction (txId, cb) { cb('An error!') },
- cancelTransaction (txId, cb) { cb() },
- clearSeedWordCache (cb) { cb() },
+ it('creates COMPLETED_TX with the cancelled transaction ID', function (done) {
+ store.dispatch(actions.cancelTx({ id: txId }))
+ .then(() => {
+ const storeActions = store.getActions()
+ const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX)
+ assert.equal(completedTxAction.value, txId)
+ done()
})
-
- actions.cancelTx({value: firstTxId})((action) => {
- result = reducers(initialState, action)
- })
- done()
- })
-
- it('should transition to the account detail view', function () {
- assert.equal(result.appState.currentView.name, 'accountDetail')
- })
-
- it('should have no unconfirmed txs remaining', function () {
- var count = getUnconfirmedTxCount(result)
- assert.equal(count, 0)
- })
})
})
})
-
-function getUnconfirmedTxCount (state) {
- var txs = state.metamask.unapprovedTxs
- var count = Object.keys(txs).length
- return count
-}
diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js
index d4c5ff0ec..26dc7b656 100644
--- a/test/unit/app/controllers/transactions/tx-controller-test.js
+++ b/test/unit/app/controllers/transactions/tx-controller-test.js
@@ -353,9 +353,16 @@ describe('Transaction Controller', function () {
])
})
- it('should set the transaction to rejected from unapproved', async function () {
- await txController.cancelTransaction(0)
- assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
+ it('should emit a status change to rejected', function (done) {
+ txController.once('tx:status-update', (txId, status) => {
+ try {
+ assert.equal(status, 'rejected', 'status should e rejected')
+ assert.equal(txId, 0, 'id should e 0')
+ done()
+ } catch (e) { done(e) }
+ })
+
+ txController.cancelTransaction(0)
})
})
diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js
index 2f91b1545..88bdaa60e 100644
--- a/test/unit/app/controllers/transactions/tx-state-manager-test.js
+++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js
@@ -43,14 +43,13 @@ describe('TransactionStateManager', function () {
})
describe('#setTxStatusRejected', function () {
- it('sets the tx status to rejected', function () {
+ it('sets the tx status to rejected and removes it from history', function () {
const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
txStateManager.setTxStatusRejected(1)
const result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
- assert.equal(result.length, 1)
- assert.equal(result[0].status, 'rejected')
+ assert.equal(result.length, 0)
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
@@ -287,4 +286,18 @@ describe('TransactionStateManager', function () {
})
})
+
+ describe('#_removeTx', function () {
+ it('should remove the transaction from the storage', () => {
+ txStateManager._saveTxList([ {id: 1} ])
+ txStateManager._removeTx(1)
+ assert(!txStateManager.getFullTxList().length, 'txList should be empty')
+ })
+
+ it('should only remove the transaction with ID 1 from the storage', () => {
+ txStateManager._saveTxList([ {id: 1}, {id: 2} ])
+ txStateManager._removeTx(1)
+ assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2')
+ })
+ })
})
diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js
new file mode 100644
index 000000000..3ec9f0c0e
--- /dev/null
+++ b/test/unit/migrations/027-test.js
@@ -0,0 +1,50 @@
+const assert = require('assert')
+const migration27 = require('../../../app/scripts/migrations/027')
+
+const oldStorage = {
+ 'meta': {},
+ 'data': {
+ 'TransactionController': {
+ 'transactions': [
+ ],
+ },
+ },
+}
+
+const transactions = []
+
+
+while (transactions.length < 9) {
+ transactions.push({status: 'rejected'})
+ transactions.push({status: 'unapproved'})
+ transactions.push({status: 'approved'})
+}
+
+
+oldStorage.data.TransactionController.transactions = transactions
+
+describe('migration #27', () => {
+ it('should remove rejected transactions', (done) => {
+ migration27.migrate(oldStorage)
+ .then((newStorage) => {
+ const newTransactions = newStorage.data.TransactionController.transactions
+ assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6')
+ newTransactions.forEach((txMeta) => {
+ if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected'))
+ })
+ done()
+ })
+ .catch(done)
+ })
+
+ it('should successfully migrate first time state', (done) => {
+ migration27.migrate({
+ meta: {},
+ data: require('../../../app/scripts/first-time-state'),
+ })
+ .then((migratedData) => {
+ assert.equal(migratedData.meta.version, migration27.version)
+ done()
+ }).catch(done)
+ })
+})
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 41fc3c504..1fb49c920 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -6,7 +6,6 @@ const {
calcGasTotal,
calcTokenBalance,
estimateGas,
- estimateGasPriceFromRecentBlocks,
} = require('./components/send_/send.utils')
const ethUtil = require('ethereumjs-util')
const { fetchLocale } = require('../i18n-helper')
@@ -705,11 +704,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({}))
}
@@ -746,19 +744,26 @@ function updateGasData ({
}) {
return (dispatch) => {
dispatch(actions.gasLoadingStarted())
- const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
- return Promise.all([
- Promise.resolve(estimatedGasPrice),
- estimateGas({
- estimateGasMethod: background.estimateGas,
- blockGasLimit,
- selectedAddress,
- selectedToken,
- to,
- value,
- gasPrice: estimatedGasPrice,
- }),
- ])
+ 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))
@@ -904,29 +909,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 => {
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())
@@ -937,10 +954,17 @@ 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())
+ return txData
+ })
}
}
@@ -1032,13 +1056,25 @@ function cancelTypedMsg (msgData) {
function cancelTx (txData) {
return dispatch => {
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())
+ return txData
})
- })
}
}
diff --git a/ui/app/app.js b/ui/app/app.js
index d0e48a368..74d360d3c 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -12,7 +12,7 @@ const log = require('loglevel')
const InitializeScreen = require('../../mascara/src/app/first-time').default
// accounts
const SendTransactionScreen = require('./components/send_/send.container')
-const ConfirmTxScreen = require('./conf-tx')
+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')
@@ -40,6 +39,8 @@ const Modal = require('./components/modals/index').Modal
const AppHeader = require('./components/app-header')
+import UnlockPage from './components/pages/unlock-page'
+
// Routes
const {
DEFAULT_ROUTE,
@@ -76,7 +77,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 }),
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/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..631cf5803
--- /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,
+ fiatFee,
+ ethFee,
+ onHeaderClick,
+ fiatFeeColor,
+ 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: fiatFeeColor }}
+ >
+ { fiatFee }
+ </div>
+ <div className="confirm-detail-row__eth">
+ { `\u2666 ${ethFee}` }
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmDetailRow.propTypes = {
+ label: PropTypes.string,
+ fiatFee: PropTypes.string,
+ ethFee: PropTypes.string,
+ fiatFeeColor: 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..84d0d56ed
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
@@ -0,0 +1,43 @@
+.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;
+ }
+
+ &__fiat {
+ font-size: 1.5rem;
+ }
+
+ &__eth {
+ color: $oslo-gray;
+ }
+
+ &__header-text {
+ font-size: .75rem;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ color: $scorpion;
+
+ &--edit {
+ color: $curious-blue;
+ cursor: pointer;
+ }
+
+ &--total {
+ font-size: .625rem;
+ }
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
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..93e4ae7bf
--- /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,
+ valid: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ showEdit,
+ onEdit,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ valid,
+ 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={!valid}
+ />
+ </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/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/index.scss b/ui/app/components/index.scss
index 351640f6e..32f0e90e4 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -4,6 +4,16 @@
@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/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
new file mode 100644
index 000000000..d17c290b6
--- /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..160911c10 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%;
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 85e85597a..973438b6b 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -24,6 +24,8 @@ 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',
@@ -267,7 +269,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..e82d0e70c
--- /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, 45%);
+ }
+
+ &--ropsten {
+ background-color: lighten($crimson, 45%);
+ }
+
+ &--kovan {
+ background-color: lighten($purple, 45%);
+ }
+
+ &--rinkeby {
+ background-color: lighten($tulip-tree, 45%);
+ }
+ }
+
+ &__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..d775b0362
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+
+export default class ConfirmApprove extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ tokenAmount: PropTypes.string,
+ tokenSymbol: PropTypes.string,
+ }
+
+ render () {
+ const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={`${tokenAmount} ${tokenSymbol}`}
+ warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
+ hideSubtitle
+ />
+ )
+ }
+}
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..040e499ae
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux'
+import ConfirmApprove from './confirm-approve.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction } = state
+ const {
+ tokenData = {},
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ tokenProps: { tokenSymbol } = {},
+ } = confirmTransaction
+ const { params = [] } = tokenData
+
+ let toAddress = ''
+ let tokenAmount = ''
+
+ if (params && params.length === 2) {
+ [{ value: toAddress }, { value: tokenAmount }] = params
+ }
+
+ return {
+ toAddress,
+ tokenAddress,
+ 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..46ad9ccab
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.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 ConfirmSendToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ numberOfTokens: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ editTransaction: PropTypes.func,
+ }
+
+ handleEdit (confirmTransactionData) {
+ const { editTransaction, history } = this.props
+ editTransaction(confirmTransactionData)
+ history.push(SEND_ROUTE)
+ }
+
+ render () {
+ const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={`${numberOfTokens} ${tokenSymbol}`}
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ hideSubtitle
+ />
+ )
+ }
+}
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..2d7efeed6
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
@@ -0,0 +1,72 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import ConfirmSendToken from './confirm-send-token.component'
+import { calcTokenAmount } from '../../../token-util'
+import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
+import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
+import { conversionUtil } from '../../../conversion-util'
+
+const mapStateToProps = state => {
+ const { confirmTransaction } = state
+ const {
+ tokenData = {},
+ tokenProps: { tokenSymbol, tokenDecimals } = {},
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ } = confirmTransaction
+ const { params = [] } = tokenData
+
+ let toAddress = ''
+ let tokenAmount = ''
+
+ if (params && params.length === 2) {
+ [{ value: toAddress }, { value: tokenAmount }] = params
+ }
+
+ const numberOfTokens = tokenAmount && tokenDecimals
+ ? calcTokenAmount(tokenAmount, tokenDecimals)
+ : 0
+
+ return {
+ toAddress,
+ tokenAddress,
+ tokenSymbol,
+ numberOfTokens,
+ }
+}
+
+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-send-token/index.scss b/ui/app/components/pages/confirm-send-token/index.scss
new file mode 100644
index 000000000..0476749f6
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/index.scss
@@ -0,0 +1,19 @@
+.confirm-send-token {
+ &__title {
+ padding: 4px 0;
+ display: flex;
+ align-items: center;
+ }
+
+ &__identicon {
+ flex: 0 0 auto;
+ }
+
+ &__title-text {
+ font-size: 2.25rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-left: 8px;
+ }
+}
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..842b34d2e
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,320 @@
+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,
+ 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,
+ }
+
+ 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,
+ hideDetails,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)}
+ ethFee={ethTransactionFee}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ />
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)}
+ ethFee={ethTransactionTotal}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ fiatFeeColor="#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 }
+ </span>
+ </div>
+ <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
+
+ if (onSubmit) {
+ onSubmit(txData)
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ ethTransactionAmount,
+ fiatTransactionAmount,
+ valid: propsValid,
+ errorMessage,
+ errorKey: propsErrorKey,
+ currentCurrency,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ warning,
+ } = this.props
+
+ 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}
+ 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}
+ valid={propsValid || valid}
+ 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..31108bbd0
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,169 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+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'
+
+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
+ 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..25259b98c
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -0,0 +1,77 @@
+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_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../../routes'
+import { isConfirmDeployContract } from './confirm-transaction-switch.util'
+import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } 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 } = txData
+
+
+ if (isConfirmDeployContract(txData)) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (fetchingMethodData) {
+ return <Loading />
+ }
+
+ if (name) {
+ const methodName = name.toLowerCase()
+
+ switch (methodName.toLowerCase()) {
+ 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 }} />
+ }
+ 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..622d2a37a
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
@@ -0,0 +1,2 @@
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
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..874a89fd2
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
@@ -0,0 +1,150 @@
+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 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_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?${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/home.js b/ui/app/components/pages/home.js
index c53413d3b..38aa02dae 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')
@@ -83,51 +83,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 +90,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'})
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
index b15c59863..8b333b6a8 100644
--- a/ui/app/components/pages/index.scss
+++ b/ui/app/components/pages/index.scss
@@ -3,3 +3,5 @@
@import './add-token/index';
@import './confirm-add-token/index';
+
+@import './confirm-send-token/index';
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/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 588384efb..6e30d29a4 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
@@ -21,6 +21,7 @@ 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,
@@ -39,6 +40,7 @@ export default class SendAmountRow extends Component {
primaryCurrency,
selectedToken,
tokenBalance,
+ updateGasFeeError,
updateSendAmountError,
} = this.props
@@ -52,6 +54,19 @@ export default class SendAmountRow extends Component {
selectedToken,
tokenBalance,
})
+
+ if (selectedToken) {
+ updateGasFeeError({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ }
}
updateAmount (amount) {
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 b816d948f..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
@@ -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,
@@ -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/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..95c000a34 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
@@ -13,6 +13,7 @@ const propsMethodSpies = {
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 94d9918a7..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
@@ -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-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
index e9f5fda29..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
@@ -8,6 +8,7 @@ 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,
@@ -23,11 +24,16 @@ export default class SendGasRow extends Component {
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}
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 6e6fbc8a8..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
@@ -4,7 +4,7 @@ import {
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'
@@ -15,7 +15,8 @@ function mapStateToProps (state) {
conversionRate: getConversionRate(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.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
index d069ae8c6..96f6293c2 100644
--- 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
@@ -1,9 +1,14 @@
const selectors = {
- sendGasIsInError,
+ gasFeeIsInError,
+ getGasLoadingError,
}
module.exports = selectors
-function sendGasIsInError (state) {
+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 db37f18be..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
@@ -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 e928c8aba..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
@@ -22,7 +22,10 @@ proxyquire('../send-gas-row.container.js', {
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
index a5196334e..d46dd9d8b 100644
--- 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
@@ -1,11 +1,12 @@
import assert from 'assert'
import {
- sendGasIsInError,
+ gasFeeIsInError,
+ getGasLoadingError,
} from '../send-gas-row.selectors.js'
describe('send-gas-row selectors', () => {
- describe('sendGasIsInError()', () => {
+ describe('getGasLoadingError()', () => {
it('should return send.errors.gasLoading', () => {
const state = {
send: {
@@ -15,7 +16,33 @@ describe('send-gas-row selectors', () => {
},
}
- assert.equal(sendGasIsInError(state), '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-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js
index ee0578791..2085f1dce 100644
--- a/ui/app/components/send_/send-footer/send-footer.component.js
+++ b/ui/app/components/send_/send-footer/send-footer.component.js
@@ -52,6 +52,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.
@@ -64,7 +65,7 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts)
- editingTransactionId
+ const promise = editingTransactionId
? update({
amount,
editingTransactionId,
@@ -77,7 +78,8 @@ export default class SendFooter extends Component {
})
: sign({ selectedToken, to, amount, from, gas, gasPrice })
- this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
+ Promise.resolve(promise)
+ .then(() => history.push(CONFIRM_TRANSACTION_ROUTE))
}
formShouldBeDisabled () {
diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send_/send-footer/send-footer.container.js
index 260ff40bc..0af6fcfa1 100644
--- a/ui/app/components/send_/send-footer/send-footer.container.js
+++ b/ui/app/components/send_/send-footer/send-footer.container.js
@@ -87,7 +87,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/tests/send-footer-component.test.js b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js
index e071fe54f..4b2cd327d 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
@@ -166,10 +166,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.component.js b/ui/app/components/send_/send.component.js
index d705a3527..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'
@@ -116,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) {
@@ -147,6 +160,10 @@ export default class SendTransactionScreen extends PersistentForm {
this.updateGas()
}
+ componentWillUnmount () {
+ this.props.resetSendState()
+ }
+
render () {
const { history } = this.props
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.utils.js b/ui/app/components/send_/send.utils.js
index 34275248f..aa255c3d4 100644
--- a/ui/app/components/send_/send.utils.js
+++ b/ui/app/components/send_/send.utils.js
@@ -30,13 +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,
@@ -46,9 +47,9 @@ function calcGasTotal (gasLimit, gasPrice) {
function isBalanceSufficient ({
amount = '0x0',
- amountConversionRate = 0,
- balance,
- conversionRate,
+ amountConversionRate = 1,
+ balance = '0x0',
+ conversionRate = 1,
gasTotal = '0x0',
primaryCurrency,
}) {
@@ -110,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,
@@ -149,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) + ''
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-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js
index a518a64e9..b8579e0e4 100644
--- a/ui/app/components/send_/tests/send-utils.test.js
+++ b/ui/app/components/send_/tests/send-utils.test.js
@@ -17,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}`),
@@ -49,6 +53,7 @@ const {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
calcTokenBalance,
isBalanceSufficient,
@@ -143,6 +148,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,
@@ -163,6 +180,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({
@@ -222,6 +265,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,
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/signature-request.js b/ui/app/components/signature-request.js
index bbb340fcf..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()),
}
}
@@ -247,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 df3bd59bb..99ca7335c 100644
--- a/ui/app/components/token-balance.js
+++ b/ui/app/components/token-balance.js
@@ -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..e539514ec 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,
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/conf-tx.js b/ui/app/conf-tx.js
index 461587cb1..4e8aaa07d 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -105,7 +105,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),
@@ -174,7 +174,6 @@ ConfirmTxScreen.prototype.render = function () {
]),
*/
-
return currentTxView({
// Properties
txData: txData,
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/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/index.scss b/ui/app/css/itcss/components/index.scss
index 1d87b8004..5be040906 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -58,6 +58,4 @@
@import './welcome-screen.scss';
-@import './sender-to-recipient.scss';
-
@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
index 6c8be0b6d..545a2a940 100644
--- a/ui/app/css/itcss/components/network.scss
+++ b/ui/app/css/itcss/components/network.scss
@@ -159,15 +159,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/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 667e45ba2..bbfd85c90 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -332,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/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/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..ad247a348
--- /dev/null
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -0,0 +1,116 @@
+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'
+
+abiDecoder.addABI(abi)
+
+import MethodRegistry from 'eth-method-registry'
+const registry = new MethodRegistry({ provider: global.ethereumProvider })
+
+import {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} from '../../conversion-util'
+
+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
+}
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/routes.js b/ui/app/routes.js
index 0ff3f644d..7ac606b1a 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -10,8 +10,6 @@ const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
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 +21,14 @@ 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_TOKEN_METHOD_PATH = '/token-method'
+const SIGNATURE_REQUEST_PATH = '/signature-request'
+
module.exports = {
DEFAULT_ROUTE,
UNLOCK_ROUTE,
@@ -36,9 +42,7 @@ module.exports = {
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
SEND_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
- SIGNATURE_REQUEST_ROUTE,
WELCOME_ROUTE,
INITIALIZE_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
@@ -48,4 +52,11 @@ 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_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index 3e2253550..d86462275 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -27,6 +27,7 @@ const selectors = {
autoAddToBetaUI,
getSendMaxModeState,
getCurrentViewContext,
+ getTotalUnapprovedCount,
}
module.exports = selectors
@@ -181,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..cde83804d
--- /dev/null
+++ b/ui/app/selectors/confirm-transaction.js
@@ -0,0 +1,65 @@
+import { createSelector } from 'reselect'
+import txHelper from '../../lib/tx-helper'
+
+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,
+ }
+ }
+)
+
+export const currentCurrencySelector = state => state.metamask.currentCurrency
+export const conversionRateSelector = state => state.metamask.conversionRate
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 {}