aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Tseung <alextsg@users.noreply.github.com>2018-07-12 09:31:50 +0800
committerGitHub <noreply@github.com>2018-07-12 09:31:50 +0800
commit0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9 (patch)
tree10251992448d308123c16a6a01e02d7b422ddad2
parent4521de19e641e4cda27b906c47b46929ddb831ec (diff)
parent67017711df521e4d9f92cfc756b5468f7704a79c (diff)
downloadtangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar.gz
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar.bz2
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar.lz
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar.xz
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.tar.zst
tangerine-wallet-browser-0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9.zip
Merge pull request #4691 from MetaMask/i4404-confirm-refactor
Refactor and redesign confirm transaction views
-rw-r--r--app/_locales/en/messages.json24
-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/controllers/network/enums.js3
-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--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.js369
-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.js14
-rw-r--r--test/unit/actions/tx_test.js102
-rw-r--r--ui/app/actions.js70
-rw-r--r--ui/app/app.js7
-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.js94
-rw-r--r--ui/app/components/pages/index.scss2
-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.utils.js8
-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/tooltip-v2.js4
-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/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/tests/confirm-transaction.duck.test.js675
-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/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
125 files changed, 5145 insertions, 775 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 621775592..897f16f04 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"
},
@@ -582,6 +600,9 @@
"message": "or",
"description": "choice between creating or importing a new account"
},
+ "origin": {
+ "message": "Origin"
+ },
"password": {
"message": "Password"
},
@@ -911,6 +932,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/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/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/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 b07b1ecd7..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)
@@ -366,8 +391,7 @@ describe('MetaMask', function () {
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)
})
})
@@ -383,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()
@@ -404,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'))
@@ -466,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)
@@ -479,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'))
@@ -492,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()
@@ -524,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()
@@ -532,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'))
@@ -556,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')
-
- await delay(regularDelayMs * 10)
- const [extension, tokenFactory] = await driver.getAllWindowHandles()
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = windowHandles[1]
+ await delay(regularDelayMs * 2)
- 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()
@@ -601,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)
@@ -668,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()
@@ -684,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()
@@ -709,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)
@@ -747,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()
@@ -766,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 () {
@@ -785,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/))
@@ -799,11 +867,110 @@ 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/))
+ })
+ })
+
describe('Hide token', () => {
it('hides the token when clicked', async () => {
const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis'))
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 d5e80151c..406863ca6 100644
--- a/test/integration/lib/send-new-ui.js
+++ b/test/integration/lib/send-new-ui.js
@@ -125,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/ui/app/actions.js b/ui/app/actions.js
index ad890f565..1fb49c920 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -704,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({}))
}
@@ -910,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())
@@ -943,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
+ })
}
}
@@ -1038,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 257239a61..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')
@@ -77,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..86bd32c8a 100644
--- a/ui/app/components/pages/home.js
+++ b/ui/app/components/pages/home.js
@@ -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/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js
index 6471ae1a3..ff42e58de 100644
--- a/ui/app/components/send_/send-footer/send-footer.component.js
+++ b/ui/app/components/send_/send-footer/send-footer.component.js
@@ -48,6 +48,7 @@ export default class SendFooter extends Component {
// updateTx,
update,
toAccounts,
+ history,
} = this.props
// Should not be needed because submit should be disabled if there are errors.
@@ -60,7 +61,7 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts)
- editingTransactionId
+ const promise = editingTransactionId
? update({
amount,
editingTransactionId,
@@ -73,7 +74,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.utils.js b/ui/app/components/send_/send.utils.js
index c4537f335..aa255c3d4 100644
--- a/ui/app/components/send_/send.utils.js
+++ b/ui/app/components/send_/send.utils.js
@@ -37,7 +37,7 @@ module.exports = {
removeLeadingZeroes,
}
-function calcGasTotal (gasLimit, gasPrice) {
+function calcGasTotal (gasLimit = '0', gasPrice = '0') {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
@@ -47,9 +47,9 @@ function calcGasTotal (gasLimit, gasPrice) {
function isBalanceSufficient ({
amount = '0x0',
- amountConversionRate = 0,
- balance,
- conversionRate,
+ amountConversionRate = 1,
+ balance = '0x0',
+ conversionRate = 1,
gasTotal = '0x0',
primaryCurrency,
}) {
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/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/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/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/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/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/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