Merge pull request #4691 from MetaMask/i4404-confirm-refactor

Refactor and redesign confirm transaction views
This commit is contained in:
Alexander Tseung 2018-07-11 15:31:50 -10:00 committed by GitHub
commit 0d4dbbec2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 5150 additions and 780 deletions

View File

@ -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 worlds 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"
},

14
app/images/alert-red.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 774 B

19
app/images/alert.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 1.1 KiB

18
app/images/caret-left.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -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,

View File

@ -10,7 +10,7 @@ module.exports = {
signPersonalMessage: (msgData, cb) => {
const stateUpdate = {
unapprovedPersonalMsgs: {},
unapprovedPersonalMsgsCount: 0,
unapprovedPersonalMsgCount: 0,
}
return cb(null, stateUpdate)
},

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -58,5 +58,23 @@
}
},
"identities": {},
"computedBalances": {}
"computedBalances": {},
"confirmTransaction": {
"txData": {},
"tokenData": {},
"methodData": {},
"tokenProps": {
"tokenDecimals": "",
"tokenSymbol": ""
},
"fiatTransactionAmount": "",
"fiatTransactionFee": "",
"fiatTransactionTotal": "",
"ethTransactionAmount": "",
"ethTransactionFee": "",
"ethTransactionTotal": "",
"hexGasTotal": "",
"nonce": "",
"fetchingMethodData": false
}
}

View File

@ -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
}
}

View File

@ -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
}
}

134
package-lock.json generated
View File

@ -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",

View File

@ -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",

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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')
const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet)
const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
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 send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
await send1eth.click()
await waitUntilXWindowHandles(driver, 2)
let windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = windowHandles[1]
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[2])
await delay(regularDelayMs)
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')
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = windowHandles[1]
await delay(regularDelayMs * 2)
await delay(regularDelayMs * 10)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
const [
totalSupply,
tokenName,
tokenDecimal,
tokenSymbol,
] = await findElements(driver, By.css('.form-control'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
@ -601,15 +654,16 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(tokenFactory)
await driver.switchTo().window(dapp)
await delay(tinyDelayMs)
const tokenContractAddress = await driver.findElement(By.css('#tokenAddress'))
await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/))
tokenAddress = await tokenContractAddress.getText()
await delay(regularDelayMs)
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
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'))
await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await transferToAmount.sendKeys('26')
const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
await transferAmountButton.click()
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)
const [,, popup] = await driver.getAllWindowHandles()
await driver.switchTo().window(popup)
await driver.close()
await driver.switchTo().window(dapp)
await delay(tinyDelayMs)
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`))
await transferTokens.click()
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'))

View File

@ -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 () {

View File

@ -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)
}

View File

@ -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')

View File

@ -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')

View File

@ -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'
const actions = require(path.join(__dirname, '../../../ui/app/actions.js'))
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
describe('tx confirmation screen', function () {
beforeEach(function () {
this.sinon = sinon.createSandbox()
})
afterEach(function () {
this.sinon.restore()
})
var initialState, result
describe('when there is only one tx', function () {
var firstTxId = 1457634084250832
beforeEach(function () {
initialState = {
appState: {
currentView: {
name: 'confTx',
},
const txId = 1457634084250832
const initialState = {
appState: {
currentView: {
name: 'confTx',
},
},
metamask: {
unapprovedTxs: {
[txId]: {
id: txId,
status: 'unconfirmed',
time: 1457634084250,
},
metamask: {
unapprovedTxs: {
'1457634084250832': {
id: 1457634084250832,
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
}

View File

@ -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, () => {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
resolve(txData)
background.cancelTransaction(txData.id, err => {
if (err) {
return reject(err)
}
resolve()
})
})
.then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication())
return txData
})
}
}

View File

@ -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 }),

View File

@ -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() }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
export { default } from './confirm-detail-row.component'

View File

@ -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;
}
}
}

View File

@ -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>
)
}
}

View File

@ -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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-error.component'

View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-summary.component'

View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-warning.component'

View File

@ -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;
}
}

View File

@ -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'

View File

@ -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;
}
}

View File

@ -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>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-header.component'

View File

@ -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;
}
}

View File

@ -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>
)
}
}

View File

@ -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'

View File

@ -0,0 +1,5 @@
@import './confirm-page-container-content/index';
@import './confirm-page-container-header/index';
@import './confirm-detail-row/index';

View File

@ -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`,
},
})
)

View File

@ -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';

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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',
}))
}

View File

@ -0,0 +1 @@
export { default } from './customize-gas.container'

View File

@ -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;
}
}

View File

@ -1,3 +1,5 @@
@import './customize-gas/index';
.modal-container {
width: 100%;
height: 100%;

View File

@ -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',

View File

@ -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)

View File

@ -0,0 +1,2 @@
import NetworkDisplay from './network-display.container'
module.exports = NetworkDisplay

View File

@ -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;
}
}
}

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -1 +1,4 @@
import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer'
export { default } from './page-container.component'
export { PageContainerHeader, PageContainerFooter }

View File

@ -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;
}
}

View File

@ -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)}

View File

@ -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>
)
}
}

View File

@ -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>
)

View File

@ -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
/>
)
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './confirm-approve.container'

View File

@ -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()}
/>
)
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './confirm-deploy-contract.container'

View File

@ -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)}
/>
)
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './confirm-send-ether.container'

View File

@ -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
/>
)
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './confirm-send-token.container'

View File

@ -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;
}
}

View File

@ -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()}
/>
)
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './confirm-transaction-base.container'

View File

@ -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 />
}
}

View File

@ -0,0 +1,2 @@
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'

View File

@ -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)

View File

@ -0,0 +1,4 @@
export function isConfirmDeployContract (txData = {}) {
const { txParams = {} } = txData
return !txParams.to
}

View File

@ -0,0 +1,2 @@
import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
module.exports = ConfirmTransactionSwitch

View File

@ -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 />
}
}

View File

@ -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)

View File

@ -0,0 +1,2 @@
import ConfirmTransaction from './confirm-transaction.container'
module.exports = ConfirmTransaction

View File

@ -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'})

View File

@ -3,3 +3,5 @@
@import './add-token/index';
@import './confirm-add-token/index';
@import './confirm-send-token/index';

View File

@ -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 () {

View File

@ -87,7 +87,7 @@ function mapDispatchToProps (dispatch) {
unapprovedTxs,
})
dispatch(updateTransaction(editingTx))
return dispatch(updateTransaction(editingTx))
},
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)

View File

@ -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()
})
})
})

View File

@ -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,
}) {

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './sender-to-recipient.component'

View File

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More