diff --git a/CHANGELOG.md b/CHANGELOG.md index d86fcd713..4ad52b795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master + - Remove rejected transactions from transaction history + ## 4.8.0 Thur Jun 14 2018 - [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error. diff --git a/README.md b/README.md index 70faa8856..513a1d1cb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ If you're a user seeking support, [here is our support site](https://metamask.he [Mission Statement](./MISSION.md) -[Internal documentation](./docs/jsdocs) +[Internal documentation](./docs#documentation) ## Developing Compatible Dapps diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 14d2f923a..c40a8c3b6 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -43,6 +43,9 @@ "message": "MetaMask", "description": "The name of the application" }, + "approve": { + "message": "Approve" + }, "approved": { "message": "Approved" }, @@ -95,6 +98,9 @@ "buyCoinbaseExplainer": { "message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin." }, + "bytes": { + "message": "Bytes" + }, "ok": { "message": "Ok" }, @@ -176,6 +182,9 @@ "copyContractAddress": { "message": "Copy Contract Address" }, + "copyAddress": { + "message": "Copy address to clipboard" + }, "copyToClipboard": { "message": "Copy to clipboard" }, @@ -307,6 +316,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "parameters": { + "message": "Parameters" + }, "passwordNotLongEnough": { "message": "Password not long enough" }, @@ -351,6 +363,9 @@ "fromShapeShift": { "message": "From ShapeShift" }, + "functionType": { + "message": "Function Type" + }, "gas": { "message": "Gas", "description": "Short indication of gas cost" @@ -406,6 +421,9 @@ "hereList": { "message": "Here's a list!!!!" }, + "hexData": { + "message": "Hex Data" + }, "hide": { "message": "Hide" }, @@ -618,6 +636,9 @@ "message": "or", "description": "choice between creating or importing a new account" }, + "origin": { + "message": "Origin" + }, "password": { "message": "Password" }, @@ -962,6 +983,9 @@ "transactionNumber": { "message": "Transaction Number" }, + "transfer": { + "message": "Transfer" + }, "transfers": { "message": "Transfers" }, diff --git a/app/images/alert-red.svg b/app/images/alert-red.svg new file mode 100644 index 000000000..ac5b30e27 --- /dev/null +++ b/app/images/alert-red.svg @@ -0,0 +1,14 @@ + + + + Artboard Copy + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/app/images/alert.svg b/app/images/alert.svg new file mode 100644 index 000000000..534eda194 --- /dev/null +++ b/app/images/alert.svg @@ -0,0 +1,19 @@ + + + + 7414FFD8-B28A-4593-9D7E-19E73D687B50 + Created with sketchtool. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/caret-left.svg b/app/images/caret-left.svg new file mode 100644 index 000000000..0ea266161 --- /dev/null +++ b/app/images/caret-left.svg @@ -0,0 +1,18 @@ + + + + 8439120D-5704-4273-B416-FEE134322584 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 9da7f309c..3190eb37c 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -4,6 +4,7 @@ const KOVAN = 'kovan' const MAINNET = 'mainnet' const LOCALHOST = 'localhost' +const MAINNET_CODE = 1 const ROPSTEN_CODE = 3 const RINKEYBY_CODE = 4 const KOVAN_CODE = 42 @@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby' const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' - module.exports = { ROPSTEN, RINKEBY, KOVAN, MAINNET, LOCALHOST, + MAINNET_CODE, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 0aae4774b..28a18ca2e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter { */ setTxStatusRejected (txId) { this._setTxStatus(txId, 'rejected') + this._removeTx(txId) } /** @@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter { _saveTxList (transactions) { this.store.updateState({ transactions }) } + + _removeTx (txId) { + const transactionList = this.getFullTxList() + this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId)) + } } module.exports = TransactionStateManager diff --git a/app/scripts/migrations/027.js b/app/scripts/migrations/027.js new file mode 100644 index 000000000..d6ebef580 --- /dev/null +++ b/app/scripts/migrations/027.js @@ -0,0 +1,35 @@ +// next version number +const version = 27 + +/* + +normalizes txParams on unconfirmed txs + +*/ +const clone = require('clone') + +module.exports = { + version, + + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + return versionedData + }, +} + +function transformState (state) { + const newState = state + + if (newState.TransactionController) { + if (newState.TransactionController.transactions) { + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected') + } + } + + return newState +} diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js index 665f72898..aee68854b 100644 --- a/development/backGroundConnectionModifiers.js +++ b/development/backGroundConnectionModifiers.js @@ -10,7 +10,7 @@ module.exports = { signPersonalMessage: (msgData, cb) => { const stateUpdate = { unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgsCount: 0, + unapprovedPersonalMsgCount: 0, } return cb(null, stateUpdate) }, diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json index 71ccbd96c..2c2e17704 100644 --- a/development/states/confirm-new-ui.json +++ b/development/states/confirm-new-ui.json @@ -156,5 +156,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index 3c9caafb0..829f513a8 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -73,7 +73,7 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658" }, "status": "unapproved", - "time": 1537889069339, + "time": 1537889070000, "type": "eth_sign" } }, @@ -86,11 +86,11 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" }, "status": "unapproved", - "time": 1517889069339, + "time": 1537889065000, "type": "personal_sign" } }, - "unapprovedPersonalMsgCount": 0, + "unapprovedPersonalMsgCount": 1 , "unapprovedTypedMessages": { "8997167822566869": { "id": 8997167822566869, @@ -102,7 +102,7 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" }, "status": "unapproved", - "time": 1617889069339, + "time": 1537889060000, "type": "eth_signTypedData" } }, @@ -172,5 +172,32 @@ "scrollToBottom": false, "forgottenPassword": null }, - "identities": {} + "identities": {}, + "confirmTransaction": { + "txData": { + "id": 8927167822566864, + "msgParams": { + "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0", + "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658" + }, + "status": "unapproved", + "time": 1537889069339, + "type": "eth_sign" + }, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 8c8b18a91..6848c0840 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -130,5 +130,23 @@ "scrollToBottom": false, "forgottenPassword": null }, - "identities": {} + "identities": {}, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/first-time.json b/development/states/first-time.json index f10eefd35..e88ec6d65 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -58,5 +58,23 @@ } }, "identities": {}, - "computedBalances": {} + "computedBalances": {}, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/send-edit.json b/development/states/send-edit.json index b05acbf3b..8e5c25a82 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -156,5 +156,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index b457749ee..ad2ff3d6e 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -135,5 +135,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/package-lock.json b/package-lock.json index 8b5976bc9..6491f71dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8529,6 +8529,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", @@ -26341,6 +26443,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", @@ -26742,6 +26853,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", diff --git a/package.json b/package.json index be1cd8d85..84db617bd 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.6", "eth-json-rpc-infura": "^3.0.0", + "eth-method-registry": "^1.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.4.2", @@ -180,6 +181,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", @@ -286,6 +288,7 @@ "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.6.2", "react-testutils-additions": "^15.2.0", + "redux-mock-store": "^1.5.3", "redux-test-utils": "^0.2.2", "resolve-url-loader": "^2.3.0", "rimraf": "^2.6.2", diff --git a/test/e2e/beta/contract-test/contract.js b/test/e2e/beta/contract-test/contract.js index 18c866f21..8af008dce 100644 --- a/test/e2e/beta/contract-test/contract.js +++ b/test/e2e/beta/contract-test/contract.js @@ -32,35 +32,103 @@ var piggybankContract = web3.eth.contract([{'constant': false, 'inputs': [{'name const deployButton = document.getElementById('deployButton') const depositButton = document.getElementById('depositButton') const withdrawButton = document.getElementById('withdrawButton') +const sendButton = document.getElementById('sendButton') +const createToken = document.getElementById('createToken') +const transferTokens = document.getElementById('transferTokens') +const approveTokens = document.getElementById('approveTokens') deployButton.addEventListener('click', async function (event) { + var piggybank = await piggybankContract.new( + { + from: web3.eth.accounts[0], + data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + gas: '4700000', + }, function (e, contract) { + console.log(e, contract) + if (typeof contract.address !== 'undefined') { + console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) - var piggybank = await piggybankContract.new( - { - from: web3.eth.accounts[0], - data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', - gas: '4700000', - }, function (e, contract) { - console.log(e, contract) - if (typeof contract.address !== 'undefined') { - console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) - - console.log(`contract`, contract) - - depositButton.addEventListener('click', function (event) { - contract.deposit({ from: web3.eth.accounts[0], value: '0x29a2241af62c0000' }, function (result) { - console.log(result) - }) + console.log(`contract`, contract) + + depositButton.addEventListener('click', function (event) { + contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) { + console.log(result) + }) + }) + + withdrawButton.addEventListener('click', function (event) { + contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) { + console.log(result) }) + }) + } + }) + + console.log(piggybank) +}) + +sendButton.addEventListener('click', function (event) { + web3.eth.sendTransaction({ + from: web3.eth.accounts[0], + to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + value: '0x29a2241af62c0000', + gas: 21000, + gasPrice: 20000000000, + }, (result) => { + console.log(result) + }) +}) + + +createToken.addEventListener('click', async function (event) { + var _initialAmount = 100 + var _tokenName = 'TST' + var _decimalUnits = 0 + var _tokenSymbol = 'TST' + var humanstandardtokenContract = web3.eth.contract([{'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'approve', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_from', 'type': 'address'}, {'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transferFrom', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{'name': '', 'type': 'uint8'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}, {'name': '_extraData', 'type': 'bytes'}], 'name': 'approveAndCall', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}, {'name': '_spender', 'type': 'address'}], 'name': 'allowance', 'outputs': [{'name': 'remaining', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initialAmount', 'type': 'uint256'}, {'name': '_tokenName', 'type': 'string'}, {'name': '_decimalUnits', 'type': 'uint8'}, {'name': '_tokenSymbol', 'type': 'string'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'payable': false, 'stateMutability': 'nonpayable', 'type': 'fallback'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_from', 'type': 'address'}, {'indexed': true, 'name': '_to', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_owner', 'type': 'address'}, {'indexed': true, 'name': '_spender', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Approval', 'type': 'event'}]) + return humanstandardtokenContract.new( + _initialAmount, + _tokenName, + _decimalUnits, + _tokenSymbol, + { + from: web3.eth.accounts[0], + data: '0x60806040526040805190810160405280600481526020017f48302e3100000000000000000000000000000000000000000000000000000000815250600690805190602001906200005192919062000143565b503480156200005f57600080fd5b50604051620011f3380380620011f383398101806040528101908080519060200190929190805182019291906020018051906020019092919080518201929190505050836000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508360028190555082600390805190602001906200010492919062000143565b5081600460006101000a81548160ff021916908360ff16021790555080600590805190602001906200013892919062000143565b5050505050620001f2565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200018657805160ff1916838001178555620001b7565b82800160010185558215620001b7579182015b82811115620001b657825182559160200191906001019062000199565b5b509050620001c69190620001ca565b5090565b620001ef91905b80821115620001eb576000816000905550600101620001d1565b5090565b90565b610ff180620002026000396000f3006080604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100c1578063095ea7b31461015157806318160ddd146101b657806323b872dd146101e1578063313ce5671461026657806354fd4d501461029757806370a082311461032757806395d89b411461037e578063a9059cbb1461040e578063cae9ca5114610473578063dd62ed3e1461051e575b3480156100bb57600080fd5b50600080fd5b3480156100cd57600080fd5b506100d6610595565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101165780820151818401526020810190506100fb565b50505050905090810190601f1680156101435780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015d57600080fd5b5061019c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610633565b604051808215151515815260200191505060405180910390f35b3480156101c257600080fd5b506101cb610725565b6040518082815260200191505060405180910390f35b3480156101ed57600080fd5b5061024c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061072b565b604051808215151515815260200191505060405180910390f35b34801561027257600080fd5b5061027b6109a4565b604051808260ff1660ff16815260200191505060405180910390f35b3480156102a357600080fd5b506102ac6109b7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102ec5780820151818401526020810190506102d1565b50505050905090810190601f1680156103195780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561033357600080fd5b50610368600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610a55565b6040518082815260200191505060405180910390f35b34801561038a57600080fd5b50610393610a9d565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103d35780820151818401526020810190506103b8565b50505050905090810190601f1680156104005780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561041a57600080fd5b50610459600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610b3b565b604051808215151515815260200191505060405180910390f35b34801561047f57600080fd5b50610504600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509192919290505050610ca1565b604051808215151515815260200191505060405180910390f35b34801561052a57600080fd5b5061057f600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610f3e565b6040518082815260200191505060405180910390f35b60038054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561062b5780601f106106005761010080835404028352916020019161062b565b820191906000526020600020905b81548152906001019060200180831161060e57829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60025481565b6000816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101580156107f7575081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410155b80156108035750600082115b1561099857816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a36001905061099d565b600090505b9392505050565b600460009054906101000a900460ff1681565b60068054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610a4d5780601f10610a2257610100808354040283529160200191610a4d565b820191906000526020600020905b815481529060010190602001808311610a3057829003601f168201915b505050505081565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60058054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610b335780601f10610b0857610100808354040283529160200191610b33565b820191906000526020600020905b815481529060010190602001808311610b1657829003601f168201915b505050505081565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610b8b5750600082115b15610c9657816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610c9b565b600090505b92915050565b600082600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a38373ffffffffffffffffffffffffffffffffffffffff1660405180807f72656365697665417070726f76616c28616464726573732c75696e743235362c81526020017f616464726573732c627974657329000000000000000000000000000000000000815250602e01905060405180910390207c01000000000000000000000000000000000000000000000000000000009004338530866040518563ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018481526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828051906020019080838360005b83811015610ee2578082015181840152602081019050610ec7565b50505050905090810190601f168015610f0f5780820380516001836020036101000a031916815260200191505b509450505050506000604051808303816000875af1925050501515610f3357600080fd5b600190509392505050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050929150505600a165627a7a723058200052919e0bc22b5adcd3d320be977df3a1dcc35d1a0160287383ba371900a1c50029', + gas: '4700000', + gasPrice: '20000000000', + }, function (e, contract) { + console.log(e, contract) + if (typeof contract.address !== 'undefined') { + console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) + + document.getElementById('tokenAddress').innerHTML = contract.address - withdrawButton.addEventListener('click', function (event) { - contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) { - console.log(result) - }) + transferTokens.addEventListener('click', function (event) { + console.log(`event`, event) + contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', { + from: web3.eth.accounts[0], + to: contract.address, + data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', + gas: 60000, + gasPrice: '20000000000', + }, function (result) { + console.log('result', result) }) - } - }) + }) - console.log(piggybank) + approveTokens.addEventListener('click', function (event) { + contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', { + from: web3.eth.accounts[0], + to: contract.address, + data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', + gas: 60000, + gasPrice: '20000000000', + }, function (result) { + console.log(result) + }) + }) + } + }) }) + diff --git a/test/e2e/beta/contract-test/index.html b/test/e2e/beta/contract-test/index.html index 0868633f7..0d63fd940 100644 --- a/test/e2e/beta/contract-test/index.html +++ b/test/e2e/beta/contract-test/index.html @@ -1,8 +1,33 @@ + + E2E Test Dapp + - - - - +
+
Contract
+
+ + + +
+
+
+
Send eth
+
+ +
+
+
+
Send tokens
+
+
+ + + +
+
+ + + \ No newline at end of file diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js index fcc3e96d6..828f87db7 100644 --- a/test/e2e/beta/helpers.js +++ b/test/e2e/beta/helpers.js @@ -1,16 +1,21 @@ const fs = require('fs') const mkdirp = require('mkdirp') const pify = require('pify') +const assert = require('assert') const {until} = require('selenium-webdriver') const { delay } = require('../func') module.exports = { + assertElementNotPresent, checkBrowserForConsoleErrors, - loadExtension, - verboseReportOnFailure, + closeAllWindowHandlesExcept, findElement, findElements, + loadExtension, openNewPage, + switchToWindowWithTitle, + verboseReportOnFailure, + waitUntilXWindowHandles, } async function loadExtension (driver, extensionId) { @@ -72,9 +77,57 @@ async function openNewPage (driver, url) { await delay(1000) const handles = await driver.getAllWindowHandles() - const secondHandle = handles[1] - await driver.switchTo().window(secondHandle) + const lastHandle = handles[handles.length - 1] + await driver.switchTo().window(lastHandle) await driver.get(url) await delay(1000) } + +async function waitUntilXWindowHandles (driver, x) { + const windowHandles = await driver.getAllWindowHandles() + if (windowHandles.length === x) return + await delay(1000) + return await waitUntilXWindowHandles(driver, x) +} + +async function switchToWindowWithTitle (driver, title, windowHandles) { + if (!windowHandles) { + windowHandles = await driver.getAllWindowHandles() + } else if (windowHandles.length === 0) { + throw new Error('No window with title: ' + title) + } + const firstHandle = windowHandles[0] + await driver.switchTo().window(firstHandle) + const handleTitle = await driver.getTitle() + + if (handleTitle === title) { + return firstHandle + } else { + return await switchToWindowWithTitle(driver, title, windowHandles.slice(1)) + } +} + +async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) { + exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions + windowHandles = windowHandles || await driver.getAllWindowHandles() + const lastWindowHandle = windowHandles.pop() + if (!exceptions.includes(lastWindowHandle)) { + await driver.switchTo().window(lastWindowHandle) + await delay(1000) + await driver.close() + await delay(1000) + } + return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles) +} + +async function assertElementNotPresent (webdriver, driver, by) { + try { + const dataTab = await findElement(driver, by, 4000) + if (dataTab) { + assert(false, 'Data tab should not be present') + } + } catch (err) { + assert(err instanceof webdriver.error.NoSuchElementError) + } +} diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index b07b1ecd7..caa49d450 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -11,12 +11,16 @@ const { getExtensionIdFirefox, } = require('../func') const { + assertElementNotPresent, + checkBrowserForConsoleErrors, + closeAllWindowHandlesExcept, findElement, findElements, - checkBrowserForConsoleErrors, loadExtension, - verboseReportOnFailure, openNewPage, + switchToWindowWithTitle, + verboseReportOnFailure, + waitUntilXWindowHandles, } = require('./helpers') describe('MetaMask', function () { @@ -25,7 +29,7 @@ describe('MetaMask', function () { let tokenAddress const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' - const tinyDelayMs = 1000 + const tinyDelayMs = 200 const regularDelayMs = tinyDelayMs * 2 const largeDelayMs = regularDelayMs * 2 @@ -80,20 +84,29 @@ describe('MetaMask', function () { networkSelector = await findElement(driver, By.css('#network_component')) } catch (e) { await loadExtension(driver, extensionId) + await delay(largeDelayMs * 2) + networkSelector = await findElement(driver, By.css('#network_component')) } await delay(regularDelayMs) }) - it('use the local network', async function () { + it('uses the local network', async function () { await networkSelector.click() await delay(regularDelayMs) - const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`)) + const networks = await findElements(driver, By.css('.dropdown-menu-item')) + const localhost = networks[4] + await driver.wait(until.elementTextMatches(localhost, /Localhost/)) await localhost.click() await delay(regularDelayMs) }) it('selects the new UI option', async () => { + try { + const overlay = await findElement(driver, By.css('.full-flex-height')) + await driver.wait(until.stalenessOf(overlay)) + } catch (e) {} + const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]")) await button.click() await delay(regularDelayMs) @@ -188,8 +201,20 @@ describe('MetaMask', function () { await delay(regularDelayMs) }) - async function retypeSeedPhrase (words) { + async function retypeSeedPhrase (words, wasReloaded) { try { + if (wasReloaded) { + const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button') + await driver.wait(until.elementLocated(byRevealButton, 10000)) + const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) + await revealSeedPhraseButton.click() + await delay(regularDelayMs) + + const nextScreen = await findElement(driver, By.css('.backup-phrase button')) + await nextScreen.click() + await delay(regularDelayMs) + } + const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000) await word0.click() @@ -250,7 +275,7 @@ describe('MetaMask', function () { await delay(tinyDelayMs) } catch (e) { await loadExtension(driver, extensionId) - await retypeSeedPhrase(words) + await retypeSeedPhrase(words, true) } } @@ -303,7 +328,7 @@ describe('MetaMask', function () { it('accepts the account password after lock', async () => { await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') await driver.findElement(By.id('password')).sendKeys(Key.ENTER) - await delay(regularDelayMs * 4) + await delay(largeDelayMs * 4) }) }) @@ -324,10 +349,10 @@ describe('MetaMask', function () { const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`)) await create.click() - await delay(regularDelayMs) + await delay(largeDelayMs) }) - it('should correct account name', async () => { + it('should display correct account name', async () => { const accountName = await findElement(driver, By.css('.account-name')) assert.equal(await accountName.getText(), '2nd account') await delay(regularDelayMs) @@ -366,8 +391,7 @@ describe('MetaMask', function () { it('balance renders', async () => { const balance = await findElement(driver, By.css('.balance-display .token-amount')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '100.000 ETH') + await driver.wait(until.elementTextMatches(balance, /100.+ETH/)) await delay(regularDelayMs) }) }) @@ -383,6 +407,9 @@ describe('MetaMask', function () { await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') + const inputValue = await inputAmount.getAttribute('value') + assert.equal(inputValue, '1') + // Set the gas limit const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button')) await configureGas.click() @@ -404,59 +431,71 @@ describe('MetaMask', function () { it('confirms the transaction', async function () { const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() - await delay(regularDelayMs) + await delay(largeDelayMs) }) it('finds the transaction in the transactions list', async function () { const transactions = await findElements(driver, By.css('.tx-list-item')) assert.equal(transactions.length, 1) - const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000) + if (process.env.SELENIUM_BROWSER !== 'firefox') { + const txValues = await findElement(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000) + } }) }) - describe('Send ETH from Faucet', () => { - it('starts a send transaction inside Faucet', async () => { - await openNewPage(driver, 'https://faucet.metamask.io') + describe('Send ETH from dapp', () => { + it('starts a send transaction inside the dapp', async () => { + await openNewPage(driver, 'http://127.0.0.1:8080/') + await delay(regularDelayMs) - const [extension, faucet] = await driver.getAllWindowHandles() - await driver.switchTo().window(faucet) + await waitUntilXWindowHandles(driver, 2) + let windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = windowHandles[1] - const faucetPageTitle = await findElement(driver, By.css('.container-fluid')) - await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/)) + await driver.switchTo().window(dapp) await delay(regularDelayMs) - const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000) - await send1eth.click() + const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000) + await send3eth.click() await delay(regularDelayMs) - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) + windowHandles = await driver.getAllWindowHandles() + await driver.switchTo().window(windowHandles[2]) await delay(regularDelayMs) - const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000) + assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`)) + + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000) await confirmButton.click() await delay(regularDelayMs) - await driver.switchTo().window(faucet) - await delay(regularDelayMs) - await driver.close() - await delay(regularDelayMs) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) await delay(regularDelayMs) - await loadExtension(driver, extensionId) - await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + const transactions = await findElements(driver, By.css('.tx-list-item')) + assert.equal(transactions.length, 2) + + const txValues = await findElement(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000) }) }) describe('Deploy contract and call contract methods', () => { let extension - let contractTestPage - it('confirms a deploy contract transaction', async () => { - await openNewPage(driver, 'http://127.0.0.1:8080/'); + let dapp + it('creates a deploy contract transaction', async () => { + const windowHandles = await driver.getAllWindowHandles() + extension = windowHandles[0] + dapp = windowHandles[1] + await delay(tinyDelayMs) - [extension, contractTestPage] = await driver.getAllWindowHandles() + await driver.switchTo().window(dapp) await delay(regularDelayMs) const deployContractButton = await findElement(driver, By.css('#deployButton')) @@ -466,10 +505,28 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(regularDelayMs) - const txListItem = await findElement(driver, By.css('.tx-list-item')) + const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`)) await txListItem.click() await delay(regularDelayMs) + }) + + it('displays the contract creation data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await (regularDelayMs) + + await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`)) + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/)) + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await (regularDelayMs) + }) + + it('confirms a deploy contract transaction', async () => { const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() await delay(regularDelayMs) @@ -479,10 +536,11 @@ describe('MetaMask', function () { const txAccounts = await findElements(driver, By.css('.tx-list-account')) assert.equal(await txAccounts[0].getText(), 'Contract Deployment') + await delay(regularDelayMs) }) it('calls and confirms a contract method where ETH is sent', async () => { - await driver.switchTo().window(contractTestPage) + await driver.switchTo().window(dapp) await delay(regularDelayMs) const depositButton = await findElement(driver, By.css('#depositButton')) @@ -492,17 +550,19 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(regularDelayMs) - const txListItem = await findElement(driver, By.css('.tx-list-item')) - await txListItem.click() + await findElements(driver, By.css('.tx-list-pending-item-container')) + const [txListValue] = await findElements(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000) + await txListValue.click() await delay(regularDelayMs) // Set the gas limit - const configureGas = await findElement(driver, By.css('.sliders-icon-container')) + const configureGas = await findElement(driver, By.css('.confirm-detail-row__header-text--edit')) await configureGas.click() await delay(regularDelayMs) const gasModal = await driver.findElement(By.css('span .modal')) - await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title'))) + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) await gasPriceInput.clear() @@ -524,7 +584,7 @@ describe('MetaMask', function () { await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000) + await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000) const txAccounts = await findElements(driver, By.css('.tx-list-account')) const firstTxAddress = await txAccounts[0].getText() @@ -532,7 +592,7 @@ describe('MetaMask', function () { }) it('calls and confirms a contract method where ETH is received', async () => { - await driver.switchTo().window(contractTestPage) + await driver.switchTo().window(dapp) await delay(regularDelayMs) const withdrawButton = await findElement(driver, By.css('#withdrawButton')) @@ -556,38 +616,31 @@ describe('MetaMask', function () { const txValues = await findElement(driver, By.css('.tx-list-value')) await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000) - await driver.switchTo().window(contractTestPage) - await driver.close() + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) }) it('renders the correct ETH balance', async () => { const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) - await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000) - const tokenAmount = await balance.getText() - assert.ok(/^86.*ETH.*$/.test(tokenAmount)) await delay(regularDelayMs) + if (process.env.SELENIUM_BROWSER !== 'firefox') { + await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000) + const tokenAmount = await balance.getText() + assert.ok(/^92.*ETH.*$/.test(tokenAmount)) + await delay(regularDelayMs) + } }) }) - describe('Add a custom token from TokenFactory', () => { + describe('Add a custom token from a dapp', () => { it('creates a new token', async () => { - openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory') - - await delay(regularDelayMs * 10) - const [extension, tokenFactory] = await driver.getAllWindowHandles() + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = windowHandles[1] + await delay(regularDelayMs * 2) - const [ - totalSupply, - tokenName, - tokenDecimal, - tokenSymbol, - ] = await findElements(driver, By.css('.form-control')) - - await totalSupply.sendKeys('100') - await tokenName.sendKeys('Test') - await tokenDecimal.sendKeys('0') - await tokenSymbol.sendKeys('TST') + await driver.switchTo().window(dapp) + await delay(regularDelayMs) const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`)) await createToken.click() @@ -601,15 +654,16 @@ describe('MetaMask', function () { await confirmButton.click() await delay(regularDelayMs) - await driver.switchTo().window(tokenFactory) - await delay(regularDelayMs) + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) - const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) - tokenAddress = await tokenContactAddress.getText() + const tokenContractAddress = await driver.findElement(By.css('#tokenAddress')) + await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/)) + tokenAddress = await tokenContractAddress.getText() - await driver.close() - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) + await delay(regularDelayMs) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -668,7 +722,7 @@ describe('MetaMask', function () { gasModal = await driver.findElement(By.css('span .modal')) }) - it('customizes gas', async () => { + it('opens customizes gas modal', async () => { await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title'))) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() @@ -684,6 +738,24 @@ describe('MetaMask', function () { await delay(regularDelayMs) }) + it('displays the token transfer data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await (regularDelayMs) + + const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionTypeText = await functionType.getText() + assert.equal(functionTypeText, 'Transfer') + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/)) + + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await (regularDelayMs) + }) + it('submits the transaction', async function () { const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() @@ -709,37 +781,33 @@ describe('MetaMask', function () { }) }) - describe('Send a custom token from TokenFactory', () => { + describe('Send a custom token from dapp', () => { let gasModal it('sends an already created token', async () => { - openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`) - - const [extension] = await driver.getAllWindowHandles() - - const [ - transferToAddress, - transferToAmount, - ] = await findElements(driver, By.css('.form-control')) + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) - await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') - await transferToAmount.sendKeys('26') + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) - const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`)) - await transferAmountButton.click() - await delay(regularDelayMs) + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`)) + await transferTokens.click() - const [,, popup] = await driver.getAllWindowHandles() - await driver.switchTo().window(popup) - await driver.close() + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) - await delay(regularDelayMs) + await delay(largeDelayMs) - const [txListItem] = await findElements(driver, By.css('.tx-list-item')) - await txListItem.click() + await findElements(driver, By.css('.tx-list-pending-item-container')) + const [txListValue] = await findElements(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000) + await txListValue.click() await delay(regularDelayMs) // Set the gas limit - const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button'))) + const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')), 10000) await configureGas.click() await delay(regularDelayMs) @@ -747,7 +815,7 @@ describe('MetaMask', function () { }) it('customizes gas', async () => { - await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title'))) + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) await gasPriceInput.clear() @@ -766,12 +834,12 @@ describe('MetaMask', function () { await gasLimitInput.sendKeys(Key.BACK_SPACE) } - const save = await findElement(driver, By.css('.send-v2__customize-gas__save')) + const save = await findElement(driver, By.css('.customize-gas__save')) await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInput = await findElement(driver, By.css('.currency-display__input')) - assert.equal(await gasFeeInput.getAttribute('value'), 0.0006) + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) + assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') }) it('submits the transaction', async function () { @@ -785,7 +853,7 @@ describe('MetaMask', function () { assert.equal(transactions.length, 2) const txValues = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/)) + await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/)) const txStatuses = await findElements(driver, By.css('.tx-list-status')) await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) @@ -799,11 +867,110 @@ describe('MetaMask', function () { // or possibly until we use latest version of firefox in the tests if (process.env.SELENIUM_BROWSER !== 'firefox') { const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount')) - assert.equal(await tokenBalanceAmount.getText(), '24') + assert.equal(await tokenBalanceAmount.getText(), '43') } }) }) + describe('Approves a custom token from dapp', () => { + let gasModal + it('approves an already created token', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) + + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) + + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) + await transferTokens.click() + + await closeAllWindowHandlesExcept(driver, extension) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + const [txListItem] = await findElements(driver, By.css('.tx-list-item')) + const [txListValue] = await findElements(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txListValue, /0\sETH/)) + await txListItem.click() + await delay(regularDelayMs) + }) + + it('displays the token approval data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await (regularDelayMs) + + const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionTypeText = await functionType.getText() + assert.equal(functionTypeText, 'Approve') + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/)) + + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await (regularDelayMs) + + const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) + const approvalWarningText = await approvalWarning.getText() + assert(approvalWarningText.match(/By approving this/)) + await (regularDelayMs) + }) + + it('opens the gas edit modal', async () => { + const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit'))) + await configureGas.click() + await delay(regularDelayMs) + + gasModal = await driver.findElement(By.css('span .modal')) + }) + + it('customizes gas', async () => { + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) + + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) + await gasPriceInput.clear() + await delay(tinyDelayMs) + await gasPriceInput.sendKeys('10') + await delay(tinyDelayMs) + await gasLimitInput.clear() + await delay(tinyDelayMs) + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await gasLimitInput.sendKeys('60000') + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) + + // Needed for different behaviour of input in different versions of firefox + const gasLimitInputValue = await gasLimitInput.getAttribute('value') + if (gasLimitInputValue === '600001') { + await gasLimitInput.sendKeys(Key.BACK_SPACE) + } + + const save = await findElement(driver, By.css('.customize-gas__save')) + await save.click() + await driver.wait(until.stalenessOf(gasModal)) + + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) + assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + }) + + it('submits the transaction', async function () { + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + const txValues = await findElements(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/)) + const txStatuses = await findElements(driver, By.css('.tx-list-status')) + await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + }) + }) + describe('Hide token', () => { it('hides the token when clicked', async () => { const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis')) diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js index a32b924b8..b6efae5b3 100644 --- a/test/e2e/metamask.spec.js +++ b/test/e2e/metamask.spec.js @@ -234,7 +234,7 @@ describe('Metamask popup page', function () { submitButton.click() - await delay(500) + await delay(1500) }) it('finds the transaction in the transactions list', async function () { diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index 5613c0dcb..dcc25c493 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -1,5 +1,6 @@ const reactTriggerChange = require('react-trigger-change') const { + timeout, queryAsync, } = require('../../lib/util') @@ -18,14 +19,14 @@ async function runConfirmSigRequestsTest (assert, done) { selectState.val('confirm sig requests') reactTriggerChange(selectState[0]) - // await timeout(1000000) - const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable') if (pendingRequestItem[0]) { pendingRequestItem[0].click() } + await timeout(1000) + let confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') @@ -37,7 +38,7 @@ async function runConfirmSigRequestsTest (assert, done) { let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - + await timeout(1000) confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') @@ -46,7 +47,7 @@ async function runConfirmSigRequestsTest (assert, done) { confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - + await timeout(1000) confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') @@ -57,6 +58,5 @@ async function runConfirmSigRequestsTest (assert, done) { confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - const txView = await queryAsync($, '.tx-view') - assert.ok(txView[0], 'Should return to the account details screen after confirming') + await timeout(2000) } diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js index 3ad1a23e5..d42b7495d 100644 --- a/test/integration/lib/currency-localization.js +++ b/test/integration/lib/currency-localization.js @@ -19,6 +19,7 @@ async function runCurrencyLocalizationTest (assert, done) { console.log('*** start runCurrencyLocalizationTest') const selectState = await queryAsync($, 'select') selectState.val('currency localization') + await timeout(1000) reactTriggerChange(selectState[0]) await timeout(1000) const txView = await queryAsync($, '.tx-view') diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index d5e80151c..406863ca6 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -125,18 +125,18 @@ async function runSendFlowTest (assert, done) { reactTriggerChange(selectState[0]) const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first() - assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name') + assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name') const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last() assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') - const confirmScreenRows = await queryAsync($, '.confirm-screen-rows') - const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0] - assert.equal(confirmScreenGas.textContent, '$3.60 USD', 'confirm screen should show correct gas') - const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2] - assert.equal(confirmScreenTotal.textContent, '$2,405.36 USD', 'confirm screen should show correct total') + const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat') + const confirmScreenGas = confirmScreenRowFiats[0] + assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas') + const confirmScreenTotal = confirmScreenRowFiats[1] + assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total') - const confirmScreenBackButton = await queryAsync($, '.page-container__back-button') + const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button') confirmScreenBackButton[0].click() const sendFromFieldItemInEdit = await queryAsync($, '.account-list-item') diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index c110f71fc..160cd4552 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -1,74 +1,54 @@ -// var jsdom = require('mocha-jsdom') var assert = require('assert') -var freeze = require('deep-freeze-strict') var path = require('path') -var sinon = require('sinon') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' -describe('tx confirmation screen', function () { - beforeEach(function () { - this.sinon = sinon.createSandbox() - }) - - afterEach(function () { - this.sinon.restore() - }) +const actions = require(path.join(__dirname, '../../../ui/app/actions.js')) - var initialState, result +const middlewares = [thunk] +const mockStore = configureMockStore(middlewares) - describe('when there is only one tx', function () { - var firstTxId = 1457634084250832 - - beforeEach(function () { - initialState = { - appState: { - currentView: { - name: 'confTx', - }, - }, - metamask: { - unapprovedTxs: { - '1457634084250832': { - id: 1457634084250832, - status: 'unconfirmed', - time: 1457634084250, - }, - }, +describe('tx confirmation screen', function () { + const txId = 1457634084250832 + const initialState = { + appState: { + currentView: { + name: 'confTx', + }, + }, + metamask: { + unapprovedTxs: { + [txId]: { + id: txId, + status: 'unconfirmed', + time: 1457634084250, }, - } - freeze(initialState) + }, + }, + } + + const store = mockStore(initialState) + + describe('cancelTx', function () { + before(function (done) { + actions._setBackgroundConnection({ + approveTransaction (txId, cb) { cb('An error!') }, + cancelTransaction (txId, cb) { cb() }, + clearSeedWordCache (cb) { cb() }, + getState (cb) { cb() }, + }) + done() }) - describe('cancelTx', function () { - before(function (done) { - actions._setBackgroundConnection({ - approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId, cb) { cb() }, - clearSeedWordCache (cb) { cb() }, + it('creates COMPLETED_TX with the cancelled transaction ID', function (done) { + store.dispatch(actions.cancelTx({ id: txId })) + .then(() => { + const storeActions = store.getActions() + const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX) + assert.equal(completedTxAction.value, txId) + done() }) - - actions.cancelTx({value: firstTxId})((action) => { - result = reducers(initialState, action) - }) - done() - }) - - it('should transition to the account detail view', function () { - assert.equal(result.appState.currentView.name, 'accountDetail') - }) - - it('should have no unconfirmed txs remaining', function () { - var count = getUnconfirmedTxCount(result) - assert.equal(count, 0) - }) }) }) }) - -function getUnconfirmedTxCount (state) { - var txs = state.metamask.unapprovedTxs - var count = Object.keys(txs).length - return count -} diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index d4c5ff0ec..26dc7b656 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -353,9 +353,16 @@ describe('Transaction Controller', function () { ]) }) - it('should set the transaction to rejected from unapproved', async function () { - await txController.cancelTransaction(0) - assert.equal(txController.txStateManager.getTx(0).status, 'rejected') + it('should emit a status change to rejected', function (done) { + txController.once('tx:status-update', (txId, status) => { + try { + assert.equal(status, 'rejected', 'status should e rejected') + assert.equal(txId, 0, 'id should e 0') + done() + } catch (e) { done(e) } + }) + + txController.cancelTransaction(0) }) }) diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js index 2f91b1545..88bdaa60e 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager-test.js +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -43,14 +43,13 @@ describe('TransactionStateManager', function () { }) describe('#setTxStatusRejected', function () { - it('sets the tx status to rejected', function () { + it('sets the tx status to rejected and removes it from history', function () { const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx) txStateManager.setTxStatusRejected(1) const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') + assert.equal(result.length, 0) }) it('should emit a rejected event to signal the exciton of callback', (done) => { @@ -287,4 +286,18 @@ describe('TransactionStateManager', function () { }) }) + + describe('#_removeTx', function () { + it('should remove the transaction from the storage', () => { + txStateManager._saveTxList([ {id: 1} ]) + txStateManager._removeTx(1) + assert(!txStateManager.getFullTxList().length, 'txList should be empty') + }) + + it('should only remove the transaction with ID 1 from the storage', () => { + txStateManager._saveTxList([ {id: 1}, {id: 2} ]) + txStateManager._removeTx(1) + assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2') + }) + }) }) diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js new file mode 100644 index 000000000..3ec9f0c0e --- /dev/null +++ b/test/unit/migrations/027-test.js @@ -0,0 +1,50 @@ +const assert = require('assert') +const migration27 = require('../../../app/scripts/migrations/027') + +const oldStorage = { + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], + }, + }, +} + +const transactions = [] + + +while (transactions.length < 9) { + transactions.push({status: 'rejected'}) + transactions.push({status: 'unapproved'}) + transactions.push({status: 'approved'}) +} + + +oldStorage.data.TransactionController.transactions = transactions + +describe('migration #27', () => { + it('should remove rejected transactions', (done) => { + migration27.migrate(oldStorage) + .then((newStorage) => { + const newTransactions = newStorage.data.TransactionController.transactions + assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6') + newTransactions.forEach((txMeta) => { + if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected')) + }) + done() + }) + .catch(done) + }) + + it('should successfully migrate first time state', (done) => { + migration27.migrate({ + meta: {}, + data: require('../../../app/scripts/first-time-state'), + }) + .then((migratedData) => { + assert.equal(migratedData.meta.version, migration27.version) + done() + }).catch(done) + }) +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 9330a864b..4b2c53b96 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -811,11 +811,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({})) } @@ -1017,29 +1016,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()) @@ -1050,10 +1061,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 + }) } } @@ -1145,13 +1163,25 @@ function cancelTypedMsg (msgData) { function cancelTx (txData) { return dispatch => { log.debug(`background.cancelTransaction`) + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { - background.cancelTransaction(txData.id, () => { + background.cancelTransaction(txData.id, err => { + if (err) { + return reject(err) + } + + resolve() + }) + }) + .then(() => updateMetamaskStateFromBackground()) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => { dispatch(actions.clearSend()) dispatch(actions.completedTx(txData.id)) - resolve(txData) + dispatch(actions.hideLoadingIndication()) + return txData }) - }) } } diff --git a/ui/app/app.js b/ui/app/app.js index 670b7e2d0..74d360d3c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -12,7 +12,7 @@ const log = require('loglevel') const InitializeScreen = require('../../mascara/src/app/first-time').default // accounts const SendTransactionScreen = require('./components/send_/send.container') -const ConfirmTxScreen = require('./conf-tx') +const ConfirmTransaction = require('./components/pages/confirm-transaction') // slideout menu const WalletView = require('./components/wallet-view') @@ -22,7 +22,6 @@ const Home = require('./components/pages/home') const Authenticated = require('./components/pages/authenticated') const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') -const UnlockPage = require('./components/pages/unlock-page') const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') @@ -40,6 +39,8 @@ const Modal = require('./components/modals/index').Modal const AppHeader = require('./components/app-header') +import UnlockPage from './components/pages/unlock-page' + // Routes const { DEFAULT_ROUTE, @@ -76,7 +77,10 @@ class App extends Component { h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), - h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), + h(Authenticated, { + path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, + component: ConfirmTransaction, + }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 62b04562a..07ca6cf84 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -91,7 +91,6 @@ class AppHeader extends Component { network, provider, history, - location, isUnlocked, } = this.props @@ -126,7 +125,7 @@ class AppHeader extends Component { network={network} provider={provider} onClick={event => this.handleNetworkIndicatorClick(event)} - disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE} + disabled={this.isConfirming()} /> { this.renderAccountMenu() } diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js index e8e798445..1e0ef1b64 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/button/button.component.js @@ -5,15 +5,24 @@ import classnames from 'classnames' const CLASSNAME_DEFAULT = 'btn-default' const CLASSNAME_PRIMARY = 'btn-primary' const CLASSNAME_SECONDARY = 'btn-secondary' +const CLASSNAME_CONFIRM = 'btn-confirm' const CLASSNAME_LARGE = 'btn--large' const typeHash = { default: CLASSNAME_DEFAULT, primary: CLASSNAME_PRIMARY, secondary: CLASSNAME_SECONDARY, + confirm: CLASSNAME_CONFIRM, } -class Button extends Component { +export default class Button extends Component { + static propTypes = { + type: PropTypes.string, + large: PropTypes.bool, + className: PropTypes.string, + children: PropTypes.string, + } + render () { const { type, large, className, ...buttonProps } = this.props @@ -31,13 +40,3 @@ class Button extends Component { ) } } - -Button.propTypes = { - type: PropTypes.string, - large: PropTypes.bool, - className: PropTypes.string, - children: PropTypes.string, -} - -export default Button - diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js new file mode 100644 index 000000000..631cf5803 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +const ConfirmDetailRow = props => { + const { + label, + fiatFee, + ethFee, + onHeaderClick, + fiatFeeColor, + headerText, + headerTextClassName, + } = props + + return ( +
+
+ { label } +
+
+
onHeaderClick && onHeaderClick()} + > + { headerText } +
+
+ { fiatFee } +
+
+ { `\u2666 ${ethFee}` } +
+
+
+ ) +} + +ConfirmDetailRow.propTypes = { + label: PropTypes.string, + fiatFee: PropTypes.string, + ethFee: PropTypes.string, + fiatFeeColor: PropTypes.string, + onHeaderClick: PropTypes.func, + headerText: PropTypes.string, + headerTextClassName: PropTypes.string, +} + +export default ConfirmDetailRow diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/confirm-page-container/confirm-detail-row/index.js new file mode 100644 index 000000000..056afff04 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.js @@ -0,0 +1 @@ +export { default } from './confirm-detail-row.component' diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss new file mode 100644 index 000000000..84d0d56ed --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -0,0 +1,43 @@ +.confirm-detail-row { + padding: 14px 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + &__label { + font-size: .75rem; + font-weight: 500; + color: $scorpion; + text-transform: uppercase; + } + + &__details { + flex: 1; + text-align: end; + } + + &__fiat { + font-size: 1.5rem; + } + + &__eth { + color: $oslo-gray; + } + + &__header-text { + font-size: .75rem; + text-transform: uppercase; + margin-bottom: 6px; + color: $scorpion; + + &--edit { + color: $curious-blue; + cursor: pointer; + } + + &--total { + font-size: .625rem; + } + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js new file mode 100644 index 000000000..08923af88 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { Tabs, Tab } from '../../tabs' +import { + ConfirmPageContainerSummary, + ConfirmPageContainerError, + ConfirmPageContainerWarning, +} from './' + +export default class ConfirmPageContainerContent extends Component { + static propTypes = { + action: PropTypes.string, + dataComponent: PropTypes.node, + detailsComponent: PropTypes.node, + errorKey: PropTypes.string, + errorMessage: PropTypes.string, + hideSubtitle: PropTypes.bool, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, + subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + summaryComponent: PropTypes.node, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + titleComponent: PropTypes.func, + warning: PropTypes.string, + } + + renderContent () { + const { detailsComponent, dataComponent } = this.props + + if (detailsComponent && dataComponent) { + return this.renderTabs() + } else { + return detailsComponent || dataComponent + } + } + + renderTabs () { + const { detailsComponent, dataComponent } = this.props + + return ( + + + { detailsComponent } + + + { dataComponent } + + + ) + } + + render () { + const { + action, + errorKey, + errorMessage, + title, + subtitle, + hideSubtitle, + identiconAddress, + nonce, + summaryComponent, + detailsComponent, + dataComponent, + warning, + } = this.props + + return ( +
+ { + warning && ( + + ) + } + { + summaryComponent || ( + + ) + } + { this.renderContent() } + { + (errorKey || errorMessage) && ( +
+ +
+ ) + } +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js new file mode 100644 index 000000000..70ebdeb20 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerError = (props, context) => { + const { errorMessage, errorKey } = props + const error = errorKey ? context.t(errorKey) : errorMessage + + return ( +
+ + { `ALERT: ${error}` } +
+ ) +} + +ConfirmPageContainerError.propTypes = { + errorMessage: PropTypes.string, + errorKey: PropTypes.string, +} + +ConfirmPageContainerError.contextTypes = { + t: PropTypes.func, +} + +export default ConfirmPageContainerError diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js new file mode 100644 index 000000000..4ac95d0e3 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-error.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss new file mode 100644 index 000000000..e99b0f631 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss @@ -0,0 +1,17 @@ +.confirm-page-container-error { + height: 32px; + border: 1px solid $monzo; + color: $monzo; + background: lighten($monzo, 56%); + border-radius: 4px; + font-size: .75rem; + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 16px; + + &__icon { + margin-right: 8px; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js new file mode 100644 index 000000000..3b1ee62c5 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -0,0 +1,56 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../identicon' + +const ConfirmPageContainerSummary = props => { + const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props + + return ( +
+
+
+ { action } +
+ { + nonce && ( +
+ { `#${nonce}` } +
+ ) + } +
+
+ { + identiconAddress && ( + + ) + } +
+ { title } +
+
+ { + hideSubtitle ||
+ { subtitle } +
+ } +
+ ) +} + +ConfirmPageContainerSummary.propTypes = { + action: PropTypes.string, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hideSubtitle: PropTypes.bool, + className: PropTypes.string, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, +} + +export default ConfirmPageContainerSummary diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js new file mode 100644 index 000000000..ed1b28cf2 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-summary.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss new file mode 100644 index 000000000..7f0f5d37a --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -0,0 +1,54 @@ +.confirm-page-container-summary { + padding: 16px 24px 0; + background-color: #f9fafa; + height: 133px; + box-sizing: border-box; + + &__action-row { + display: flex; + justify-content: space-between; + } + + &__action { + text-transform: uppercase; + color: $oslo-gray; + font-size: .75rem; + padding: 3px 8px; + border: 1px solid $oslo-gray; + border-radius: 4px; + display: inline-block; + } + + &__nonce { + color: $oslo-gray; + } + + &__title { + padding: 4px 0; + display: flex; + align-items: center; + } + + &__identicon { + flex: 0 0 auto; + margin-right: 8px; + } + + &__title-text { + font-size: 2.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__subtitle { + color: $oslo-gray; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &--border { + border-bottom: 1px solid $geyser; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js new file mode 100644 index 000000000..79901c8fc --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerWarning = props => { + return ( +
+ +
+ { props.warning } +
+
+ ) +} + +ConfirmPageContainerWarning.propTypes = { + warning: PropTypes.string, +} + +export default ConfirmPageContainerWarning diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js new file mode 100644 index 000000000..6e48bd144 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-warning.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss new file mode 100644 index 000000000..50545a1a2 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss @@ -0,0 +1,18 @@ +.confirm-page-container-warning { + background-color: #fffcdb; + display: flex; + justify-content: center; + align-items: center; + border-bottom: 1px solid $geyser; + padding: 12px 24px; + + &__icon { + flex: 0 0 auto; + margin-right: 16px; + } + + &__warning { + font-size: .75rem; + color: #5f5922; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js new file mode 100644 index 000000000..1469dd438 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js @@ -0,0 +1,4 @@ +export { default } from './confirm-page-container-content.component' +export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary' +export { default as ConfirmPageContainerError } from './confirm-page-container-error' +export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss new file mode 100644 index 000000000..39797a43f --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss @@ -0,0 +1,66 @@ +@import './confirm-page-container-error/index'; + +@import './confirm-page-container-warning/index'; + +@import './confirm-page-container-summary/index'; + +.confirm-page-container-content { + overflow-y: auto; + flex: 1; + + &__error-container { + padding: 0 16px 16px 16px; + } + + &__details { + box-sizing: border-box; + padding: 0 24px; + } + + &__data { + padding: 16px; + color: $oslo-gray; + } + + &__data-box { + background-color: #f9fafa; + padding: 12px; + font-size: .75rem; + margin-bottom: 16px; + word-wrap: break-word; + max-height: 200px; + overflow-y: auto; + + &-label { + text-transform: uppercase; + padding: 8px 0 12px; + font-size: 12px; + } + } + + &__data-field { + display: flex; + flex-direction: row; + + &-label { + font-weight: 500; + padding-right: 16px; + } + + &:not(:last-child) { + margin-bottom: 5px; + } + } + + &__gas-fee { + border-bottom: 1px solid $geyser; + } + + &__function-type { + font-size: .875rem; + font-weight: 500; + text-transform: capitalize; + color: $black; + padding-left: 5px; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js new file mode 100644 index 000000000..e6fe8f82c --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, +} from '../../../../../app/scripts/lib/enums' +import NetworkDisplay from '../../network-display' + +export default class ConfirmPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + showEdit: PropTypes.bool, + onEdit: PropTypes.func, + children: PropTypes.node, + } + + renderTop () { + const { onEdit, showEdit } = this.props + const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP + + if (!showEdit && isFullScreen) { + return null + } + + return ( +
+
+ + onEdit()} + > + { this.context.t('edit') } + +
+ { !isFullScreen && } +
+ ) + } + + render () { + const { children } = this.props + + return ( +
+ { this.renderTop() } + { children } +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js new file mode 100644 index 000000000..71feb6931 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-header.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss new file mode 100644 index 000000000..43e1e4427 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss @@ -0,0 +1,27 @@ +.confirm-page-container-header { + display: flex; + flex-direction: column; + flex: 0 0 auto; + + &__row { + display: flex; + justify-content: space-between; + border-bottom: 1px solid $geyser; + padding: 13px 13px 13px 24px; + flex: 0 0 auto; + } + + &__back-button-container { + display: flex; + justify-content: center; + align-items: center; + } + + &__back-button { + color: #2f9ae0; + font-size: 1rem; + cursor: pointer; + font-weight: 400; + padding-left: 5px; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js new file mode 100644 index 000000000..93e4ae7bf --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SenderToRecipient from '../sender-to-recipient' +import { PageContainerFooter } from '../page-container' +import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './' + +export default class ConfirmPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + // Header + action: PropTypes.string, + hideSubtitle: PropTypes.bool, + onEdit: PropTypes.func, + showEdit: PropTypes.bool, + subtitle: PropTypes.string, + title: PropTypes.string, + titleComponent: PropTypes.func, + // Sender to Recipient + fromAddress: PropTypes.string, + fromName: PropTypes.string, + toAddress: PropTypes.string, + toName: PropTypes.string, + // Content + contentComponent: PropTypes.node, + errorKey: PropTypes.string, + errorMessage: PropTypes.string, + fiatTransactionAmount: PropTypes.string, + fiatTransactionFee: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionAmount: PropTypes.string, + ethTransactionFee: PropTypes.string, + ethTransactionTotal: PropTypes.string, + onEditGas: PropTypes.func, + dataComponent: PropTypes.node, + detailsComponent: PropTypes.node, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, + summaryComponent: PropTypes.node, + warning: PropTypes.string, + // Footer + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + valid: PropTypes.bool, + } + + render () { + const { + showEdit, + onEdit, + fromName, + fromAddress, + toName, + toAddress, + valid, + errorKey, + errorMessage, + contentComponent, + action, + title, + titleComponent, + subtitle, + hideSubtitle, + summaryComponent, + detailsComponent, + dataComponent, + onCancel, + onSubmit, + identiconAddress, + nonce, + warning, + } = this.props + + return ( +
+ onEdit()} + > + + + { + contentComponent || ( + + ) + } + onCancel()} + onSubmit={() => onSubmit()} + submitText={this.context.t('confirm')} + submitButtonType="confirm" + disabled={!valid} + /> +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/index.js b/ui/app/components/confirm-page-container/index.js new file mode 100644 index 000000000..ee88aa5d3 --- /dev/null +++ b/ui/app/components/confirm-page-container/index.js @@ -0,0 +1,8 @@ +export { default } from './confirm-page-container.component' +export { default as ConfirmPageContainerHeader } from './confirm-page-container-header' +export { default as ConfirmDetailRow } from './confirm-detail-row' +export { + default as ConfirmPageContainerContent, + ConfirmPageContainerSummary, + ConfirmPageContainerError, +} from './confirm-page-container-content' diff --git a/ui/app/components/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss new file mode 100644 index 000000000..af7a5b555 --- /dev/null +++ b/ui/app/components/confirm-page-container/index.scss @@ -0,0 +1,5 @@ +@import './confirm-page-container-content/index'; + +@import './confirm-page-container-header/index'; + +@import './confirm-detail-row/index'; diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js index 7e94e0af5..a45da4c10 100644 --- a/ui/app/components/dropdowns/components/network-dropdown-icon.js +++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js @@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () { backgroundColor, isSelected, innerBorder = 'none', + diameter = '12', } = this.props return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, @@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () { style: { background: backgroundColor, border: innerBorder, + height: `${diameter}px`, + width: `${diameter}px`, }, }) ) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 351640f6e..32f0e90e4 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -4,6 +4,16 @@ @import './info-box/index'; +@import './network-display/index'; + +@import './confirm-page-container/index'; + +@import './page-container/index'; + @import './pages/index'; @import './modals/index'; + +@import './sender-to-recipient/index'; + +@import './tabs/index'; diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js new file mode 100644 index 000000000..d17c290b6 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.component.js @@ -0,0 +1,140 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import GasModalCard from '../../customize-gas-modal/gas-modal-card' +import { MIN_GAS_PRICE_GWEI } from '../../send_/send.constants' + +import { + getDecimalGasLimit, + getDecimalGasPrice, + getPrefixedHexGasLimit, + getPrefixedHexGasPrice, +} from './customize-gas.util' + +export default class CustomizeGas extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + txData: PropTypes.object.isRequired, + hideModal: PropTypes.func, + validate: PropTypes.func, + onSubmit: PropTypes.func, + } + + state = { + gasPrice: 0, + gasLimit: 0, + originalGasPrice: 0, + originalGasLimit: 0, + } + + componentDidMount () { + const { txData = {} } = this.props + const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData + + const gasLimit = getDecimalGasLimit(hexGasLimit) + const gasPrice = getDecimalGasPrice(hexGasPrice) + + this.setState({ + gasPrice, + gasLimit, + originalGasPrice: gasPrice, + originalGasLimit: gasLimit, + }) + } + + handleRevert () { + const { originalGasPrice, originalGasLimit } = this.state + + this.setState({ + gasPrice: originalGasPrice, + gasLimit: originalGasLimit, + }) + } + + handleSave () { + const { onSubmit, hideModal } = this.props + const { gasLimit, gasPrice } = this.state + const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice) + const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit) + + Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit })) + .then(() => hideModal()) + } + + validate () { + const { gasLimit, gasPrice } = this.state + return this.props.validate({ + gasPrice: getPrefixedHexGasPrice(gasPrice), + gasLimit: getPrefixedHexGasLimit(gasLimit), + }) + } + + render () { + const { t } = this.context + const { hideModal } = this.props + const { gasPrice, gasLimit } = this.state + const { valid, errorKey } = this.validate() + + return ( +
+
+
+
+ { this.context.t('customGas') } +
+
hideModal()} + /> +
+
+ this.setState({ gasPrice: value })} + title={t('gasPrice')} + copy={t('gasPriceCalculation')} + /> + this.setState({ gasLimit: value })} + title={t('gasLimit')} + copy={t('gasLimitCalculation')} + /> +
+
+ { !valid &&
{ t(errorKey) }
} +
this.handleRevert()} + > + { t('revert') } +
+
+ + +
+
+
+
+ ) + } +} diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/modals/customize-gas/customize-gas.container.js new file mode 100644 index 000000000..46a799795 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.container.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import CustomizeGas from './customize-gas.component' +import { hideModal } from '../../../actions' + +const mapStateToProps = state => { + const { appState: { modal: { modalState: { props } } } } = state + const { txData, onSubmit, validate } = props + + return { + txData, + onSubmit, + validate, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas) diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/modals/customize-gas/customize-gas.util.js new file mode 100644 index 000000000..6ba4a7705 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.util.js @@ -0,0 +1,34 @@ +import ethUtil from 'ethereumjs-util' +import { conversionUtil } from '../../../conversion-util' + +export function getDecimalGasLimit (hexGasLimit) { + return conversionUtil(hexGasLimit, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) +} + +export function getDecimalGasPrice (hexGasPrice) { + return conversionUtil(hexGasPrice, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }) +} + +export function getPrefixedHexGasLimit (gasLimit) { + return ethUtil.addHexPrefix(conversionUtil(gasLimit, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + })) +} + +export function getPrefixedHexGasPrice (gasPrice) { + return ethUtil.addHexPrefix(conversionUtil(gasPrice, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + })) +} diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/modals/customize-gas/index.js new file mode 100644 index 000000000..3a0ab7edc --- /dev/null +++ b/ui/app/components/modals/customize-gas/index.js @@ -0,0 +1 @@ +export { default } from './customize-gas.container' diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/modals/customize-gas/index.scss new file mode 100644 index 000000000..e10452691 --- /dev/null +++ b/ui/app/components/modals/customize-gas/index.scss @@ -0,0 +1,110 @@ +.customize-gas { + border: 1px solid #D8D8D8; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); + font-family: Roboto; + display: flex; + flex-flow: column; + + @media screen and (max-width: $break-small) { + width: 100vw; + height: 100vh; + } + + &__header { + height: 52px; + border-bottom: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__title { + margin-left: 19.25px; + } + + &__close::after { + content: '\00D7'; + font-size: 1.8em; + color: $dusty-gray; + font-family: sans-serif; + cursor: pointer; + margin-right: 19.25px; + } + + &__content { + display: flex; + flex-flow: column nowrap; + height: 100%; + } + + &__body { + display: flex; + margin-bottom: 24px; + + @media screen and (max-width: $break-small) { + flex-flow: column; + flex: 1 1 auto; + } + } + + &__footer { + height: 75px; + border-top: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + position: relative; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__buttons { + display: flex; + justify-content: space-between; + margin-right: 21.25px; + } + + &__revert, &__cancel, &__save, &__save__error { + display: flex; + justify-content: center; + align-items: center; + padding: 0 3px; + cursor: pointer; + } + + &__revert { + color: $silver-chalice; + font-size: 16px; + margin-left: 21.25px; + } + + &__cancel, &__save, &__save__error { + width: 85.74px; + min-width: initial; + } + + &__save__error { + opacity: 0.5; + cursor: auto; + } + + &__error-message { + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + line-height: 12px; + color: $red; + } +} diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index 591e35148..b7f1f249d 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -1,3 +1,5 @@ +@import './customize-gas/index'; + .modal-container { width: 100%; height: 100%; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index e40944165..f59825ed1 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -25,6 +25,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', @@ -281,7 +283,31 @@ const MODALS = { CUSTOMIZE_GAS: { contents: [ - h(CustomizeGasModal, {}, []), + h(CustomizeGasModal), + ], + mobileModalStyle: { + width: '100vw', + height: '100vh', + top: '0', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: '720px', + height: '377px', + top: '80px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + }, + + CONFIRM_CUSTOMIZE_GAS: { + contents: [ + h(ConfirmCustomizeGasModal), ], mobileModalStyle: { width: '100vw', diff --git a/ui/app/components/network-display.js b/ui/app/components/network-display.js deleted file mode 100644 index 59719d9a4..000000000 --- a/ui/app/components/network-display.js +++ /dev/null @@ -1,56 +0,0 @@ -const { Component } = require('react') -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon') - -const networkToColorHash = { - 1: '#038789', - 3: '#e91550', - 42: '#690496', - 4: '#ebb33f', -} - -class NetworkDisplay extends Component { - renderNetworkIcon () { - const { network } = this.props - const networkColor = networkToColorHash[network] - - return networkColor - ? h(NetworkDropdownIcon, { backgroundColor: networkColor }) - : h('i.fa.fa-question-circle.fa-med', { - style: { - margin: '0 4px', - color: 'rgb(125, 128, 130)', - }, - }) - } - - render () { - const { provider: { type } } = this.props - return h('.network-display__container', [ - this.renderNetworkIcon(), - h('.network-name', this.context.t(type)), - ]) - } -} - -NetworkDisplay.propTypes = { - network: PropTypes.string, - provider: PropTypes.object, - t: PropTypes.func, -} - -const mapStateToProps = ({ metamask: { network, provider } }) => { - return { - network, - provider, - } -} - -NetworkDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(NetworkDisplay) - diff --git a/ui/app/components/network-display/index.js b/ui/app/components/network-display/index.js new file mode 100644 index 000000000..f6878ae5b --- /dev/null +++ b/ui/app/components/network-display/index.js @@ -0,0 +1,2 @@ +import NetworkDisplay from './network-display.container' +module.exports = NetworkDisplay diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss new file mode 100644 index 000000000..e82d0e70c --- /dev/null +++ b/ui/app/components/network-display/index.scss @@ -0,0 +1,54 @@ +.network-display { + &__container { + display: flex; + align-items: center; + justify-content: flex-start; + background-color: lighten(rgb(125, 128, 130), 45%); + padding: 0 10px; + border-radius: 4px; + height: 25px; + + &--mainnet { + background-color: lighten($blue-lagoon, 45%); + } + + &--ropsten { + background-color: lighten($crimson, 45%); + } + + &--kovan { + background-color: lighten($purple, 45%); + } + + &--rinkeby { + background-color: lighten($tulip-tree, 45%); + } + } + + &__name { + font-size: .75rem; + padding-left: 5px; + } + + &__icon { + height: 10px; + width: 10px; + border-radius: 10px; + + &--mainnet { + background-color: $blue-lagoon; + } + + &--ropsten { + background-color: $crimson; + } + + &--kovan { + background-color: $purple; + } + + &--rinkeby { + background-color: $tulip-tree; + } + } +} diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js new file mode 100644 index 000000000..38626af20 --- /dev/null +++ b/ui/app/components/network-display/network-display.component.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { + MAINNET_CODE, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, +} from '../../../../app/scripts/controllers/network/enums' + +const networkToClassHash = { + [MAINNET_CODE]: 'mainnet', + [ROPSTEN_CODE]: 'ropsten', + [RINKEYBY_CODE]: 'rinkeby', + [KOVAN_CODE]: 'kovan', +} + +export default class NetworkDisplay extends Component { + static propTypes = { + network: PropTypes.string, + provider: PropTypes.object, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderNetworkIcon () { + const { network } = this.props + const networkClass = networkToClassHash[network] + + return networkClass + ?
+ :
+ } + + render () { + const { network, provider: { type } } = this.props + const networkClass = networkToClassHash[network] + + return ( +
+ { + networkClass + ?
+ :
+ } +
+ { this.context.t(type) } +
+
+ ) + } +} diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/network-display/network-display.container.js new file mode 100644 index 000000000..99a14fff4 --- /dev/null +++ b/ui/app/components/network-display/network-display.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import NetworkDisplay from './network-display.component' + +const mapStateToProps = ({ metamask: { network, provider } }) => { + return { + network, + provider, + } +} + +export default connect(mapStateToProps)(NetworkDisplay) diff --git a/ui/app/components/page-container/index.js b/ui/app/components/page-container/index.js index 415870b37..913b8c9c6 100644 --- a/ui/app/components/page-container/index.js +++ b/ui/app/components/page-container/index.js @@ -1 +1,4 @@ +import PageContainerHeader from './page-container-header' +import PageContainerFooter from './page-container-footer' export { default } from './page-container.component' +export { PageContainerHeader, PageContainerFooter } diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss new file mode 100644 index 000000000..06c3ef709 --- /dev/null +++ b/ui/app/components/page-container/index.scss @@ -0,0 +1,186 @@ +.page-container { + width: 408px; + background-color: $white; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); + z-index: 25; + display: flex; + flex-flow: column; + border-radius: 8px; + + &__header { + display: flex; + flex-flow: column; + border-bottom: 1px solid $geyser; + padding: 16px; + flex: 0 0 auto; + position: relative; + + &--no-padding-bottom { + padding-bottom: 0; + } + } + + &__header-close { + color: $tundora; + position: absolute; + top: 16px; + right: 16px; + cursor: pointer; + overflow: hidden; + + &::after { + content: '\00D7'; + font-size: 40px; + line-height: 20px; + } + } + + &__header-row { + padding-bottom: 10px; + display: flex; + justify-content: space-between; + } + + &__footer { + display: flex; + flex-flow: row; + justify-content: center; + border-top: 1px solid $geyser; + padding: 16px; + flex: 0 0 auto; + + .btn-default, + .btn-confirm { + font-size: 1rem; + } + } + + &__footer-button { + height: 55px; + font-size: 1rem; + text-transform: uppercase; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + } + + &__back-button { + color: #2f9ae0; + font-size: 1rem; + cursor: pointer; + font-weight: 400; + } + + &__title { + color: $black; + font-size: 2rem; + font-weight: 500; + line-height: 2rem; + } + + &__subtitle { + padding-top: .5rem; + line-height: initial; + font-size: .9rem; + color: $gray; + } + + &__tabs { + display: flex; + margin-top: 16px; + } + + &__tab { + min-width: 5rem; + padding: 8px; + color: $dusty-gray; + font-family: Roboto; + font-size: 1rem; + text-align: center; + cursor: pointer; + border-bottom: none; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + + &--selected { + color: $curious-blue; + border-bottom: 3px solid $curious-blue; + } + } + + &--full-width { + width: 100% !important; + } + + &--full-height { + height: 100% !important; + max-height: initial !important; + min-height: initial !important; + } + + &__content { + overflow-y: auto; + flex: 1; + } + + &__warning-container { + background: $linen; + padding: 20px; + display: flex; + align-items: start; + } + + &__warning-message { + padding-left: 15px; + } + + &__warning-title { + font-weight: 500; + } + + &__warning-icon { + padding-top: 5px; + } +} + +@media screen and (max-width: 250px) { + .page-container { + &__footer { + flex-flow: column-reverse; + } + + &__footer-button { + width: 100%; + margin-bottom: 1rem; + margin-right: 0; + + &:first-of-type { + margin-bottom: 0; + } + } + } +} + +@media screen and (max-width: 575px) { + .page-container { + height: 100%; + width: 100%; + overflow-y: auto; + background-color: $white; + border-radius: 0; + flex: 1; + } +} + +@media screen and (min-width: 576px) { + .page-container { + max-height: 82vh; + min-height: 570px; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js index 0458ae78a..3d15df294 100644 --- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js @@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component { onSubmit: PropTypes.func, submitText: PropTypes.string, disabled: PropTypes.bool, + submitButtonType: PropTypes.string, } static contextTypes = { @@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component { onSubmit, submitText, disabled, + submitButtonType, } = this.props return ( @@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {