Merge branch 'i3725-refactor-send-component-' into i3914-fix-newui-send-gas-estimation

feature/default_network_editable
Dan 7 years ago
commit 701611e317
  1. 1
      .eslintrc
  2. 19
      CHANGELOG.md
  3. 14
      MISSION.md
  4. 8
      README.md
  5. 23
      app/_locales/en/messages.json
  6. 17
      app/images/check-icon.svg
  7. 5
      app/manifest.json
  8. 14
      app/scripts/background.js
  9. 8
      app/scripts/controllers/transactions/index.js
  10. 21
      app/scripts/controllers/transactions/tx-state-manager.js
  11. 24
      app/scripts/lib/cleanErrorStack.js
  12. 11
      app/scripts/lib/get-first-preferred-lang-code.js
  13. 35
      app/scripts/metamask-controller.js
  14. 2
      mascara/src/app/first-time/create-password-screen.js
  15. 2
      mascara/src/app/first-time/import-seed-phrase-screen.js
  16. 210
      package-lock.json
  17. 9
      package.json
  18. 406
      test/e2e/beta/from-import-beta-ui.spec.js
  19. 55
      test/e2e/beta/helpers.js
  20. 491
      test/e2e/beta/metamask-beta-ui.spec.js
  21. 10
      test/e2e/beta/run-all.sh
  22. 11
      test/e2e/func.js
  23. 7
      test/e2e/metamask.spec.js
  24. 10
      test/integration/lib/add-token.js
  25. 6
      test/integration/lib/confirm-sig-requests.js
  26. 12
      test/integration/lib/send-new-ui.js
  27. 12
      test/screens/new-ui.js
  28. 50
      ui/app/actions.js
  29. 3
      ui/app/app.js
  30. 23
      ui/app/components/button/button.component.js
  31. 19
      ui/app/components/button/button.stories.js
  32. 113
      ui/app/components/currency-input.js
  33. 2
      ui/app/components/customize-gas-modal/index.js
  34. 2
      ui/app/components/index.scss
  35. 8
      ui/app/components/input-number.js
  36. 6
      ui/app/components/loading-screen/loading-screen.component.js
  37. 54
      ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js
  38. 13
      ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js
  39. 2
      ui/app/components/modals/confirm-reset-account/index.js
  40. 2
      ui/app/components/modals/deposit-ether-modal.js
  41. 6
      ui/app/components/modals/export-private-key-modal.js
  42. 52
      ui/app/components/modals/index.scss
  43. 71
      ui/app/components/modals/modal.js
  44. 46
      ui/app/components/modals/notification-modals/confirm-reset-account.js
  45. 2
      ui/app/components/modals/notification/index.js
  46. 30
      ui/app/components/modals/notification/notification.component.js
  47. 38
      ui/app/components/modals/notification/notification.container.js
  48. 2
      ui/app/components/modals/transaction-confirmed/index.js
  49. 24
      ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js
  50. 2
      ui/app/components/modals/welcome-beta/index.js
  51. 23
      ui/app/components/modals/welcome-beta/welcome-beta.component.js
  52. 2
      ui/app/components/page-container/page-container-footer/page-container-footer.component.js
  53. 2
      ui/app/components/pages/add-token/add-token.component.js
  54. 4
      ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
  55. 2
      ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
  56. 4
      ui/app/components/pages/create-account/import-account/json.js
  57. 6
      ui/app/components/pages/create-account/import-account/private-key.js
  58. 4
      ui/app/components/pages/create-account/new-account.js
  59. 6
      ui/app/components/pages/keychains/reveal-seed.js
  60. 8
      ui/app/components/pages/settings/settings.js
  61. 20
      ui/app/components/pages/unlock-page/unlock-page.component.js
  62. 60
      ui/app/components/pending-tx/confirm-send-ether.js
  63. 57
      ui/app/components/pending-tx/confirm-send-token.js
  64. 5
      ui/app/components/pending-tx/index.js
  65. 57
      ui/app/components/send/currency-display.js
  66. 6
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
  67. 38
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
  68. 2
      ui/app/components/send_/tests/send-component.test.js
  69. 2
      ui/app/components/shapeshift-form.js
  70. 4
      ui/app/components/signature-request.js
  71. 95
      ui/app/components/text-field/text-field.component.js
  72. 29
      ui/app/components/text-field/text-field.stories.js
  73. 10
      ui/app/components/tx-list-item.js
  74. 63
      ui/app/conf-tx.js
  75. 49
      ui/app/css/itcss/components/buttons.scss
  76. 22
      ui/app/css/itcss/components/currency-display.scss
  77. 4
      ui/app/css/itcss/components/loading-overlay.scss
  78. 6
      ui/app/css/itcss/generic/index.scss
  79. 19
      ui/app/reducers/app.js

@ -142,6 +142,7 @@
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
"padded-blocks": "off", "padded-blocks": "off",
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"react/no-deprecated": 0,
"semi": [2, "never"], "semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }], "semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [1, "always"], "space-before-blocks": [1, "always"],

@ -2,6 +2,25 @@
## Current Master ## Current Master
- Adds error messages when passwords don't match in onboarding flow.
- Adds modal notification if a retry in the process of being confirmed is dropped.
- New unlock screen design.
- Design improvements to the add token screen.
- Fix inconsistencies in confirm screen between extension and browser window modes.
- Fix scrolling in deposit ether modal.
- Fix styling of app spinner.
- Font weight changed from 300 to 400.
- New reveal screen design.
- Styling improvements to labels in first time flow and signature request headers.
## 4.6.1 Mon Apr 30 2018
- Fix bug where sending a transaction resulted in an infinite spinner
- Allow transactions with a 0 gwei gas price
- Handle encoding errors in ERC20 symbol + digits
- Fix ShapeShift forms (new + old ui)
- Fix sourcemaps
## 4.6.0 Thu Apr 26 2018 ## 4.6.0 Thu Apr 26 2018
- Correctly format currency conversion for locally selected preferred currency. - Correctly format currency conversion for locally selected preferred currency.

@ -0,0 +1,14 @@
# MetaMask Philosophy
## Mission
Making it safe and easy for the most people to use the decentralized web to the greatest degree that is empowering to them.
## Vision
To realize the highest goals achievable for the human race with the twin powers of peer to peer networks and cryptography. To empower users to hold and use their own keys on these new networks as securely and intelligibly as possible, enabling a new world of peer to peer agreements and economies, in hopes that we may collectively overcome the many great problems that we face together, through the power of strong cooperation.
## Strategy
We provide software for users to manage accounts, for sites to easily propose actions to users, and for users to coherently review actions before approving them. We build on this rapidly evolving set of protocols with the goal of empowering the most people to the greatest degree, and aspire to continuously evolve our offering to pursue that goal.

@ -1,12 +1,16 @@
# MetaMask Browser Extension # MetaMask Browser Extension
[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension) [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension)
[Internal documentation](./docs/jsdocs)
## Support ## Support
If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/). If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/).
## Introduction
[Mission Statement](./MISSION.md)
[Internal documentation](./docs/jsdocs)
## Developing Compatible Dapps ## Developing Compatible Dapps
If you're a web dapp developer, we've got two types of guides for you: If you're a web dapp developer, we've got two types of guides for you:

@ -405,6 +405,9 @@
"infoHelp": { "infoHelp": {
"message": "Info & Help" "message": "Info & Help"
}, },
"initialTransactionConfirmed": {
"message": "Your initial transaction was confirmed by the network. Click OK to go back."
},
"insufficientFunds": { "insufficientFunds": {
"message": "Insufficient funds." "message": "Insufficient funds."
}, },
@ -523,6 +526,9 @@
"networks": { "networks": {
"message": "Networks" "message": "Networks"
}, },
"nevermind": {
"message": "Nevermind"
},
"newAccount": { "newAccount": {
"message": "New Account" "message": "New Account"
}, },
@ -637,9 +643,15 @@
"rejected": { "rejected": {
"message": "Rejected" "message": "Rejected"
}, },
"reset": {
"message": "Reset"
},
"resetAccount": { "resetAccount": {
"message": "Reset Account" "message": "Reset Account"
}, },
"resetAccountDescription": {
"message": "Resetting your account will clear your transaction history."
},
"restoreFromSeed": { "restoreFromSeed": {
"message": "Restore account?" "message": "Restore account?"
}, },
@ -679,6 +691,9 @@
"ropsten": { "ropsten": {
"message": "Ropsten Test Network" "message": "Ropsten Test Network"
}, },
"rpc": {
"message": "Custom RPC"
},
"currentRpc": { "currentRpc": {
"message": "Current RPC" "message": "Current RPC"
}, },
@ -704,10 +719,10 @@
"save": { "save": {
"message": "Save" "message": "Save"
}, },
"reprice_title": { "speedUpTitle": {
"message": "Reprice Transaction" "message": "Speed Up Transaction"
}, },
"reprice_subtitle": { "speedUpSubtitle": {
"message": "Increase your gas price to attempt to overwrite and speed up your transaction" "message": "Increase your gas price to attempt to overwrite and speed up your transaction"
}, },
"saveAsCsvFile": { "saveAsCsvFile": {
@ -898,7 +913,7 @@
"message": "Welcome to the New UI (Beta)" "message": "Welcome to the New UI (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "You are now using the new Metamask UI. Take a look around, try out new features like sending tokens, and let us know if you have any issues." "message": "You are now using the new Metamask UI."
}, },
"unapproved": { "unapproved": {
"message": "Unapproved" "message": "Unapproved"

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>76BCDB09-52B0-41CB-908F-12F9087A2F1B</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Confirm-TX-screen" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="confirmed-alert" transform="translate(-144.000000, -53.000000)" stroke="#61BA00" stroke-width="4">
<g id="Group-17-Copy" transform="translate(22.000000, 20.000000)">
<g id="check-icon" transform="translate(124.000000, 35.000000)">
<circle id="Oval-5" cx="48" cy="48" r="48"></circle>
<polyline id="Path-3" stroke-linecap="round" points="29.76 52.8 41.0023819 64.32 71.04 34.56"></polyline>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -1,7 +1,7 @@
{ {
"name": "__MSG_appName__", "name": "__MSG_appName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "4.6.0", "version": "4.6.1",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "__MSG_appDescription__", "description": "__MSG_appDescription__",
@ -67,6 +67,7 @@
"externally_connectable": { "externally_connectable": {
"matches": [ "matches": [
"https://metamask.io/*" "https://metamask.io/*"
] ],
"ids": ["*"]
} }
} }

@ -309,6 +309,7 @@ function setupController (initState, initLangCode) {
// connect to other contexts // connect to other contexts
// //
extension.runtime.onConnect.addListener(connectRemote) extension.runtime.onConnect.addListener(connectRemote)
extension.runtime.onConnectExternal.addListener(connectExternal)
const metamaskInternalProcessHash = { const metamaskInternalProcessHash = {
[ENVIRONMENT_TYPE_POPUP]: true, [ENVIRONMENT_TYPE_POPUP]: true,
@ -335,9 +336,9 @@ function setupController (initState, initLangCode) {
function connectRemote (remotePort) { function connectRemote (remotePort) {
const processName = remotePort.name const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName] const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
const portStream = new PortStream(remotePort)
if (isMetaMaskInternalProcess) { if (isMetaMaskInternalProcess) {
const portStream = new PortStream(remotePort)
// communication with popup // communication with popup
controller.isClientOpen = true controller.isClientOpen = true
controller.setupTrustedCommunication(portStream, 'MetaMask') controller.setupTrustedCommunication(portStream, 'MetaMask')
@ -370,12 +371,17 @@ function setupController (initState, initLangCode) {
}) })
} }
} else { } else {
// communication with page connectExternal(remotePort)
const originDomain = urlUtil.parse(remotePort.sender.url).hostname
controller.setupUntrustedCommunication(portStream, originDomain)
} }
} }
// communication with page or other extension
function connectExternal(remotePort) {
const originDomain = urlUtil.parse(remotePort.sender.url).hostname
const portStream = new PortStream(remotePort)
controller.setupUntrustedCommunication(portStream, originDomain)
}
// //
// User Interface setup // User Interface setup
// //

@ -8,6 +8,7 @@ const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('./pending-tx-tracker') const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('./nonce-tracker') const NonceTracker = require('./nonce-tracker')
const txUtils = require('./lib/util') const txUtils = require('./lib/util')
const cleanErrorStack = require('../../lib/cleanErrorStack')
const log = require('loglevel') const log = require('loglevel')
/** /**
@ -118,6 +119,7 @@ class TransactionController extends EventEmitter {
@param txParams {object} - txParams for the transaction @param txParams {object} - txParams for the transaction
@param opts {object} - with the key origin to put the origin on the txMeta @param opts {object} - with the key origin to put the origin on the txMeta
*/ */
async newUnapprovedTransaction (txParams, opts = {}) { async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams) const initialTxMeta = await this.addUnapprovedTransaction(txParams)
@ -130,11 +132,11 @@ class TransactionController extends EventEmitter {
case 'submitted': case 'submitted':
return resolve(finishedTxMeta.hash) return resolve(finishedTxMeta.hash)
case 'rejected': case 'rejected':
return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) return reject(cleanErrorStack(new Error('MetaMask Tx Signature: User denied transaction signature.')))
case 'failed': case 'failed':
return reject(new Error(finishedTxMeta.err.message)) return reject(cleanErrorStack(new Error(finishedTxMeta.err.message)))
default: default:
return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)) return reject(cleanErrorStack(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)))
} }
}) })
}) })

@ -2,6 +2,7 @@ const extend = require('xtend')
const EventEmitter = require('events') const EventEmitter = require('events')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const log = require('loglevel')
const txStateHistoryHelper = require('./lib/tx-state-history-helper') const txStateHistoryHelper = require('./lib/tx-state-history-helper')
const createId = require('../../lib/random-id') const createId = require('../../lib/random-id')
const { getFinalStates } = require('./lib/util') const { getFinalStates } = require('./lib/util')
@ -398,13 +399,19 @@ class TransactionStateManager extends EventEmitter {
_setTxStatus (txId, status) { _setTxStatus (txId, status) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.status = status txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId) setTimeout(() => {
this.emit(`tx:status-update`, txId, status) try {
if (['submitted', 'rejected', 'failed'].includes(status)) { this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.emit(`${txMeta.id}:finished`, txMeta) this.emit(`${txMeta.id}:${status}`, txId)
} this.emit(`tx:status-update`, txId, status)
this.updateTx(txMeta, `txStateManager: setting status to ${status}`) if (['submitted', 'rejected', 'failed'].includes(status)) {
this.emit('update:badge') this.emit(`${txMeta.id}:finished`, txMeta)
}
this.emit('update:badge')
} catch (error) {
log.error(error)
}
})
} }
/** /**

@ -0,0 +1,24 @@
/**
* Returns error without stack trace for better UI display
* @param {Error} err - error
* @returns {Error} Error with clean stack trace.
*/
function cleanErrorStack(err){
var name = err.name
name = (name === undefined) ? 'Error' : String(name)
var msg = err.message
msg = (msg === undefined) ? '' : String(msg)
if (name === '') {
err.stack = err.message
} else if (msg === '') {
err.stack = err.name
} else {
err.stack = err.name + ': ' + err.message
}
return err
}
module.exports = cleanErrorStack

@ -2,6 +2,12 @@ const extension = require('extensionizer')
const promisify = require('pify') const promisify = require('pify')
const allLocales = require('../../_locales/index.json') const allLocales = require('../../_locales/index.json')
const isSupported = extension.i18n && extension.i18n.getAcceptLanguages
const getPreferredLocales = isSupported ? promisify(
extension.i18n.getAcceptLanguages,
{ errorFirst: false }
) : async () => []
const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-')) const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-'))
/** /**
@ -12,10 +18,7 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r
* *
*/ */
async function getFirstPreferredLangCode () { async function getFirstPreferredLangCode () {
const userPreferredLocaleCodes = await promisify( const userPreferredLocaleCodes = await getPreferredLocales()
extension.i18n.getAcceptLanguages,
{ errorFirst: false }
)()
const firstPreferredLangCode = userPreferredLocaleCodes const firstPreferredLangCode = userPreferredLocaleCodes
.map(code => code.toLowerCase()) .map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code)) .find(code => existingLocaleCodes.includes(code))

@ -45,6 +45,7 @@ const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000') const GWEI_BN = new BN('1000000000')
const percentile = require('percentile') const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel') const log = require('loglevel')
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -350,7 +351,7 @@ module.exports = class MetamaskController extends EventEmitter {
verifySeedPhrase: nodeify(this.verifySeedPhrase, this), verifySeedPhrase: nodeify(this.verifySeedPhrase, this),
clearSeedWordCache: this.clearSeedWordCache.bind(this), clearSeedWordCache: this.clearSeedWordCache.bind(this),
resetAccount: nodeify(this.resetAccount, this), resetAccount: nodeify(this.resetAccount, this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
// vault management // vault management
submitPassword: nodeify(keyringController.submitPassword, keyringController), submitPassword: nodeify(keyringController.submitPassword, keyringController),
@ -610,15 +611,15 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {any} args - The data required by that strategy to import an account. * @param {any} args - The data required by that strategy to import an account.
* @param {Function} cb - A callback function called with a state update on success. * @param {Function} cb - A callback function called with a state update on success.
*/ */
importAccountWithStrategy (strategy, args, cb) { async importAccountWithStrategy (strategy, args) {
accountImporter.importAccount(strategy, args) const privateKey = await accountImporter.importAccount(strategy, args)
.then((privateKey) => { const keyring = await this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ])
return this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ]) const accounts = await keyring.getAccounts()
}) // update accounts in preferences controller
.then(keyring => keyring.getAccounts()) const allAccounts = await this.keyringController.getAccounts()
.then((accounts) => this.preferencesController.setSelectedAddress(accounts[0])) this.preferencesController.setAddresses(allAccounts)
.then(() => { cb(null, this.keyringController.fullUpdate()) }) // set new account as selected
.catch((reason) => { cb(reason) }) await this.preferencesController.setSelectedAddress(accounts[0])
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -644,9 +645,9 @@ module.exports = class MetamaskController extends EventEmitter {
case 'signed': case 'signed':
return cb(null, data.rawSig) return cb(null, data.rawSig)
case 'rejected': case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied message signature.')) return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default: default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
} }
}) })
} }
@ -704,7 +705,7 @@ module.exports = class MetamaskController extends EventEmitter {
*/ */
newUnsignedPersonalMessage (msgParams, cb) { newUnsignedPersonalMessage (msgParams, cb) {
if (!msgParams.from) { if (!msgParams.from) {
return cb(new Error('MetaMask Message Signature: from field is required.')) return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.')))
} }
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
@ -715,9 +716,9 @@ module.exports = class MetamaskController extends EventEmitter {
case 'signed': case 'signed':
return cb(null, data.rawSig) return cb(null, data.rawSig)
case 'rejected': case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied message signature.')) return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default: default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
} }
}) })
} }
@ -783,9 +784,9 @@ module.exports = class MetamaskController extends EventEmitter {
case 'signed': case 'signed':
return cb(null, data.rawSig) return cb(null, data.rawSig)
case 'rejected': case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied message signature.')) return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default: default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
} }
}) })
} }

@ -143,6 +143,7 @@ class CreatePasswordScreen extends Component {
autoComplete="new-password" autoComplete="new-password"
margin="normal" margin="normal"
fullWidth fullWidth
largeLabel
/> />
<TextField <TextField
id="confirm-password" id="confirm-password"
@ -155,6 +156,7 @@ class CreatePasswordScreen extends Component {
autoComplete="confirm-password" autoComplete="confirm-password"
margin="normal" margin="normal"
fullWidth fullWidth
largeLabel
/> />
<button <button
className="first-time-flow__button" className="first-time-flow__button"

@ -146,6 +146,7 @@ class ImportSeedPhraseScreen extends Component {
error={passwordError} error={passwordError}
autoComplete="new-password" autoComplete="new-password"
margin="normal" margin="normal"
largeLabel
/> />
<TextField <TextField
id="confirm-password" id="confirm-password"
@ -157,6 +158,7 @@ class ImportSeedPhraseScreen extends Component {
error={confirmPasswordError} error={confirmPasswordError}
autoComplete="confirm-password" autoComplete="confirm-password"
margin="normal" margin="normal"
largeLabel
/> />
<button <button
className="first-time-flow__button" className="first-time-flow__button"

210
package-lock.json generated

@ -309,7 +309,7 @@
}, },
"@sinonjs/formatio": { "@sinonjs/formatio": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
"integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -1500,7 +1500,8 @@
}, },
"dependencies": { "dependencies": {
"bignumber.js": { "bignumber.js": {
"version": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" "version": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2",
"from": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2"
}, },
"chai": { "chai": {
"version": "3.5.0", "version": "3.5.0",
@ -7173,6 +7174,12 @@
"domelementtype": "1.3.0" "domelementtype": "1.3.0"
} }
}, },
"dot-only-hunter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dot-only-hunter/-/dot-only-hunter-1.0.3.tgz",
"integrity": "sha1-9k0h7b5v8xFJlfEGGmGpNcMAIEs=",
"dev": true
},
"dotenv": { "dotenv": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz",
@ -8007,6 +8014,7 @@
"dependencies": { "dependencies": {
"async-eventemitter": { "async-eventemitter": {
"version": "github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c", "version": "github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c",
"from": "async-eventemitter@github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c",
"requires": { "requires": {
"async": "2.6.0" "async": "2.6.0"
} }
@ -8161,55 +8169,59 @@
} }
}, },
"eth-keyring-controller": { "eth-keyring-controller": {
"version": "3.1.1", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.1.1.tgz", "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.1.4.tgz",
"integrity": "sha512-Z9HTzrop/V4Ld8Wq7uwetKecfWIyx25/uL8aFoZxV3kegZGoXaWoRmNy+4oW0WNLp4BcJ1lk6QfsGEdlymGjmA==", "integrity": "sha512-NNlVB/TBc8p9CblwECjPlUR+7MNQKiBa7tEFxIzZ9MjjNCEYPWDXTm0vJZzuDtVmFxYwIA53UD0QEn0QNxWNEQ==",
"dev": true,
"requires": { "requires": {
"bip39": "2.4.0", "bip39": "^2.4.0",
"bluebird": "3.5.1", "bluebird": "^3.5.0",
"browser-passworder": "2.0.3", "browser-passworder": "^2.0.3",
"eth-hd-keyring": "1.2.2", "eth-hd-keyring": "^1.2.2",
"eth-sig-util": "1.4.2", "eth-sig-util": "^1.4.0",
"eth-simple-keyring": "1.2.1", "eth-simple-keyring": "^1.2.2",
"ethereumjs-util": "5.2.0", "ethereumjs-util": "^5.1.2",
"loglevel": "1.6.0", "loglevel": "^1.5.0",
"obs-store": "2.4.1", "obs-store": "^2.4.1",
"promise-filter": "1.1.0" "promise-filter": "^1.1.0"
}, },
"dependencies": { "dependencies": {
"babelify": { "babelify": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"dev": true,
"requires": { "requires": {
"babel-core": "6.26.0", "babel-core": "^6.0.14",
"object-assign": "4.1.1" "object-assign": "^4.0.0"
} }
}, },
"ethereumjs-util": { "ethereumjs-util": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
"integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
"dev": true,
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "^4.11.0",
"create-hash": "1.1.3", "create-hash": "^1.1.2",
"ethjs-util": "0.1.4", "ethjs-util": "^0.1.3",
"keccak": "1.4.0", "keccak": "^1.0.2",
"rlp": "2.0.0", "rlp": "^2.0.0",
"safe-buffer": "5.1.1", "safe-buffer": "^5.1.1",
"secp256k1": "3.4.0" "secp256k1": "^3.0.1"
} }
}, },
"obs-store": { "obs-store": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/obs-store/-/obs-store-2.4.1.tgz", "resolved": "https://registry.npmjs.org/obs-store/-/obs-store-2.4.1.tgz",
"integrity": "sha512-wpA8G4uSn8cnCKZ0pFTvqsamvy0Sm1hR2ot0Qonbfj5yBMwdAp/eD4vDI+U/ZCbV1hb2V5GapL8YKUdGCvahgg==", "integrity": "sha512-wpA8G4uSn8cnCKZ0pFTvqsamvy0Sm1hR2ot0Qonbfj5yBMwdAp/eD4vDI+U/ZCbV1hb2V5GapL8YKUdGCvahgg==",
"dev": true,
"requires": { "requires": {
"babel-preset-es2015": "6.24.1", "babel-preset-es2015": "^6.22.0",
"babelify": "7.3.0", "babelify": "^7.3.0",
"readable-stream": "2.3.3", "readable-stream": "^2.2.2",
"through2": "2.0.3", "through2": "^2.0.3",
"xtend": "4.0.1" "xtend": "^4.0.1"
} }
} }
} }
@ -8256,6 +8268,7 @@
"dependencies": { "dependencies": {
"ethereumjs-abi": { "ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf", "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "4.11.8",
"ethereumjs-util": "5.1.3" "ethereumjs-util": "5.1.3"
@ -8278,29 +8291,31 @@
} }
}, },
"eth-simple-keyring": { "eth-simple-keyring": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.2.1.tgz", "resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.2.2.tgz",
"integrity": "sha1-bXs1LcWppQINYfafryHvsvY2P0U=", "integrity": "sha512-uQVBYshHUOaXVoat1BpLA/QNMCr4hgdFBgwIB7rRmQ+m3vQQAseUsOM+biPDYzq6end+6LjcccElLpQaIZe6dg==",
"dev": true,
"requires": { "requires": {
"eth-sig-util": "1.4.2", "eth-sig-util": "^1.4.2",
"ethereumjs-util": "5.2.0", "ethereumjs-util": "^5.1.1",
"ethereumjs-wallet": "0.6.0", "ethereumjs-wallet": "^0.6.0",
"events": "1.1.1", "events": "^1.1.1",
"xtend": "4.0.1" "xtend": "^4.0.1"
}, },
"dependencies": { "dependencies": {
"ethereumjs-util": { "ethereumjs-util": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
"integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
"dev": true,
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "^4.11.0",
"create-hash": "1.1.3", "create-hash": "^1.1.2",
"ethjs-util": "0.1.4", "ethjs-util": "^0.1.3",
"keccak": "1.4.0", "keccak": "^1.0.2",
"rlp": "2.0.0", "rlp": "^2.0.0",
"safe-buffer": "5.1.1", "safe-buffer": "^5.1.1",
"secp256k1": "3.4.0" "secp256k1": "^3.0.1"
} }
} }
} }
@ -8487,7 +8502,7 @@
"eth-query": "2.1.2", "eth-query": "2.1.2",
"ethereumjs-block": "1.7.0", "ethereumjs-block": "1.7.0",
"ethereumjs-tx": "1.3.3", "ethereumjs-tx": "1.3.3",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "^5.0.1",
"ethereumjs-vm": "2.3.5", "ethereumjs-vm": "2.3.5",
"through2": "2.0.3", "through2": "2.0.3",
"treeify": "1.1.0", "treeify": "1.1.0",
@ -8636,7 +8651,7 @@
"async": "2.6.0", "async": "2.6.0",
"ethereum-common": "0.2.0", "ethereum-common": "0.2.0",
"ethereumjs-tx": "1.3.3", "ethereumjs-tx": "1.3.3",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "^5.0.0",
"merkle-patricia-tree": "2.3.0" "merkle-patricia-tree": "2.3.0"
} }
}, },
@ -8646,7 +8661,7 @@
"integrity": "sha1-7OBR0+/b53GtKlGNYWMsoqt17Ls=", "integrity": "sha1-7OBR0+/b53GtKlGNYWMsoqt17Ls=",
"requires": { "requires": {
"ethereum-common": "0.0.18", "ethereum-common": "0.0.18",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9" "ethereumjs-util": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"ethereum-common": { "ethereum-common": {
@ -8658,6 +8673,7 @@
}, },
"ethereumjs-util": { "ethereumjs-util": {
"version": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "version": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"from": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "4.11.8",
"create-hash": "1.1.3", "create-hash": "1.1.3",
@ -8732,16 +8748,16 @@
"integrity": "sha1-x7kULEtZUJsziiBLYyiupA3Txk4=" "integrity": "sha1-x7kULEtZUJsziiBLYyiupA3Txk4="
}, },
"ethjs": { "ethjs": {
"version": "0.3.6", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.6.tgz", "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.4.0.tgz",
"integrity": "sha512-9ojnSkV5XXSM5vo0pKgZpE+SNBPxqSUN0dZmMP5dBZVFOYctRd9tfaZ80Jnde3M4JrfUhhkbG5QFvewitaAY7Q==", "integrity": "sha512-UnQeRMpQ+JETN2FviexEskUwByid+eO8rybjPnk2DNUzjUn0VKNrUbiCAud7Es6otDFwjUeOS58vMZwkZxIIog==",
"requires": { "requires": {
"bn.js": "4.11.6", "bn.js": "4.11.6",
"ethjs-abi": "0.2.1", "ethjs-abi": "0.2.1",
"ethjs-contract": "0.1.9", "ethjs-contract": "0.2.3",
"ethjs-filter": "0.1.5", "ethjs-filter": "0.1.8",
"ethjs-provider-http": "0.1.6", "ethjs-provider-http": "0.1.6",
"ethjs-query": "0.3.4", "ethjs-query": "0.3.8",
"ethjs-unit": "0.1.6", "ethjs-unit": "0.1.6",
"ethjs-util": "0.1.3", "ethjs-util": "0.1.3",
"js-sha3": "0.5.5", "js-sha3": "0.5.5",
@ -8764,12 +8780,13 @@
} }
}, },
"ethjs-contract": { "ethjs-contract": {
"version": "0.1.9", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.1.9.tgz", "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.3.tgz",
"integrity": "sha1-HCdmiWpW1H7B1tZhgpxJzDilUgo=", "integrity": "sha512-fKsHm57wxwHrZhVlD8AHU2lC2G3c1fmvoEz15BpqIkuGWiTbjuvrQo2Avc+3EQpSsTFWNdyxC0h1WKRcn5kkyQ==",
"requires": { "requires": {
"babel-runtime": "6.26.0",
"ethjs-abi": "0.2.0", "ethjs-abi": "0.2.0",
"ethjs-filter": "0.1.5", "ethjs-filter": "0.1.8",
"ethjs-util": "0.1.3", "ethjs-util": "0.1.3",
"js-sha3": "0.5.5" "js-sha3": "0.5.5"
}, },
@ -8786,6 +8803,30 @@
} }
} }
}, },
"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.8",
"resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.8.tgz",
"integrity": "sha512-/J5JydqrOzU8O7VBOwZKUWXxHDGr46VqNjBCJgBVNNda+tv7Xc8Y2uJc6aMHHVbeN3YOQ7YRElgIc0q1CI02lQ==",
"requires": {
"babel-runtime": "6.26.0",
"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": { "ethjs-util": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz",
@ -8934,6 +8975,35 @@
"resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.5.tgz", "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.5.tgz",
"integrity": "sha1-ARKvYBfCRnfjK4/esg5hlgGbdZg=" "integrity": "sha1-ARKvYBfCRnfjK4/esg5hlgGbdZg="
}, },
"ethjs-format": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.7.tgz",
"integrity": "sha512-uNYAi+r3/mvR3xYu2AfSXx5teP4ovy9z2FrRsblU+h2logsaIKZPi9V3bn3V7wuRcnG0HZ3QydgZuVaRo06C4Q==",
"requires": {
"bn.js": "4.11.6",
"ethjs-schema": "0.2.1",
"ethjs-util": "0.1.3",
"is-hex-prefixed": "1.0.0",
"number-to-bn": "1.7.0",
"strip-hex-prefix": "1.0.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-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"
}
}
}
},
"ethjs-provider-http": { "ethjs-provider-http": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/ethjs-provider-http/-/ethjs-provider-http-0.1.6.tgz", "resolved": "https://registry.npmjs.org/ethjs-provider-http/-/ethjs-provider-http-0.1.6.tgz",
@ -8995,6 +9065,11 @@
"resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.1.5.tgz", "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.1.5.tgz",
"integrity": "sha1-CZ4i8n3EwYtpeKSF/DaxsPeWkIA=" "integrity": "sha1-CZ4i8n3EwYtpeKSF/DaxsPeWkIA="
}, },
"ethjs-schema": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.2.1.tgz",
"integrity": "sha512-DXd8lwNrhT9sjsh/Vd2Z+4pfyGxhc0POVnLBUfwk5udtdoBzADyq+sK39dcb48+ZU+2VgtwHxtGWnLnCfmfW5g=="
},
"ethjs-unit": { "ethjs-unit": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz",
@ -9036,7 +9111,7 @@
}, },
"event-stream": { "event-stream": {
"version": "3.3.4", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -11762,6 +11837,7 @@
}, },
"gulp": { "gulp": {
"version": "github:gulpjs/gulp#71c094a51c7972d26f557899ddecab0210ef3776", "version": "github:gulpjs/gulp#71c094a51c7972d26f557899ddecab0210ef3776",
"from": "github:gulpjs/gulp#4.0",
"requires": { "requires": {
"glob-watcher": "4.0.0", "glob-watcher": "4.0.0",
"gulp-cli": "2.0.1", "gulp-cli": "2.0.1",
@ -18144,7 +18220,7 @@
"integrity": "sha512-LKd2OoIT9Re/OG38zXbd5pyHIk2IfcOUczCwkYXl5iJIbufg9nqpweh66VfPwMkUlrEvc7YVvtQdmSrB9V9TkQ==", "integrity": "sha512-LKd2OoIT9Re/OG38zXbd5pyHIk2IfcOUczCwkYXl5iJIbufg9nqpweh66VfPwMkUlrEvc7YVvtQdmSrB9V9TkQ==",
"requires": { "requires": {
"async": "1.5.2", "async": "1.5.2",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "^5.0.0",
"level-ws": "0.0.0", "level-ws": "0.0.0",
"levelup": "1.3.9", "levelup": "1.3.9",
"memdown": "1.4.1", "memdown": "1.4.1",
@ -26436,6 +26512,18 @@
"object-assign": "4.1.1" "object-assign": "4.1.1"
} }
}, },
"react": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
"integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
"requires": {
"create-react-class": "15.6.2",
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.1"
}
},
"react-hyperscript": { "react-hyperscript": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz", "resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz",
@ -31194,7 +31282,8 @@
}, },
"dependencies": { "dependencies": {
"bignumber.js": { "bignumber.js": {
"version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git"
} }
} }
}, },
@ -31675,6 +31764,7 @@
}, },
"websocket": { "websocket": {
"version": "git://github.com/frozeman/WebSocket-Node.git#7004c39c42ac98875ab61126e5b4a925430f592c", "version": "git://github.com/frozeman/WebSocket-Node.git#7004c39c42ac98875ab61126e5b4a925430f592c",
"from": "websocket@git://github.com/frozeman/WebSocket-Node.git#7004c39c42ac98875ab61126e5b4a925430f592c",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"nan": "2.8.0", "nan": "2.8.0",

@ -9,12 +9,14 @@
"dist": "gulp dist", "dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json", "doc": "jsdoc -c development/tools/.jsdoc.json",
"test": "npm run test:unit && npm run test:integration && npm run lint", "test": "npm run test:unit && npm run test:integration && npm run lint",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", "test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss", "test:integration:build": "gulp build:scss",
"test:e2e:chrome": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:chrome'", "test:e2e:chrome": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:chrome'",
"test:e2e:chrome:beta": "SELENIUM_BROWSER=chrome test/e2e/beta/run-all.sh",
"test:e2e:firefox": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:firefox'", "test:e2e:firefox": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:firefox'",
"test:e2e:firefox:beta": "SELENIUM_BROWSER=firefox test/e2e/beta/run-all.sh",
"test:e2e:run:chrome": "SELENIUM_BROWSER=chrome mocha test/e2e/metamask.spec --bail --recursive", "test:e2e:run:chrome": "SELENIUM_BROWSER=chrome mocha test/e2e/metamask.spec --bail --recursive",
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/metamask.spec --bail --recursive", "test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/metamask.spec --bail --recursive",
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'", "test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
@ -96,7 +98,6 @@
"eth-hd-keyring": "^1.2.1", "eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.6", "eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0", "eth-json-rpc-infura": "^3.0.0",
"eth-keyring-controller": "^3.1.1",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2", "eth-sig-util": "^1.4.2",
@ -106,7 +107,7 @@
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
"etherscan-link": "^1.0.2", "etherscan-link": "^1.0.2",
"ethjs": "^0.3.4", "ethjs": "^0.4.0",
"ethjs-contract": "^0.2.0", "ethjs-contract": "^0.2.0",
"ethjs-ens": "^2.0.0", "ethjs-ens": "^2.0.0",
"ethjs-query": "^0.3.4", "ethjs-query": "^0.3.4",
@ -219,6 +220,7 @@
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"deep-freeze-strict": "^1.1.1", "deep-freeze-strict": "^1.1.1",
"del": "^3.0.0", "del": "^3.0.0",
"dot-only-hunter": "^1.0.3",
"envify": "^4.0.0", "envify": "^4.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5", "enzyme-adapter-react-15": "^1.0.5",
@ -227,6 +229,7 @@
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0", "eth-json-rpc-middleware": "^1.6.0",
"eth-keyring-controller": "^3.1.4",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0", "ganache-cli": "^6.1.0",

@ -0,0 +1,406 @@
const path = require('path')
const assert = require('assert')
const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver
const {
delay,
buildChromeWebDriver,
buildFirefoxWebdriver,
installWebExt,
getExtensionIdChrome,
getExtensionIdFirefox,
} = require('../func')
const {
checkBrowserForConsoleErrors,
loadExtension,
verboseReportOnFailure,
} = require('./helpers')
describe('Using MetaMask with an existing account', function () {
let extensionId
let driver
let tokenAddress
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
const regularDelayMs = 1000
const largeDelayMs = regularDelayMs * 2
const waitingNewPageDelayMs = regularDelayMs * 10
this.timeout(0)
this.bail(true)
before(async function () {
switch (process.env.SELENIUM_BROWSER) {
case 'chrome': {
const extensionPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extensionPath)
extensionId = await getExtensionIdChrome(driver)
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(regularDelayMs)
break
}
case 'firefox': {
const extensionPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver()
await installWebExt(driver, extensionPath)
await delay(regularDelayMs)
extensionId = await getExtensionIdFirefox(driver)
await driver.get(`moz-extension://${extensionId}/popup.html`)
await delay(regularDelayMs)
break
}
}
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
if (errors.length) {
const errorReports = errors.map(err => err.message)
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
console.error(new Error(errorMessage))
}
}
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(driver, this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('New UI setup', async function () {
it('switches to first tab', async function () {
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
await delay(regularDelayMs)
})
it('use the local network', async function () {
const [networkSelector] = await driver.findElements(By.css('#network_component'))
await networkSelector.click()
await delay(regularDelayMs)
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click()
await delay(regularDelayMs)
// Close all other tabs
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
await driver.switchTo().window(oldUi)
await driver.close()
await driver.switchTo().window(infoPage)
await driver.close()
await driver.switchTo().window(newUi)
await delay(regularDelayMs)
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button'))
await continueBtn.click()
await delay(regularDelayMs)
})
})
describe('First time flow starting from an existing seed phrase', () => {
it('imports a seed phrase', async () => {
const [seedPhrase] = await driver.findElements(By.xpath(`//a[contains(text(), 'Import with seed phrase')]`))
await seedPhrase.click()
await delay(regularDelayMs)
const [seedTextArea] = await driver.findElements(By.css('textarea.import-account__secret-phrase'))
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
const [password] = await driver.findElements(By.id('password'))
await password.sendKeys('correct horse battery staple')
const [confirmPassword] = await driver.findElements(By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple')
const [importButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Import')]`))
await importButton.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const element = await driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(regularDelayMs)
const [acceptTos] = await driver.findElements(By.css('.tou button'))
await acceptTos.click()
await delay(regularDelayMs)
})
})
describe('Show account information', () => {
it('shows the correct account address', async () => {
await driver.findElement(By.css('.wallet-view__details-button')).click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs)
const [address] = await driver.findElements(By.css('input.qr-ellip-address'))
assert.equal(await address.getAttribute('value'), testAddress)
await driver.executeScript("document.querySelector('.account-modal-close').click()")
await delay(largeDelayMs)
})
it('shows a QR code for the account', async () => {
await driver.findElement(By.css('.wallet-view__details-button')).click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs)
await driver.executeScript("document.querySelector('.account-modal-close').click()")
await delay(regularDelayMs)
})
})
describe('Log out and log back in', () => {
it('logs out of the account', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click()
await delay(regularDelayMs)
})
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(largeDelayMs)
})
})
describe('Add an account', () => {
it('choose Create Account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`))
await createAccount.click()
await delay(regularDelayMs)
})
it('set account name', async () => {
const [accountName] = await driver.findElements(By.css('.new-account-create-form input'))
await accountName.sendKeys('2nd account')
await delay(regularDelayMs)
const [createButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`))
await createButton.click()
await delay(regularDelayMs)
})
it('should show the correct account name', async () => {
const [accountName] = await driver.findElements(By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs)
})
})
describe('Switch back to original account', () => {
it('chooses the original account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [originalAccountMenuItem] = await driver.findElements(By.css('.account-menu__name'))
await originalAccountMenuItem.click()
await delay(regularDelayMs)
})
})
describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () {
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]'))
const [inputAmount] = await driver.findElements(By.css('.currency-display__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
// Set the gas limit
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
await delay(regularDelayMs)
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await delay(regularDelayMs)
// Continue to next screen
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
it('confirms the transaction', async function () {
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await driver.findElements(By.css('.tx-list-item'))
assert.equal(transactions.length, 1)
const txValues = await driver.findElements(By.css('.tx-list-value'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH')
})
})
describe('Send ETH from Faucet', () => {
it('starts a send transaction inside Faucet', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")')
await delay(waitingNewPageDelayMs)
const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`))
await send1eth.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
await driver.close()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
})
})
describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('picks an existing token', async () => {
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input'))
await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs)
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]"))
await token.click()
await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(largeDelayMs)
})
it('renders the balance for the new token', async () => {
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT')
await delay(regularDelayMs)
})
})
describe('Add a custom token from TokenFactory', () => {
it('creates a new token', async () => {
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")')
await delay(waitingNewPageDelayMs)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
await driver.switchTo().window(tokenFactory)
const [
totalSupply,
tokenName,
tokenDecimal,
tokenSymbol,
] = await driver.findElements(By.css('input'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(tokenFactory)
await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
})
it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('picks the new Test token', async () => {
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]"))
await addCustomToken.click()
await delay(regularDelayMs)
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input'))
await newTokenAddress.sendKeys(tokenAddress)
await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(regularDelayMs)
})
it('renders the balance for the new token', async () => {
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST')
await delay(regularDelayMs)
})
})
})

@ -0,0 +1,55 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const pify = require('pify')
module.exports = {
checkBrowserForConsoleErrors,
loadExtension,
verboseReportOnFailure,
}
async function loadExtension (driver, extensionId) {
switch (process.env.SELENIUM_BROWSER) {
case 'chrome': {
await driver.get(`chrome-extension://${extensionId}/home.html`)
break
}
case 'firefox': {
await driver.get(`moz-extension://${extensionId}/home.html`)
break
}
}
}
async function checkBrowserForConsoleErrors (driver) {
const ignoredLogTypes = ['WARNING']
const ignoredErrorMessages = [
// React throws error warnings on "dataset", but still sets the data-* properties correctly
'Warning: Unknown prop `dataset` on ',
// Third-party Favicon 404s show up as errors
'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)',
// React Development build - known issue blocked by test build sys
'Warning: It looks like you\'re using a minified copy of the development build of React.',
// Redux Development build - known issue blocked by test build sys
'This means that you are running a slower development build of Redux.',
]
const browserLogs = await driver.manage().logs().get('browser')
const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString()))
const errorObjects = errorEntries.map(entry => entry.toJSON())
return errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message)))
}
async function verboseReportOnFailure (driver, test) {
let artifactDir
if (process.env.SELENIUM_BROWSER === 'chrome') {
artifactDir = `./test-artifacts/chrome/${test.title}`
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
artifactDir = `./test-artifacts/firefox/${test.title}`
}
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}

@ -0,0 +1,491 @@
const path = require('path')
const assert = require('assert')
const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver
const {
delay,
buildChromeWebDriver,
buildFirefoxWebdriver,
installWebExt,
getExtensionIdChrome,
getExtensionIdFirefox,
} = require('../func')
const {
checkBrowserForConsoleErrors,
loadExtension,
verboseReportOnFailure,
} = require('./helpers')
describe('MetaMask', function () {
let extensionId
let driver
let tokenAddress
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const tinyDelayMs = 500
const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2
const waitingNewPageDelayMs = regularDelayMs * 10
this.timeout(0)
this.bail(true)
before(async function () {
switch (process.env.SELENIUM_BROWSER) {
case 'chrome': {
const extPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extPath)
extensionId = await getExtensionIdChrome(driver)
await driver.get(`chrome-extension://${extensionId}/popup.html`)
break
}
case 'firefox': {
const extPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver()
await installWebExt(driver, extPath)
await delay(700)
extensionId = await getExtensionIdFirefox(driver)
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
}
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
if (errors.length) {
const errorReports = errors.map(err => err.message)
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
console.error(new Error(errorMessage))
}
}
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('New UI setup', async function () {
it('switches to first tab', async function () {
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
await delay(regularDelayMs)
})
it('use the local network', async function () {
const [networkSelector] = await driver.findElements(By.css('#network_component'))
await networkSelector.click()
await delay(regularDelayMs)
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click()
await delay(regularDelayMs)
// Close all other tabs
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
await driver.switchTo().window(oldUi)
await driver.close()
await driver.switchTo().window(infoPage)
await driver.close()
await driver.switchTo().window(newUi)
await delay(regularDelayMs)
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button'))
await continueBtn.click()
await delay(regularDelayMs)
})
})
describe('Going through the first time flow', () => {
it('accepts a secure password', async () => {
const [passwordBox] = await driver.findElements(By.css('.create-password #create-password'))
const [passwordBoxConfirm] = await driver.findElements(By.css('.create-password #confirm-password'))
const [button] = await driver.findElements(By.css('.create-password button'))
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
await button.click()
await delay(regularDelayMs)
})
it('clicks through the unique image screen', async () => {
const [nextScreen] = await driver.findElements(By.css('.unique-image button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const [bottomOfTos] = await driver.findElements(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const [acceptTos] = await driver.findElements(By.css('.tou button'))
await acceptTos.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {
const [revealSeedPhrase] = await driver.findElements(By.css('.backup-phrase__secret-blocker'))
await revealSeedPhrase.click()
await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.css('.backup-phrase button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
const [word0] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[0]}')]`))
await word0.click()
await delay(tinyDelayMs)
const [word1] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[1]}')]`))
await word1.click()
await delay(tinyDelayMs)
const [word2] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[2]}')]`))
await word2.click()
await delay(tinyDelayMs)
const [word3] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[3]}')]`))
await word3.click()
await delay(tinyDelayMs)
const [word4] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[4]}')]`))
await word4.click()
await delay(tinyDelayMs)
const [word5] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[5]}')]`))
await word5.click()
await delay(tinyDelayMs)
const [word6] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[6]}')]`))
await word6.click()
await delay(tinyDelayMs)
const [word7] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[7]}')]`))
await word7.click()
await delay(tinyDelayMs)
const [word8] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[8]}')]`))
await word8.click()
await delay(tinyDelayMs)
const [word9] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[9]}')]`))
await word9.click()
await delay(tinyDelayMs)
const [word10] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[10]}')]`))
await word10.click()
await delay(tinyDelayMs)
const [word11] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[11]}')]`))
await word11.click()
await delay(tinyDelayMs)
const [confirm] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
await delay(regularDelayMs)
})
it('clicks through the deposit modal', async () => {
const [closeModal] = await driver.findElements(By.css('.page-container__header-close'))
await closeModal.click()
await delay(regularDelayMs)
})
})
describe('Show account information', () => {
it('shows the QR code for the account', async () => {
await driver.findElement(By.css('.wallet-view__details-button')).click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs)
await driver.executeScript("document.querySelector('.account-modal-close').click()")
await delay(regularDelayMs * 4)
})
})
describe('Log out an log back in', () => {
it('logs out of the account', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click()
await delay(regularDelayMs)
})
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)
})
})
describe('Add account', () => {
it('choose Create Account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`))
await createAccount.click()
await delay(regularDelayMs)
})
it('set account name', async () => {
const [accountName] = await driver.findElements(By.css('.new-account-create-form input'))
await accountName.sendKeys('2nd account')
await delay(regularDelayMs)
const [create] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`))
await create.click()
await delay(regularDelayMs)
})
it('should correct account name', async () => {
const [accountName] = await driver.findElements(By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs)
})
})
describe('Import seed phrase', () => {
it('logs out of the vault', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click()
await delay(regularDelayMs)
})
it('imports seed phrase', async () => {
const [restoreSeedLink] = await driver.findElements(By.css('.unlock-page__link--import'))
assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase')
await restoreSeedLink.click()
await delay(regularDelayMs)
const [seedTextArea] = await driver.findElements(By.css('textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple')
await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple')
await driver.findElement(By.css('button:nth-child(2)')).click()
await delay(regularDelayMs)
})
it('balance renders', async () => {
const balance = await driver.findElement(By.css('.balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100.000 ETH')
await delay(regularDelayMs)
})
})
describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () {
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]'))
const [inputAmount] = await driver.findElements(By.css('.currency-display__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
// Set the gas limit
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
await delay(regularDelayMs)
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await delay(regularDelayMs)
// Continue to next screen
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
it('confirms the transaction', async function () {
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await driver.findElements(By.css('.tx-list-item'))
assert.equal(transactions.length, 1)
const txValues = await driver.findElements(By.css('.tx-list-value'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH')
})
})
describe('Send ETH from Faucet', () => {
it('starts a send transaction inside Faucet', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")')
await delay(waitingNewPageDelayMs)
const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`))
await send1eth.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
await driver.close()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
})
})
describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('can pick a token from the existing options', async () => {
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input'))
await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs)
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]"))
await token.click()
await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(largeDelayMs)
})
it('renders the balance for the chosen token', async () => {
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT')
await delay(regularDelayMs)
})
})
describe('Add a custom token from TokenFactory', () => {
it('creates a new token', async () => {
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")')
await delay(waitingNewPageDelayMs)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
await driver.switchTo().window(tokenFactory)
const [
totalSupply,
tokenName,
tokenDecimal,
tokenSymbol,
] = await driver.findElements(By.css('input'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(tokenFactory)
await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
})
it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('picks the newly created Test token', async () => {
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]"))
await addCustomToken.click()
await delay(regularDelayMs)
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input'))
await newTokenAddress.sendKeys(tokenAddress)
await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(regularDelayMs)
})
it('renders the balance for the new token', async () => {
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST')
await delay(regularDelayMs)
})
})
})

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
export PATH="$PATH:./node_modules/.bin"
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'

@ -1,5 +1,7 @@
require('chromedriver') require('chromedriver')
require('geckodriver') require('geckodriver')
const fs = require('fs')
const os = require('os')
const path = require('path') const path = require('path')
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
const Command = require('selenium-webdriver/lib/command').Command const Command = require('selenium-webdriver/lib/command').Command
@ -19,10 +21,15 @@ function delay (time) {
} }
function buildChromeWebDriver (extPath) { function buildChromeWebDriver (extPath) {
const tmpProfile = path.join(os.tmpdir(), fs.mkdtempSync('mm-chrome-profile'));
return new webdriver.Builder() return new webdriver.Builder()
.withCapabilities({ .withCapabilities({
chromeOptions: { chromeOptions: {
args: [`load-extension=${extPath}`], args: [
`load-extension=${extPath}`,
`user-data-dir=${tmpProfile}`,
],
binary: process.env.SELENIUM_CHROME_BINARY,
}, },
}) })
.build() .build()
@ -53,4 +60,4 @@ async function installWebExt (driver, extension) {
.defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install') .defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install')
return await driver.schedule(cmd, 'installWebExt(' + extension + ')') return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
} }

@ -121,7 +121,14 @@ describe('Metamask popup page', function () {
await delay(300) await delay(300)
}) })
it('adds a second account', async function () {
await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div')).click()
await delay(300)
await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span')).click()
})
it('shows account address', async function () { it('shows account address', async function () {
await delay(300)
accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText() accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText()
}) })

@ -43,7 +43,7 @@ async function runAddTokenFlowTest (assert, done) {
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Cancel Add Token // Cancel Add Token
const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.page-container__footer-button') const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button')
assert.ok(cancelAddTokenButton[0], 'cancel add token button present') assert.ok(cancelAddTokenButton[0], 'cancel add token button present')
cancelAddTokenButton.click() cancelAddTokenButton.click()
@ -75,15 +75,15 @@ async function runAddTokenFlowTest (assert, done) {
tokenWrapper[0].click() tokenWrapper[0].click()
// Click Next button // Click Next button
let nextButton = await queryAsync($, 'button.btn-primary--lg') let nextButton = await queryAsync($, 'button.btn-primary.btn--large')
assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
nextButton[0].click() nextButton[0].click()
// Confirm Add token // Confirm Add token
const confirmAddToken = await queryAsync($, '.confirm-add-token') const confirmAddToken = await queryAsync($, '.confirm-add-token')
assert.ok(confirmAddToken[0], 'confirm add token rendered') assert.ok(confirmAddToken[0], 'confirm add token rendered')
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found') assert.ok($('button.btn-primary.btn--large')[0], 'confirm add token button found')
$('button.btn-primary--lg')[0].click() $('button.btn-primary.btn--large')[0].click()
// Verify added token image // Verify added token image
let heroBalance = await queryAsync($, '.hero-balance') let heroBalance = await queryAsync($, '.hero-balance')
@ -120,7 +120,7 @@ async function runAddTokenFlowTest (assert, done) {
const errorMessage = await queryAsync($, '#custom-symbol-helper-text') const errorMessage = await queryAsync($, '#custom-symbol-helper-text')
assert.ok(errorMessage[0], 'error rendered') assert.ok(errorMessage[0], 'error rendered')
$('button.btn-secondary--lg')[0].click() $('button.btn-default.btn--large')[0].click()
// await timeout(100000) // await timeout(100000)

@ -38,7 +38,7 @@ async function runConfirmSigRequestsTest(assert, done) {
let confirmSigRowValue = await queryAsync($, '.request-signature__row-value') let confirmSigRowValue = await queryAsync($, '.request-signature__row-value')
assert.equal(confirmSigRowValue[0].textContent, '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0') assert.equal(confirmSigRowValue[0].textContent, '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0')
let confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click() confirmSigSignButton[0].click()
confirmSigHeadline = await queryAsync($, '.request-signature__headline') confirmSigHeadline = await queryAsync($, '.request-signature__headline')
@ -47,7 +47,7 @@ async function runConfirmSigRequestsTest(assert, done) {
confirmSigRowValue = await queryAsync($, '.request-signature__row-value') confirmSigRowValue = await queryAsync($, '.request-signature__row-value')
assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/)) assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/))
confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click() confirmSigSignButton[0].click()
confirmSigHeadline = await queryAsync($, '.request-signature__headline') confirmSigHeadline = await queryAsync($, '.request-signature__headline')
@ -57,7 +57,7 @@ async function runConfirmSigRequestsTest(assert, done) {
assert.equal(confirmSigRowValue[0].textContent, 'Hi, Alice!') assert.equal(confirmSigRowValue[0].textContent, 'Hi, Alice!')
assert.equal(confirmSigRowValue[1].textContent, '1337') assert.equal(confirmSigRowValue[1].textContent, '1337')
confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click() confirmSigSignButton[0].click()
const txView = await queryAsync($, '.tx-view') const txView = await queryAsync($, '.tx-view')

@ -101,7 +101,7 @@ async function runSendFlowTest(assert, done) {
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountField.find('.currency-display')[0].click() sendAmountField.find('.currency-display')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, 'input:text') const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
sendAmountFieldInput.val('5.1') sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0]) reactTriggerChange(sendAmountField.find('input')[0])
@ -127,9 +127,9 @@ async function runSendFlowTest(assert, done) {
) )
await customizeGas(assert, 0, 21000, '0', '$0.00 USD') await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD') await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button') const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
sendButton[0].click() sendButton[0].click()
await timeout() await timeout()
@ -165,17 +165,17 @@ async function runSendFlowTest(assert, done) {
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountFieldInEdit.find('.currency-display')[0].click() sendAmountFieldInEdit.find('.currency-display')[0].click()
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('input:text') const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
sendAmountFieldInputInEdit.val('1.0') sendAmountFieldInputInEdit.val('1.0')
reactTriggerChange(sendAmountFieldInputInEdit[0]) reactTriggerChange(sendAmountFieldInputInEdit[0])
const sendButtonInEdit = await queryAsync($, '.btn-primary--lg.page-container__footer-button') const sendButtonInEdit = await queryAsync($, '.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered') assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered')
selectState.val('send new ui') selectState.val('send new ui')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const cancelButtonInEdit = await queryAsync($, '.btn-secondary--lg.page-container__footer-button') const cancelButtonInEdit = await queryAsync($, '.btn-default.btn--large.page-container__footer-button')
cancelButtonInEdit[0].click() cancelButtonInEdit[0].click()
// sendButtonInEdit[0].click() // sendButtonInEdit[0].click()

@ -11,9 +11,8 @@ const GIFEncoder = require('gifencoder')
const pngFileStream = require('png-file-stream') const pngFileStream = require('png-file-stream')
const sizeOfPng = require('image-size/lib/types/png') const sizeOfPng = require('image-size/lib/types/png')
const By = webdriver.By const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
const localesIndex = require('../../app/_locales/index.json') const localesIndex = require('../../app/_locales/index.json')
// const localesIndex = [] const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('../e2e/func')
const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545')) const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545'))
@ -50,11 +49,10 @@ async function captureAllScreens() {
await cleanScreenShotDir() await cleanScreenShotDir()
// setup selenium and install extension
const extPath = path.resolve('dist/chrome') const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath) driver = buildChromeWebDriver(extPath)
await driver.get('chrome://extensions-frame') const extensionId = await getExtensionIdChrome(driver)
const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("#container > div.items-container > extensions-item:nth-child(2)").getAttribute("id")')
await driver.get(`chrome-extension://${extensionId}/home.html`) await driver.get(`chrome-extension://${extensionId}/home.html`)
await delay(500) await delay(500)
tabs = await driver.getAllWindowHandles() tabs = await driver.getAllWindowHandles()
@ -165,7 +163,7 @@ async function captureAllScreens() {
await delay(300) await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - password entered') await captureLanguageScreenShots('metamask account detail export private key screen - password entered')
await driver.findElement(By.css('.btn-primary--lg.export-private-key__button')).click() await driver.findElement(By.css('.btn-primary.btn--large.export-private-key__button')).click()
await delay(300) await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - reveal key') await captureLanguageScreenShots('metamask account detail export private key screen - reveal key')

@ -511,17 +511,23 @@ function requestRevealSeedWords (password) {
} }
function resetAccount () { function resetAccount () {
return (dispatch) => { return dispatch => {
background.resetAccount((err, account) => { dispatch(actions.showLoadingIndication())
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
}
log.info('Transaction history reset for ' + account) return new Promise((resolve, reject) => {
dispatch(actions.showAccountsPage()) background.resetAccount((err, account) => {
}) dispatch(actions.hideLoadingIndication())
} if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
log.info('Transaction history reset for ' + account)
dispatch(actions.showAccountsPage())
resolve(account)
})
})
}
} }
function addNewKeyring (type, opts) { function addNewKeyring (type, opts) {
@ -1436,16 +1442,24 @@ function markAccountsFound () {
function retryTransaction (txId) { function retryTransaction (txId) {
log.debug(`background.retryTransaction`) log.debug(`background.retryTransaction`)
let newTxId
return (dispatch) => { return (dispatch) => {
background.retryTransaction(txId, (err, newState) => { return new Promise((resolve, reject) => {
if (err) { background.retryTransaction(txId, (err, newState) => {
return dispatch(actions.displayWarning(err.message)) if (err) {
} dispatch(actions.displayWarning(err.message))
const { selectedAddressTxList } = newState reject(err)
const { id: newTxId } = selectedAddressTxList[selectedAddressTxList.length - 1] }
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.viewPendingTx(newTxId)) const { selectedAddressTxList } = newState
const { id } = selectedAddressTxList[selectedAddressTxList.length - 1]
newTxId = id
resolve(newState)
})
}) })
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => newTxId)
} }
} }

@ -76,7 +76,7 @@ class App extends Component {
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }), h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
@ -137,7 +137,6 @@ class App extends Component {
(isLoading || isLoadingNetwork) && h(Loading, { (isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage, loadingMessage: loadMessage,
fullScreen: true,
}), }),
// content // content

@ -2,20 +2,15 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
const SECONDARY = 'secondary' const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary' const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_PRIMARY_LARGE = 'btn-primary--lg'
const CLASSNAME_SECONDARY = 'btn-secondary' const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_SECONDARY_LARGE = 'btn-secondary--lg' const CLASSNAME_LARGE = 'btn--large'
const getClassName = (type, large = false) => { const typeHash = {
let output = type === SECONDARY ? CLASSNAME_SECONDARY : CLASSNAME_PRIMARY default: CLASSNAME_DEFAULT,
primary: CLASSNAME_PRIMARY,
if (large) { secondary: CLASSNAME_SECONDARY,
output += ` ${type === SECONDARY ? CLASSNAME_SECONDARY_LARGE : CLASSNAME_PRIMARY_LARGE}`
}
return output
} }
class Button extends Component { class Button extends Component {
@ -24,7 +19,11 @@ class Button extends Component {
return ( return (
<button <button
className={classnames(getClassName(type, large), className)} className={classnames(
typeHash[type],
large && CLASSNAME_LARGE,
className
)}
{ ...buttonProps } { ...buttonProps }
> >
{ this.props.children } { this.props.children }

@ -13,13 +13,21 @@ storiesOf('Button', module)
{text('text', 'Click me')} {text('text', 'Click me')}
</Button> </Button>
) )
.add('secondary', () => ( .add('secondary', () =>
<Button <Button
onClick={action('clicked')} onClick={action('clicked')}
type="secondary" type="secondary"
> >
{text('text', 'Click me')} {text('text', 'Click me')}
</Button> </Button>
)
.add('default', () => (
<Button
onClick={action('clicked')}
type="default"
>
{text('text', 'Click me')}
</Button>
)) ))
.add('large primary', () => ( .add('large primary', () => (
<Button <Button
@ -39,3 +47,12 @@ storiesOf('Button', module)
{text('text', 'Click me')} {text('text', 'Click me')}
</Button> </Button>
)) ))
.add('large default', () => (
<Button
onClick={action('clicked')}
type="default"
large
>
{text('text', 'Click me')}
</Button>
))

@ -1,113 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = CurrencyInput
inherits(CurrencyInput, Component)
function CurrencyInput (props) {
Component.call(this)
const sanitizedValue = sanitizeValue(props.value)
this.state = {
value: sanitizedValue,
emptyState: false,
focused: false,
}
}
function removeNonDigits (str) {
return str.match(/\d|$/g).join('')
}
// Removes characters that are not digits, then removes leading zeros
function sanitizeInteger (val) {
return String(parseInt(removeNonDigits(val) || '0', 10))
}
function sanitizeDecimal (val) {
return removeNonDigits(val)
}
// Take a single string param and returns a non-negative integer or float as a string.
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
// Removes leading zeros from the integer, and non-digits from the integer and decimal
// The integer is returned as '0' in cases where it would be empty. A decimal point is
// included in the returned string if one is included in the param
// Examples:
// sanitizeValue('0') -> '0'
// sanitizeValue('a') -> '0'
// sanitizeValue('010.') -> '10.'
// sanitizeValue('0.005') -> '0.005'
// sanitizeValue('22.200') -> '22.200'
// sanitizeValue('.200') -> '0.200'
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
function sanitizeValue (value) {
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
integer = sanitizeInteger(integer) || '0'
decimal = sanitizeDecimal(decimal)
return `${integer}${point}${decimal}`
}
CurrencyInput.prototype.handleChange = function (newValue) {
const { onInputChange } = this.props
const { value } = this.state
let parsedValue = newValue
const newValueLastIndex = newValue.length - 1
if (value === '0' && newValue[newValueLastIndex] === '0') {
parsedValue = parsedValue.slice(0, newValueLastIndex)
}
const sanitizedValue = sanitizeValue(parsedValue)
this.setState({
value: sanitizedValue,
emptyState: newValue === '' && sanitizedValue === '0',
})
onInputChange(sanitizedValue)
}
// If state.value === props.value plus a decimal point, or at least one
// zero or a decimal point and at least one zero, then this returns state.value
// after it is sanitized with getValueParts
CurrencyInput.prototype.getValueToRender = function () {
const { value } = this.props
const { value: stateValue } = this.state
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
return sanitizeValue(trailingDecimalAndZeroes
? stateValue
: value)
}
CurrencyInput.prototype.render = function () {
const {
className,
placeholder,
readOnly,
inputRef,
type,
} = this.props
const { emptyState, focused } = this.state
const inputSizeMultiplier = readOnly ? 1 : 1.2
const valueToRender = this.getValueToRender()
return h('input', {
className,
type,
value: emptyState ? '' : valueToRender,
placeholder: focused ? '' : placeholder,
size: valueToRender.length * inputSizeMultiplier,
readOnly,
onFocus: () => this.setState({ focused: true, emptyState: valueToRender === '0' }),
onBlur: () => this.setState({ focused: false, emptyState: false }),
onChange: e => this.handleChange(e.target.value),
ref: inputRef,
})
}

@ -312,7 +312,7 @@ CustomizeGasModal.prototype.render = function () {
}, [this.context.t('revert')]), }, [this.context.t('revert')]),
h('div.send-v2__customize-gas__buttons', [ h('div.send-v2__customize-gas__buttons', [
h('button.btn-secondary.send-v2__customize-gas__cancel', { h('button.btn-default.send-v2__customize-gas__cancel', {
onClick: this.props.hideModal, onClick: this.props.hideModal,
style: { style: {
marginRight: '10px', marginRight: '10px',

@ -3,3 +3,5 @@
@import './info-box/index'; @import './info-box/index';
@import './pages/index'; @import './pages/index';
@import './modals/index';

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('./currency-input')
const { const {
addCurrencies, addCurrencies,
conversionGTE, conversionGTE,
@ -51,14 +50,15 @@ InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h(CurrencyInput, { h('input', {
className: 'customize-gas-input', className: 'customize-gas-input',
value, value,
placeholder, placeholder,
type: 'number', type: 'number',
onInputChange: newValue => { onChange: e => {
this.setValue(newValue) this.setValue(e.target.value)
}, },
min: 0,
}), }),
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [

@ -1,7 +1,6 @@
const { Component } = require('react') const { Component } = require('react')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const classnames = require('classnames')
const Spinner = require('../spinner') const Spinner = require('../spinner')
class LoadingScreen extends Component { class LoadingScreen extends Component {
@ -12,9 +11,7 @@ class LoadingScreen extends Component {
render () { render () {
return ( return (
h('.loading-overlay', { h('.loading-overlay', [
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
}, [
h('.loading-overlay__container', [ h('.loading-overlay__container', [
h(Spinner, { h(Spinner, {
color: '#F7C06C', color: '#F7C06C',
@ -29,7 +26,6 @@ class LoadingScreen extends Component {
LoadingScreen.propTypes = { LoadingScreen.propTypes = {
loadingMessage: PropTypes.string, loadingMessage: PropTypes.string,
fullScreen: PropTypes.bool,
} }
module.exports = LoadingScreen module.exports = LoadingScreen

@ -0,0 +1,54 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
class ConfirmResetAccount extends Component {
static propTypes = {
hideModal: PropTypes.func.isRequired,
resetAccount: PropTypes.func.isRequired,
}
static contextTypes = {
t: PropTypes.func,
}
handleReset () {
this.props.resetAccount()
.then(() => this.props.hideModal())
}
render () {
const { t } = this.context
return (
<div className="modal-container">
<div className="modal-container__content">
<div className="modal-container__title">
{ `${t('resetAccount')}?` }
</div>
<div className="modal-container__description">
{ t('resetAccountDescription') }
</div>
</div>
<div className="modal-container__footer">
<Button
type="default"
className="modal-container__footer-button"
onClick={() => this.props.hideModal()}
>
{ t('nevermind') }
</Button>
<Button
type="secondary"
className="modal-container__footer-button"
onClick={() => this.handleReset()}
>
{ t('reset') }
</Button>
</div>
</div>
)
}
}
export default ConfirmResetAccount

@ -0,0 +1,13 @@
import { connect } from 'react-redux'
import ConfirmResetAccount from './confirm-reset-account.component'
const { hideModal, resetAccount } = require('../../../actions')
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
resetAccount: () => dispatch(resetAccount()),
}
}
export default connect(null, mapDispatchToProps)(ConfirmResetAccount)

@ -0,0 +1,2 @@
import ConfirmResetAccount from './confirm-reset-account.container'
module.exports = ConfirmResetAccount

@ -109,7 +109,7 @@ DepositEtherModal.prototype.renderRow = function ({
]), ]),
!hideButton && h('div.deposit-ether-modal__buy-row__button', [ !hideButton && h('div.deposit-ether-modal__buy-row__button', [
h('button.btn-primary--lg.deposit-ether-modal__deposit-button', { h('button.btn-primary.btn--large.deposit-ether-modal__deposit-button', {
onClick: onButtonClick, onClick: onButtonClick,
}, [buttonLabel]), }, [buttonLabel]),
]), ]),

@ -87,14 +87,14 @@ ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, lab
ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) { ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
return h('div.export-private-key-buttons', {}, [ return h('div.export-private-key-buttons', {}, [
!privateKey && this.renderButton( !privateKey && this.renderButton(
'btn-secondary--lg export-private-key__button export-private-key__button--cancel', 'btn-default btn--large export-private-key__button export-private-key__button--cancel',
() => hideModal(), () => hideModal(),
'Cancel' 'Cancel'
), ),
(privateKey (privateKey
? this.renderButton('btn-primary--lg export-private-key__button', () => hideModal(), this.context.t('done')) ? this.renderButton('btn-primary btn--large export-private-key__button', () => hideModal(), this.context.t('done'))
: this.renderButton('btn-primary--lg export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm')) : this.renderButton('btn-primary btn--large export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm'))
), ),
]) ])

@ -0,0 +1,52 @@
.modal-container {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-flow: column;
border-radius: 8px;
&__title {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0;
text-align: center;
}
&__description {
text-align: center;
font-size: .875rem;
}
&__content {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px;
@media screen and (max-width: 575px) {
justify-content: center;
padding: 28px 20px;
}
}
&__footer {
display: flex;
flex-flow: row;
justify-content: center;
border-top: 1px solid #d2d8dd;
padding: 16px;
flex: 0 0 auto;
&-button {
min-width: 0;
margin-right: 16px;
&:last-of-type {
margin-right: 0;
}
}
}
}

@ -19,7 +19,30 @@ const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal') const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal') const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./notification-modals/confirm-reset-account') const ConfirmResetAccount = require('./confirm-reset-account')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
border: '1px solid #CCCFD1',
borderRadius: '8px',
backgroundColor: '#FFFFFF',
boxShadow: '0 2px 22px 0 rgba(0,0,0,0.2)',
}
const modalContainerLaptopStyle = {
...modalContainerBaseStyle,
width: '344px',
top: '15%',
}
const modalContainerMobileStyle = {
...modalContainerBaseStyle,
width: '309px',
top: '12.5%',
}
const accountModalStyle = { const accountModalStyle = {
mobileModalStyle: { mobileModalStyle: {
@ -173,18 +196,18 @@ const MODALS = {
BETA_UI_NOTIFICATION_MODAL: { BETA_UI_NOTIFICATION_MODAL: {
contents: [ contents: [
h(NotifcationModal, { h(Notification, [
header: 'uiWelcome', h(WelcomeBeta),
message: 'uiWelcomeMessage', ]),
}),
], ],
mobileModalStyle: { mobileModalStyle: {
width: '95%', ...modalContainerMobileStyle,
top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
}, },
laptopModalStyle: { laptopModalStyle: {
width: '449px', ...modalContainerLaptopStyle,
top: 'calc(33% + 45px)', },
contentStyle: {
borderRadius: '8px',
}, },
}, },
@ -208,12 +231,13 @@ const MODALS = {
CONFIRM_RESET_ACCOUNT: { CONFIRM_RESET_ACCOUNT: {
contents: h(ConfirmResetAccount), contents: h(ConfirmResetAccount),
mobileModalStyle: { mobileModalStyle: {
width: '95%', ...modalContainerMobileStyle,
top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
}, },
laptopModalStyle: { laptopModalStyle: {
width: '473px', ...modalContainerLaptopStyle,
top: 'calc(33% + 45px)', },
contentStyle: {
borderRadius: '8px',
}, },
}, },
@ -265,6 +289,24 @@ const MODALS = {
}, },
}, },
TRANSACTION_CONFIRMED: {
disableBackdropClick: true,
contents: [
h(Notification, [
h(TransactionConfirmed),
]),
],
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
DEFAULT: { DEFAULT: {
contents: [], contents: [],
mobileModalStyle: {}, mobileModalStyle: {},
@ -306,7 +348,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal)
Modal.prototype.render = function () { Modal.prototype.render = function () {
const modal = MODALS[this.props.modalState.name || 'DEFAULT'] const modal = MODALS[this.props.modalState.name || 'DEFAULT']
const children = modal.contents const { contents: children, disableBackdropClick = false } = modal
const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle'] const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle']
const contentStyle = modal.contentStyle || {} const contentStyle = modal.contentStyle || {}
@ -326,6 +368,7 @@ Modal.prototype.render = function () {
modalStyle, modalStyle,
contentStyle, contentStyle,
backdropStyle: BACKDROPSTYLE, backdropStyle: BACKDROPSTYLE,
closeOnClick: !disableBackdropClick,
}, },
children, children,
) )

@ -1,46 +0,0 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../actions')
const NotifcationModal = require('../notification-modal')
class ConfirmResetAccount extends Component {
render () {
const { resetAccount } = this.props
return h(NotifcationModal, {
header: 'Are you sure you want to reset account?',
message: h('div', [
h('span', `Resetting is for developer use only. This button wipes the current account's transaction history,
which is used to calculate the current account nonce. `),
h('a.notification-modal__link', {
href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account',
target: '_blank',
onClick (event) { global.platform.openWindow({ url: event.target.href }) },
}, 'Read more.'),
]),
showCancelButton: true,
showConfirmButton: true,
onConfirm: resetAccount,
})
}
}
ConfirmResetAccount.propTypes = {
resetAccount: PropTypes.func,
}
const mapDispatchToProps = dispatch => {
return {
resetAccount: () => {
dispatch(actions.resetAccount())
},
}
}
module.exports = connect(null, mapDispatchToProps)(ConfirmResetAccount)

@ -0,0 +1,2 @@
import Notification from './notification.container'
module.exports = Notification

@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
const Notification = (props, context) => {
return (
<div className="modal-container">
{ props.children }
<div className="modal-container__footer">
<Button
type="primary"
onClick={() => props.onHide()}
>
{ context.t('ok') }
</Button>
</div>
</div>
)
}
Notification.propTypes = {
onHide: PropTypes.func.isRequired,
children: PropTypes.element,
}
Notification.contextTypes = {
t: PropTypes.func,
}
export default Notification

@ -0,0 +1,38 @@
import { connect } from 'react-redux'
import Notification from './notification.component'
const { hideModal } = require('../../../actions')
const mapStateToProps = state => {
const { appState: { modal: { modalState: { props } } } } = state
const { onHide } = props
return {
onHide,
}
}
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { onHide, ...otherStateProps } = stateProps
const { hideModal, ...otherDispatchProps } = dispatchProps
return {
...otherStateProps,
...otherDispatchProps,
...ownProps,
onHide: () => {
hideModal()
if (onHide && typeof onHide === 'function') {
onHide()
}
},
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notification)

@ -0,0 +1,2 @@
import TransactionConfirmed from './transaction-confirmed.component'
module.exports = TransactionConfirmed

@ -0,0 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
const TransactionConfirmed = (props, context) => {
const { t } = context
return (
<div className="modal-container__content">
<img src="images/check-icon.svg" />
<div className="modal-container__title">
{ `${t('confirmed')}!` }
</div>
<div className="modal-container__description">
{ t('initialTransactionConfirmed') }
</div>
</div>
)
}
TransactionConfirmed.contextTypes = {
t: PropTypes.func,
}
export default TransactionConfirmed

@ -0,0 +1,2 @@
import WelcomeBeta from './welcome-beta.component'
module.exports = WelcomeBeta

@ -0,0 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
const TransactionConfirmed = (props, context) => {
const { t } = context
return (
<div className="modal-container__content">
<div className="modal-container__title">
{ `${t('uiWelcome')}` }
</div>
<div className="modal-container__description">
{ t('uiWelcomeMessage') }
</div>
</div>
)
}
TransactionConfirmed.contextTypes = {
t: PropTypes.func,
}
export default TransactionConfirmed

@ -29,7 +29,7 @@ export default class PageContainerFooter extends Component {
<div className="page-container__footer"> <div className="page-container__footer">
<Button <Button
type="secondary" type="default"
large={true} large={true}
className="page-container__footer-button" className="page-container__footer-button"
onClick={() => onCancel()} onClick={() => onCancel()}

@ -323,7 +323,7 @@ class AddToken extends Component {
</div> </div>
<div className="page-container__footer"> <div className="page-container__footer">
<Button <Button
type="secondary" type="default"
large large
className="page-container__footer-button" className="page-container__footer-button"
onClick={() => { onClick={() => {

@ -11,6 +11,10 @@
width: 50%; width: 50%;
text-align: center; text-align: center;
margin-top: 8px; margin-top: 8px;
@media screen and (max-width: 575px) {
width: 60%;
}
} }
&__link { &__link {

@ -87,7 +87,7 @@ export default class ConfirmAddToken extends Component {
</div> </div>
<div className="page-container__footer"> <div className="page-container__footer">
<Button <Button
type="secondary" type="default"
large large
className="page-container__footer-button" className="page-container__footer-button"
onClick={() => history.push(ADD_TOKEN_ROUTE)} onClick={() => history.push(ADD_TOKEN_ROUTE)}

@ -51,7 +51,7 @@ class JsonImportSubview extends Component {
h('div.new-account-create-form__buttons', {}, [ h('div.new-account-create-form__buttons', {}, [
h('button.btn-secondary.new-account-create-form__button', { h('button.btn-default.new-account-create-form__button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
@ -105,7 +105,7 @@ class JsonImportSubview extends Component {
} }
this.props.importNewJsonAccount([ fileContents, password ]) this.props.importNewJsonAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux // JS runtime requires caught rejections but failures are handled by Redux
.catch() .catch()
} }
} }

@ -59,13 +59,13 @@ PrivateKeyImportView.prototype.render = function () {
h('div.new-account-import-form__buttons', {}, [ h('div.new-account-import-form__buttons', {}, [
h('button.btn-secondary--lg.new-account-create-form__button', { h('button.btn-default.btn--large.new-account-create-form__button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
h('button.btn-primary--lg.new-account-create-form__button', { h('button.btn-primary.btn--large.new-account-create-form__button', {
onClick: () => this.createNewKeychain(), onClick: () => this.createNewKeychain(),
}, [ }, [
this.context.t('import'), this.context.t('import'),
@ -91,7 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
const { importNewAccount, history } = this.props const { importNewAccount, history } = this.props
importNewAccount('Private Key', [ privateKey ]) importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux // JS runtime requires caught rejections but failures are handled by Redux
.catch() .catch()
.then(() => history.push(DEFAULT_ROUTE)) .then(() => history.push(DEFAULT_ROUTE))
} }

@ -38,13 +38,13 @@ class NewAccountCreateForm extends Component {
h('div.new-account-create-form__buttons', {}, [ h('div.new-account-create-form__buttons', {}, [
h('button.btn-secondary--lg.new-account-create-form__button', { h('button.btn-default.btn--large.new-account-create-form__button', {
onClick: () => history.push(DEFAULT_ROUTE), onClick: () => history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
h('button.btn-primary--lg.new-account-create-form__button', { h('button.btn-primary.btn--large.new-account-create-form__button', {
onClick: () => { onClick: () => {
createAccount(newAccountName || defaultAccountName) createAccount(newAccountName || defaultAccountName)
.then(() => history.push(DEFAULT_ROUTE)) .then(() => history.push(DEFAULT_ROUTE))

@ -106,10 +106,10 @@ class RevealSeedPage extends Component {
renderPasswordPromptFooter () { renderPasswordPromptFooter () {
return ( return (
h('.page-container__footer', [ h('.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', { h('button.btn-default.btn--large.page-container__footer-button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('cancel')), }, this.context.t('cancel')),
h('button.btn-primary--lg.page-container__footer-button', { h('button.btn-primary.btn--large.page-container__footer-button', {
onClick: event => this.handleSubmit(event), onClick: event => this.handleSubmit(event),
disabled: this.state.password === '', disabled: this.state.password === '',
}, this.context.t('next')), }, this.context.t('next')),
@ -120,7 +120,7 @@ class RevealSeedPage extends Component {
renderRevealSeedFooter () { renderRevealSeedFooter () {
return ( return (
h('.page-container__footer', [ h('.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', { h('button.btn-default.btn--large.page-container__footer-button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('close')), }, this.context.t('close')),
]) ])

@ -217,7 +217,7 @@ class Settings extends Component {
]), ]),
h('div.settings__content-item', [ h('div.settings__content-item', [
h('div.settings__content-item-col', [ h('div.settings__content-item-col', [
h('button.btn-primary--lg.settings__button', { h('button.btn-primary.btn--large.settings__button', {
onClick (event) { onClick (event) {
window.logStateString((err, result) => { window.logStateString((err, result) => {
if (err) { if (err) {
@ -242,7 +242,7 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('revealSeedWords')), h('div.settings__content-item', this.context.t('revealSeedWords')),
h('div.settings__content-item', [ h('div.settings__content-item', [
h('div.settings__content-item-col', [ h('div.settings__content-item-col', [
h('button.btn-primary--lg.settings__button--red', { h('button.btn-primary.btn--large.settings__button--red', {
onClick: event => { onClick: event => {
event.preventDefault() event.preventDefault()
history.push(REVEAL_SEED_ROUTE) history.push(REVEAL_SEED_ROUTE)
@ -262,7 +262,7 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('useOldUI')), h('div.settings__content-item', this.context.t('useOldUI')),
h('div.settings__content-item', [ h('div.settings__content-item', [
h('div.settings__content-item-col', [ h('div.settings__content-item-col', [
h('button.btn-primary--lg.settings__button--orange', { h('button.btn-primary.btn--large.settings__button--orange', {
onClick (event) { onClick (event) {
event.preventDefault() event.preventDefault()
setFeatureFlagToBeta() setFeatureFlagToBeta()
@ -281,7 +281,7 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('resetAccount')), h('div.settings__content-item', this.context.t('resetAccount')),
h('div.settings__content-item', [ h('div.settings__content-item', [
h('div.settings__content-item-col', [ h('div.settings__content-item-col', [
h('button.btn-primary--lg.settings__button--orange', { h('button.btn-primary.btn--large.settings__button--orange', {
onClick (event) { onClick (event) {
event.preventDefault() event.preventDefault()
showResetAccountConfirmationModal() showResetAccountConfirmationModal()

@ -34,14 +34,7 @@ class UnlockPage extends Component {
} }
} }
tryUnlockMetamask (password) { async handleSubmit (event) {
const { tryUnlockMetamask, history } = this.props
tryUnlockMetamask(password)
.then(() => history.push(DEFAULT_ROUTE))
.catch(({ message }) => this.setState({ error: message }))
}
handleSubmit (event) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -54,9 +47,14 @@ class UnlockPage extends Component {
this.setState({ error: null }) this.setState({ error: null })
tryUnlockMetamask(password) try {
.then(() => history.push(DEFAULT_ROUTE)) await tryUnlockMetamask(password)
.catch(({ message }) => this.setState({ error: message })) } catch ({ message }) {
this.setState({ error: message })
return
}
history.push(DEFAULT_ROUTE)
} }
handleInputChange ({ target }) { handleInputChange ({ target }) {

@ -295,18 +295,48 @@ ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, curren
: value : value
} }
ConfirmSendEther.prototype.editTransaction = function (txMeta) { ConfirmSendEther.prototype.editTransaction = function () {
const { editTransaction, history } = this.props const { editTransaction, history } = this.props
const txMeta = this.gatherTxMeta()
editTransaction(txMeta) editTransaction(txMeta)
history.push(SEND_ROUTE) history.push(SEND_ROUTE)
} }
ConfirmSendEther.prototype.renderNetworkDisplay = function () { ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) {
const windowType = window.METAMASK_UI_TYPE const windowType = window.METAMASK_UI_TYPE
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
windowType !== ENVIRONMENT_TYPE_POPUP
return (windowType === ENVIRONMENT_TYPE_NOTIFICATION || windowType === ENVIRONMENT_TYPE_POPUP) if (isTxReprice && isFullScreen) {
? h(NetworkDisplay) return null
: null }
return (
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => this.editTransaction(),
style: {
visibility: isTxReprice ? 'hidden' : 'initial',
},
}, 'Edit'),
!isFullScreen && h(NetworkDisplay),
])
)
}
ConfirmSendEther.prototype.renderHeader = function (isTxReprice) {
const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
const subtitle = isTxReprice
? this.context.t('speedUpSubtitle')
: this.context.t('pleaseReviewTransaction')
return (
h('.page-container__header', [
this.renderHeaderRow(isTxReprice),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
])
)
} }
ConfirmSendEther.prototype.render = function () { ConfirmSendEther.prototype.render = function () {
@ -324,6 +354,7 @@ ConfirmSendEther.prototype.render = function () {
}, },
} = this.props } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const isTxReprice = Boolean(txMeta.lastGasPrice)
const txParams = txMeta.txParams || {} const txParams = txMeta.txParams || {}
const { const {
@ -342,11 +373,6 @@ ConfirmSendEther.prototype.render = function () {
totalInETH, totalInETH,
} = this.getData() } = this.getData()
const title = txMeta.lastGasPrice ? 'Reprice Transaction' : 'Confirm'
const subtitle = txMeta.lastGasPrice
? 'Increase your gas fee to attempt to overwrite and speed up your transaction'
: 'Please review your transaction.'
const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency) const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency)
const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
@ -366,19 +392,7 @@ ConfirmSendEther.prototype.render = function () {
return ( return (
// Main Send token Card // Main Send token Card
h('.page-container', [ h('.page-container', [
h('.page-container__header', [ this.renderHeader(isTxReprice),
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => this.editTransaction(txMeta),
style: {
visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden',
},
}, 'Edit'),
this.renderNetworkDisplay(),
]),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
]),
h('.page-container__content', [ h('.page-container__content', [
h(SenderToRecipient, { h(SenderToRecipient, {
senderName: fromName, senderName: fromName,

@ -12,6 +12,7 @@ const actions = require('../../actions')
const clone = require('clone') const clone = require('clone')
const Identicon = require('../identicon') const Identicon = require('../identicon')
const GasFeeDisplay = require('../send/gas-fee-display-v2.js') const GasFeeDisplay = require('../send/gas-fee-display-v2.js')
const NetworkDisplay = require('../network-display')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN const BN = ethUtil.BN
const { const {
@ -43,6 +44,11 @@ import {
updateSendErrors, updateSendErrors,
} from '../../ducks/send.duck' } from '../../ducks/send.duck'
const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
} = require('../../../../app/scripts/lib/enums')
ConfirmSendToken.contextTypes = { ConfirmSendToken.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
@ -434,6 +440,43 @@ ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, curren
: value : value
} }
ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) {
const windowType = window.METAMASK_UI_TYPE
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
windowType !== ENVIRONMENT_TYPE_POPUP
if (isTxReprice && isFullScreen) {
return null
}
return (
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => this.editTransaction(),
style: {
visibility: isTxReprice ? 'hidden' : 'initial',
},
}, 'Edit'),
!isFullScreen && h(NetworkDisplay),
])
)
}
ConfirmSendToken.prototype.renderHeader = function (isTxReprice) {
const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
const subtitle = isTxReprice
? this.context.t('speedUpSubtitle')
: this.context.t('pleaseReviewTransaction')
return (
h('.page-container__header', [
this.renderHeaderRow(isTxReprice),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
])
)
}
ConfirmSendToken.prototype.render = function () { ConfirmSendToken.prototype.render = function () {
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const { const {
@ -447,25 +490,13 @@ ConfirmSendToken.prototype.render = function () {
}, },
} = this.getData() } = this.getData()
this.inputs = []
const isTxReprice = Boolean(txMeta.lastGasPrice) const isTxReprice = Boolean(txMeta.lastGasPrice)
const title = isTxReprice ? this.context.t('reprice_title') : this.context.t('confirm')
const subtitle = isTxReprice
? this.context.t('reprice_subtitle')
: this.context.t('pleaseReviewTransaction')
return ( return (
h('div.confirm-screen-container.confirm-send-token', [ h('div.confirm-screen-container.confirm-send-token', [
// Main Send token Card // Main Send token Card
h('div.page-container', [ h('div.page-container', [
h('div.page-container__header', [ this.renderHeader(isTxReprice),
!txMeta.lastGasPrice && h('button.confirm-screen-back-button', {
onClick: () => this.editTransaction(txMeta),
}, this.context.t('edit')),
h('div.page-container__title', title),
h('div.page-container__subtitle', subtitle),
]),
h('.page-container__content', [ h('.page-container__content', [
h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.flex-row.flex-center.confirm-screen-identicons', [
h('div.confirm-screen-account-wrapper', [ h('div.confirm-screen-account-wrapper', [

@ -130,7 +130,6 @@ PendingTx.prototype.render = function () {
if (isFetching) { if (isFetching) {
return h(Loading, { return h(Loading, {
fullScreen: true,
loadingMessage: this.context.t('generatingTransaction'), loadingMessage: this.context.t('generatingTransaction'),
}) })
} }
@ -157,9 +156,7 @@ PendingTx.prototype.render = function () {
sendTransaction, sendTransaction,
}) })
default: default:
return h(Loading, { return h(Loading)
fullScreen: true,
})
} }
} }

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('../currency-input')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
@ -22,20 +21,36 @@ function toHexWei (value) {
}) })
} }
CurrencyDisplay.prototype.componentWillMount = function () {
this.setState({
valueToRender: this.getValueToRender(this.props),
})
}
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) {
const currentValueToRender = this.getValueToRender(this.props)
const newValueToRender = this.getValueToRender(nextProps)
if (currentValueToRender !== newValueToRender) {
this.setState({
valueToRender: newValueToRender,
})
}
}
CurrencyDisplay.prototype.getAmount = function (value) { CurrencyDisplay.prototype.getAmount = function (value) {
const { selectedToken } = this.props const { selectedToken } = this.props
const { decimals } = selectedToken || {} const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'}) const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'})
return selectedToken return selectedToken
? sendAmount ? sendAmount
: toHexWei(value) : toHexWei(value)
} }
CurrencyDisplay.prototype.getValueToRender = function () { CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value }) {
const { selectedToken, conversionRate, value } = this.props if (value === '0x0') return '0'
const { decimals, symbol } = selectedToken || {} const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
@ -76,6 +91,18 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue : convertedValue
} }
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: newVal })
this.props.onChange(this.getAmount(newVal))
}
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
const valueString = String(valueToRender)
const valueLength = valueString.length || 1
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
CurrencyDisplay.prototype.render = function () { CurrencyDisplay.prototype.render = function () {
const { const {
className = 'currency-display', className = 'currency-display',
@ -85,10 +112,10 @@ CurrencyDisplay.prototype.render = function () {
convertedCurrency, convertedCurrency,
readOnly = false, readOnly = false,
inError = false, inError = false,
handleChange, onBlur,
} = this.props } = this.props
const { valueToRender } = this.state
const valueToRender = this.getValueToRender()
const convertedValueToRender = this.getConvertedValueToRender(valueToRender) const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', { return h('div', {
@ -96,24 +123,30 @@ CurrencyDisplay.prototype.render = function () {
style: { style: {
borderColor: inError ? 'red' : null, borderColor: inError ? 'red' : null,
}, },
onClick: () => this.currencyInput && this.currencyInput.focus(), onClick: () => {
this.currencyInput && this.currencyInput.focus()
},
}, [ }, [
h('div.currency-display__primary-row', [ h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [ h('div.currency-display__input-wrapper', [
h(readOnly ? 'input' : CurrencyInput, { h('input', {
className: primaryBalanceClassName, className: primaryBalanceClassName,
value: `${valueToRender}`, value: `${valueToRender}`,
placeholder: '0', placeholder: '0',
type: 'number',
readOnly, readOnly,
...(!readOnly ? { ...(!readOnly ? {
onInputChange: newValue => { onChange: e => this.handleChange(e.target.value),
handleChange(this.getAmount(newValue)) onBlur: () => onBlur(this.getAmount(valueToRender)),
},
inputRef: input => { this.currencyInput = input },
} : {}), } : {}),
ref: input => { this.currencyInput = input },
style: {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
}), }),
h('span.currency-display__currency-symbol', primaryCurrency), h('span.currency-display__currency-symbol', primaryCurrency),

@ -49,11 +49,10 @@ export default class SendAmountRow extends Component {
}) })
} }
handleAmountChange (amount) { updateAmount (amount) {
const { updateSendAmount, setMaxModeTo } = this.props const { updateSendAmount, setMaxModeTo } = this.props
setMaxModeTo(false) setMaxModeTo(false)
this.validateAmount(amount)
updateSendAmount(amount) updateSendAmount(amount)
} }
@ -78,7 +77,8 @@ export default class SendAmountRow extends Component {
<CurrencyDisplay <CurrencyDisplay
conversionRate={amountConversionRate} conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency} convertedCurrency={convertedCurrency}
handleChange={newAmount => this.handleAmountChange(newAmount)} onBlur={newAmount => this.updateAmount(newAmount)}
onChange={newAmount => this.validateAmount(newAmount)}
inError={inError} inError={inError}
primaryCurrency={primaryCurrency || 'ETH'} primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken} selectedToken={selectedToken}

@ -14,7 +14,7 @@ const propsMethodSpies = {
updateSendAmountError: sinon.spy(), updateSendAmountError: sinon.spy(),
} }
sinon.spy(SendAmountRow.prototype, 'handleAmountChange') sinon.spy(SendAmountRow.prototype, 'updateAmount')
sinon.spy(SendAmountRow.prototype, 'validateAmount') sinon.spy(SendAmountRow.prototype, 'validateAmount')
describe('SendAmountRow Component', function () { describe('SendAmountRow Component', function () {
@ -45,7 +45,7 @@ describe('SendAmountRow Component', function () {
propsMethodSpies.updateSendAmount.resetHistory() propsMethodSpies.updateSendAmount.resetHistory()
propsMethodSpies.updateSendAmountError.resetHistory() propsMethodSpies.updateSendAmountError.resetHistory()
SendAmountRow.prototype.validateAmount.resetHistory() SendAmountRow.prototype.validateAmount.resetHistory()
SendAmountRow.prototype.handleAmountChange.resetHistory() SendAmountRow.prototype.updateAmount.resetHistory()
}) })
describe('validateAmount', () => { describe('validateAmount', () => {
@ -71,11 +71,11 @@ describe('SendAmountRow Component', function () {
}) })
describe('handleAmountChange', () => { describe('updateAmount', () => {
it('should call setMaxModeTo', () => { it('should call setMaxModeTo', () => {
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
instance.handleAmountChange('someAmount') instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.setMaxModeTo.getCall(0).args, propsMethodSpies.setMaxModeTo.getCall(0).args,
@ -83,19 +83,9 @@ describe('SendAmountRow Component', function () {
) )
}) })
it('should call this.validateAmount', () => {
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
instance.handleAmountChange('someAmount')
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendAmount.getCall(0).args,
['someAmount']
)
})
it('should call updateSendAmount', () => { it('should call updateSendAmount', () => {
assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
instance.handleAmountChange('someAmount') instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.updateSendAmount.getCall(0).args, propsMethodSpies.updateSendAmount.getCall(0).args,
@ -136,7 +126,8 @@ describe('SendAmountRow Component', function () {
const { const {
conversionRate, conversionRate,
convertedCurrency, convertedCurrency,
handleChange, onBlur,
onChange,
inError, inError,
primaryCurrency, primaryCurrency,
selectedToken, selectedToken,
@ -148,11 +139,18 @@ describe('SendAmountRow Component', function () {
assert.equal(primaryCurrency, 'mockPrimaryCurrency') assert.equal(primaryCurrency, 'mockPrimaryCurrency')
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
assert.equal(value, 'mockAmount') assert.equal(value, 'mockAmount')
assert.equal(SendAmountRow.prototype.handleAmountChange.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
handleChange('mockNewAmount') onBlur('mockNewAmount')
assert.equal(SendAmountRow.prototype.handleAmountChange.callCount, 1) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
assert.deepEqual(
SendAmountRow.prototype.updateAmount.getCall(0).args,
['mockNewAmount']
)
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
onChange('mockNewAmount')
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
assert.deepEqual( assert.deepEqual(
SendAmountRow.prototype.handleAmountChange.getCall(0).args, SendAmountRow.prototype.validateAmount.getCall(0).args,
['mockNewAmount'] ['mockNewAmount']
) )
}) })

@ -25,7 +25,7 @@ const SendTransactionScreen = proxyquire('../send.component.js', {
sinon.spy(SendTransactionScreen.prototype, 'componentDidMount') sinon.spy(SendTransactionScreen.prototype, 'componentDidMount')
sinon.spy(SendTransactionScreen.prototype, 'updateGas') sinon.spy(SendTransactionScreen.prototype, 'updateGas')
describe.only('Send Component', function () { describe('Send Component', function () {
let wrapper let wrapper
beforeEach(() => { beforeEach(() => {

@ -242,7 +242,7 @@ ShapeshiftForm.prototype.render = function () {
]), ]),
!depositAddress && h('button.btn-primary--lg.shapeshift-form__shapeshift-buy-btn', { !depositAddress && h('button.btn-primary.btn--large.shapeshift-form__shapeshift-buy-btn', {
className: btnClass, className: btnClass,
disabled: !token, disabled: !token,
onClick: () => this.onBuyWithShapeShift(), onClick: () => this.onBuyWithShapeShift(),

@ -235,12 +235,12 @@ SignatureRequest.prototype.renderFooter = function () {
} }
return h('div.request-signature__footer', [ return h('div.request-signature__footer', [
h('button.btn-secondary--lg.request-signature__footer__cancel-button', { h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
onClick: event => { onClick: event => {
cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE)) cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE))
}, },
}, this.context.t('cancel')), }, this.context.t('cancel')),
h('button.btn-primary--lg', { h('button.btn-primary.btn--large', {
onClick: event => { onClick: event => {
sign(event).then(() => this.props.history.push(DEFAULT_ROUTE)) sign(event).then(() => this.props.history.push(DEFAULT_ROUTE))
}, },

@ -1,8 +1,15 @@
import React, { Component } from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { default as MaterialTextField } from '@material-ui/core/TextField' import { default as MaterialTextField } from '@material-ui/core/TextField'
const inputLabelBase = {
transform: 'none',
transition: 'none',
position: 'initial',
color: '#5b5b5b',
}
const styles = { const styles = {
materialLabel: { materialLabel: {
'&$materialFocused': { '&$materialFocused': {
@ -46,57 +53,57 @@ const styles = {
border: '1px solid #2f9ae0', border: '1px solid #2f9ae0',
}, },
}, },
largeInputLabel: {
...inputLabelBase,
fontSize: '1rem',
},
inputLabel: { inputLabel: {
...inputLabelBase,
fontSize: '.75rem', fontSize: '.75rem',
transform: 'none',
transition: 'none',
position: 'initial',
color: '#5b5b5b',
}, },
} }
class TextField extends Component { const TextField = props => {
static defaultProps = { const { error, classes, material, startAdornment, largeLabel, ...textFieldProps } = props
error: null,
}
static propTypes = { return (
error: PropTypes.string, <MaterialTextField
classes: PropTypes.object, error={Boolean(error)}
material: PropTypes.bool, helperText={error}
startAdornment: PropTypes.element, InputLabelProps={{
} shrink: material ? undefined : true,
className: material ? '' : (largeLabel ? classes.largeInputLabel : classes.inputLabel),
FormLabelClasses: {
root: material ? classes.materialLabel : classes.formLabel,
focused: material ? classes.materialFocused : classes.formLabelFocused,
error: classes.materialError,
},
}}
InputProps={{
startAdornment: startAdornment || undefined,
disableUnderline: !material,
classes: {
root: material ? '' : classes.inputRoot,
input: material ? '' : classes.input,
underline: material ? classes.materialUnderline : '',
focused: material ? '' : classes.inputFocused,
},
}}
{...textFieldProps}
/>
)
}
render () { TextField.defaultProps = {
const { error, classes, material, startAdornment, ...textFieldProps } = this.props error: null,
}
return ( TextField.propTypes = {
<MaterialTextField error: PropTypes.string,
error={Boolean(error)} classes: PropTypes.object,
helperText={error} material: PropTypes.bool,
InputLabelProps={{ startAdornment: PropTypes.element,
shrink: material ? undefined : true, largeLabel: PropTypes.bool,
className: material ? '' : classes.inputLabel,
FormLabelClasses: {
root: material ? classes.materialLabel : classes.formLabel,
focused: material ? classes.materialFocused : classes.formLabelFocused,
error: classes.materialError,
},
}}
InputProps={{
startAdornment: startAdornment || undefined,
disableUnderline: !material,
classes: {
root: material ? '' : classes.inputRoot,
input: material ? '' : classes.input,
underline: material ? classes.materialUnderline : '',
focused: material ? '' : classes.inputFocused,
},
}}
{...textFieldProps}
/>
)
}
} }
export default withStyles(styles)(TextField) export default withStyles(styles)(TextField)

@ -22,3 +22,32 @@ storiesOf('TextField', module)
error="Invalid value" error="Invalid value"
/> />
) )
.add('Mascara text', () =>
<TextField
label="Text"
type="text"
largeLabel
/>
)
.add('Material text', () =>
<TextField
label="Text"
type="text"
material
/>
)
.add('Material password', () =>
<TextField
label="Password"
type="password"
material
/>
)
.add('Material error', () =>
<TextField
type="text"
label="Name"
error="Invalid value"
material
/>
)

@ -1,5 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const { compose } = require('recompose')
const { withRouter } = require('react-router-dom')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const inherits = require('util').inherits const inherits = require('util').inherits
@ -16,13 +18,16 @@ const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
const { calcTokenAmount } = require('../token-util') const { calcTokenAmount } = require('../token-util')
const { getCurrentCurrency } = require('../selectors') const { getCurrentCurrency } = require('../selectors')
const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
TxListItem.contextTypes = { TxListItem.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(mapStateToProps, mapDispatchToProps)(TxListItem) module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TxListItem)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
@ -216,6 +221,7 @@ TxListItem.prototype.setSelectedToken = function (tokenAddress) {
TxListItem.prototype.resubmit = function () { TxListItem.prototype.resubmit = function () {
const { transactionId } = this.props const { transactionId } = this.props
this.props.retryTransaction(transactionId) this.props.retryTransaction(transactionId)
.then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
} }
TxListItem.prototype.render = function () { TxListItem.prototype.render = function () {

@ -7,6 +7,7 @@ const { compose } = require('recompose')
const actions = require('./actions') const actions = require('./actions')
const txHelper = require('../lib/tx-helper') const txHelper = require('../lib/tx-helper')
const log = require('loglevel') const log = require('loglevel')
const R = require('ramda')
const PendingTx = require('./components/pending-tx') const PendingTx = require('./components/pending-tx')
const SignatureRequest = require('./components/signature-request') const SignatureRequest = require('./components/signature-request')
@ -87,37 +88,74 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
network, network,
selectedAddressTxList, selectedAddressTxList,
send, send,
history,
match: { params: { id: transactionId } = {} },
} = this.props } = this.props
const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) let prevTx
const prevTxData = prevUnconfTxList[prevIndex] || {}
const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} if (transactionId) {
prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList)
} else {
const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network)
const prevTxData = prevUnconfTxList[prevIndex] || {}
prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {}
}
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
if (unconfTxList.length === 0 && if (prevTx.status === 'dropped') {
(prevTx.status === 'dropped' || !send.to && this.getUnapprovedMessagesTotal() === 0)) { this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
onHide: () => history.push(DEFAULT_ROUTE),
}))
return
}
if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
this.props.history.push(DEFAULT_ROUTE) this.props.history.push(DEFAULT_ROUTE)
} }
} }
ConfirmTxScreen.prototype.render = function () { ConfirmTxScreen.prototype.getTxData = function () {
const props = this.props
const { const {
network, network,
index,
unapprovedTxs,
unapprovedMsgs,
unapprovedPersonalMsgs,
unapprovedTypedMessages,
match: { params: { id: transactionId } = {} },
} = this.props
const unconfTxList = txHelper(
unapprovedTxs, unapprovedTxs,
currentCurrency,
unapprovedMsgs, unapprovedMsgs,
unapprovedPersonalMsgs, unapprovedPersonalMsgs,
unapprovedTypedMessages, unapprovedTypedMessages,
network
)
log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`)
return transactionId
? R.find(({ id }) => id + '' === transactionId)(unconfTxList)
: unconfTxList[index]
}
ConfirmTxScreen.prototype.render = function () {
const props = this.props
const {
currentCurrency,
conversionRate, conversionRate,
blockGasLimit, blockGasLimit,
// provider, // provider,
// computedBalances, // computedBalances,
} = props } = props
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) var txData = this.getTxData() || {}
var txData = unconfTxList[props.index] || {}
var txParams = txData.params || {} var txParams = txData.params || {}
// var isNotification = isPopupOrNotification() === 'notification' // var isNotification = isPopupOrNotification() === 'notification'
@ -136,7 +174,6 @@ ConfirmTxScreen.prototype.render = function () {
]), ]),
*/ */
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
return currentTxView({ return currentTxView({
// Properties // Properties

@ -2,10 +2,10 @@
Buttons Buttons
*/ */
.btn-default,
.btn-primary, .btn-primary,
.btn-primary--lg, .btn-secondary {
.btn-secondary, height: 44px;
.btn-secondary--lg {
background: $white; background: $white;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -20,10 +20,16 @@
width: 100%; width: 100%;
text-transform: uppercase; text-transform: uppercase;
outline: none; outline: none;
&--disabled,
&[disabled] {
cursor: auto;
opacity: .5;
pointer-events: none;
}
} }
.btn-primary, .btn-primary {
.btn-primary--lg {
color: $curious-blue; color: $curious-blue;
border: 2px solid $spindle; border: 2px solid $spindle;
@ -35,17 +41,23 @@
&:hover { &:hover {
border-color: $curious-blue; border-color: $curious-blue;
} }
}
&--disabled, .btn-secondary {
&[disabled] { color: $monzo;
cursor: auto; border: 2px solid lighten($monzo, 40%);
opacity: .5;
pointer-events: none; &:active {
background: lighten($monzo, 55%);
border-color: $monzo;
}
&:hover {
border-color: $monzo;
} }
} }
.btn-secondary, .btn-default {
.btn-secondary--lg {
color: $scorpion; color: $scorpion;
border: 2px solid $dusty-gray; border: 2px solid $dusty-gray;
@ -57,20 +69,9 @@
&:hover { &:hover {
border-color: $scorpion; border-color: $scorpion;
} }
&--disabled,
&[disabled] {
cursor: auto;
opacity: .5;
pointer-events: none;
}
}
.btn-primary, .btn-secondary {
height: 44px;
} }
.btn-primary--lg, .btn-secondary--lg { .btn--large {
height: 54px; height: 54px;
} }

@ -47,10 +47,32 @@
&__input-wrapper { &__input-wrapper {
position: relative; position: relative;
display: flex; display: flex;
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
} }
&__currency-symbol { &__currency-symbol {
margin-top: 1px; margin-top: 1px;
color: $scorpion; color: $scorpion;
} }
.react-numeric-input {
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
}
} }

@ -11,8 +11,8 @@
background: rgba(255, 255, 255, .8); background: rgba(255, 255, 255, .8);
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
margin-top: 56px; margin-top: 66px;
height: calc(100% - 56px); height: calc(100% - 66px);
} }
@media screen and (min-width: 576px) { @media screen and (min-width: 576px) {

@ -191,11 +191,13 @@ input.large-input {
} }
&--full-width { &--full-width {
width: initial; width: 100% !important;
} }
&--full-height { &--full-height {
height: 100%; height: 100% !important;
max-height: initial !important;
min-height: initial !important;
} }
&__content { &__content {

@ -42,6 +42,7 @@ function reduceApp (state, action) {
open: false, open: false,
modalState: { modalState: {
name: null, name: null,
props: {},
}, },
previousModalState: { previousModalState: {
name: null, name: null,
@ -88,13 +89,17 @@ function reduceApp (state, action) {
// modal methods: // modal methods:
case actions.MODAL_OPEN: case actions.MODAL_OPEN:
const { name, ...modalProps } = action.payload
return extend(appState, { return extend(appState, {
modal: Object.assign( modal: {
state.appState.modal, open: true,
{ open: true }, modalState: {
{ modalState: action.payload }, name: name,
{ previousModalState: appState.modal.modalState}, props: { ...modalProps },
), },
previousModalState: { ...appState.modal.modalState },
},
}) })
case actions.MODAL_CLOSE: case actions.MODAL_CLOSE:
@ -102,7 +107,7 @@ function reduceApp (state, action) {
modal: Object.assign( modal: Object.assign(
state.appState.modal, state.appState.modal,
{ open: false }, { open: false },
{ modalState: { name: null } }, { modalState: { name: null, props: {} } },
{ previousModalState: appState.modal.modalState}, { previousModalState: appState.modal.modalState},
), ),
}) })

Loading…
Cancel
Save