Merge branch 'develop' of github.com:MetaMask/metamask-extension into trezor-v5

feature/default_network_editable
brunobar79 6 years ago
commit 8ee01f4e99
  1. 2
      .babelrc
  2. 8
      CHANGELOG.md
  3. 2
      app/_locales/cs/messages.json
  4. 2
      app/_locales/de/messages.json
  5. 34
      app/_locales/en/messages.json
  6. 2
      app/_locales/es/messages.json
  7. 2
      app/_locales/fr/messages.json
  8. 822
      app/_locales/ko/messages.json
  9. 2
      app/_locales/ru/messages.json
  10. 2
      app/_locales/tml/messages.json
  11. 2
      app/_locales/tr/messages.json
  12. 1
      app/manifest.json
  13. 20
      app/scripts/background.js
  14. 120
      app/scripts/controllers/preferences.js
  15. 18
      app/scripts/lib/account-tracker.js
  16. 2
      app/scripts/lib/ipfsContent.js
  17. 66
      app/scripts/metamask-controller.js
  18. 1
      development/states/add-token.json
  19. 1
      development/states/confirm-new-ui.json
  20. 1
      development/states/confirm-sig-requests.json
  21. 1
      development/states/currency-localization.json
  22. 1
      development/states/first-time.json
  23. 2
      development/states/send-edit.json
  24. 2
      development/states/send-new-ui.json
  25. 2
      development/states/send.json
  26. 1
      development/states/tx-list-items.json
  27. 5
      old-ui/app/account-detail.js
  28. 202
      old-ui/app/add-suggested-token.js
  29. 2
      old-ui/app/add-token.js
  30. 6
      old-ui/app/app.js
  31. 11
      old-ui/app/components/app-bar.js
  32. 23
      package-lock.json
  33. 13
      package.json
  34. 6
      test/e2e/beta/from-import-beta-ui.spec.js
  35. 219
      test/e2e/beta/metamask-beta-ui.spec.js
  36. 4
      test/integration/lib/add-token.js
  37. 2
      test/integration/lib/confirm-sig-requests.js
  38. 8
      test/integration/lib/currency-localization.js
  39. 6
      test/integration/lib/send-new-ui.js
  40. 21
      test/integration/lib/tx-list-items.js
  41. 71
      test/unit/app/controllers/metamask-controller-test.js
  42. 110
      test/unit/app/controllers/preferences-controller-test.js
  43. 33
      ui/app/account-and-transaction-details.js
  44. 57
      ui/app/actions.js
  45. 67
      ui/app/app.js
  46. 48
      ui/app/components/balance-component.js
  47. 267
      ui/app/components/buy-button-subview.js
  48. 3
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
  49. 4
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
  50. 8
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
  51. 4
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
  52. 5
      ui/app/components/confirm-page-container/confirm-page-container.component.js
  53. 26
      ui/app/components/currency-display/currency-display.component.js
  54. 19
      ui/app/components/currency-display/currency-display.container.js
  55. 1
      ui/app/components/currency-display/index.js
  56. 27
      ui/app/components/currency-display/tests/currency-display.component.test.js
  57. 61
      ui/app/components/currency-display/tests/currency-display.container.test.js
  58. 60
      ui/app/components/custom-radio-list.js
  59. 2
      ui/app/components/dropdowns/components/account-dropdowns.js
  60. 12
      ui/app/components/dropdowns/network-dropdown.js
  61. 64
      ui/app/components/identicon.js
  62. 26
      ui/app/components/index.scss
  63. 1
      ui/app/components/menu-bar/index.js
  64. 23
      ui/app/components/menu-bar/index.scss
  65. 52
      ui/app/components/menu-bar/menu-bar.component.js
  66. 26
      ui/app/components/menu-bar/menu-bar.container.js
  67. 2
      ui/app/components/modals/account-details-modal.js
  68. 4
      ui/app/components/modals/account-modal-container.js
  69. 48
      ui/app/components/modals/export-private-key-modal.js
  70. 5
      ui/app/components/modals/hide-token-confirmation-modal.js
  71. 2
      ui/app/components/page-container/index.scss
  72. 28
      ui/app/components/page-container/page-container-header/page-container-header.component.js
  73. 84
      ui/app/components/page-container/page-container.component.js
  74. 84
      ui/app/components/pages/add-token/add-token.component.js
  75. 126
      ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
  76. 29
      ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
  77. 2
      ui/app/components/pages/confirm-add-suggested-token/index.js
  78. 4
      ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
  79. 2
      ui/app/components/pages/confirm-add-token/token-balance/index.js
  80. 16
      ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
  81. 14
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  82. 4
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
  83. 17
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
  84. 3
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
  85. 6
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
  86. 239
      ui/app/components/pages/home.js
  87. 77
      ui/app/components/pages/home/home.component.js
  88. 30
      ui/app/components/pages/home/home.container.js
  89. 1
      ui/app/components/pages/home/index.js
  90. 29
      ui/app/components/pages/settings/settings.js
  91. 56
      ui/app/components/pending-msg-details.js
  92. 73
      ui/app/components/pending-msg.js
  93. 3
      ui/app/components/send/send-content/send-content.component.js
  94. 2
      ui/app/components/send/send-content/send-to-row/send-to-row.component.js
  95. 2
      ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
  96. 14
      ui/app/components/send/send-content/tests/send-content-component.test.js
  97. 3
      ui/app/components/send/send.component.js
  98. 2
      ui/app/components/send/send.container.js
  99. 5
      ui/app/components/send/send.selectors.js
  100. 5
      ui/app/components/send/tests/send-component.test.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,4 +1,4 @@
{ {
"presets": [["env"], "react", "stage-0"], "presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"],
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"] "plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
} }

@ -2,11 +2,13 @@
## Current Develop Branch ## Current Develop Branch
- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method.
## 4.9.3 Wed Aug 15 2018 ## 4.9.3 Wed Aug 15 2018
- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses. - [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses.
- (#4961)[https://github.com/MetaMask/metamask-extension/pull/4961]: Add a download seed phrase link. - [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link.
- (#5060)[https://github.com/MetaMask/metamask-extension/pull/5060]: Fix bug where gas was not updating properly. - [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly.
## 4.9.2 Mon Aug 09 2018 ## 4.9.2 Mon Aug 09 2018

@ -796,7 +796,7 @@
"message": "Testovací faucet" "message": "Testovací faucet"
}, },
"to": { "to": {
"message": "Komu: " "message": "Komu"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 na ETH přes ShapeShift", "message": "$1 na ETH přes ShapeShift",

@ -775,7 +775,7 @@
"message": "Testfaucet" "message": "Testfaucet"
}, },
"to": { "to": {
"message": "An:" "message": "An"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 an ETH via ShapeShift", "message": "$1 an ETH via ShapeShift",

@ -29,6 +29,9 @@
"addTokens": { "addTokens": {
"message": "Add Tokens" "message": "Add Tokens"
}, },
"addSuggestedTokens": {
"message": "Add Suggested Tokens"
},
"addAcquiredTokens": { "addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask" "message": "Add the tokens you've acquired using MetaMask"
}, },
@ -451,6 +454,9 @@
"hideTokenPrompt": { "hideTokenPrompt": {
"message": "Hide Token?" "message": "Hide Token?"
}, },
"history": {
"message": "History"
},
"howToDeposit": { "howToDeposit": {
"message": "How would you like to deposit Ether?" "message": "How would you like to deposit Ether?"
}, },
@ -651,7 +657,7 @@
"message": "No transaction history." "message": "No transaction history."
}, },
"noTransactions": { "noTransactions": {
"message": "No Transactions" "message": "You have no transactions"
}, },
"notFound": { "notFound": {
"message": "Not Found" "message": "Not Found"
@ -702,6 +708,9 @@
"pasteSeed": { "pasteSeed": {
"message": "Paste your seed phrase here!" "message": "Paste your seed phrase here!"
}, },
"pending": {
"message": "pending"
},
"personalAddressDetected": { "personalAddressDetected": {
"message": "Personal address detected. Input the token contract address." "message": "Personal address detected. Input the token contract address."
}, },
@ -730,6 +739,9 @@
"qrCode": { "qrCode": {
"message": "Show QR Code" "message": "Show QR Code"
}, },
"queue": {
"message": "Queue"
},
"readdToken": { "readdToken": {
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu." "message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
}, },
@ -870,6 +882,12 @@
"secretPhrase": { "secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault." "message": "Enter your secret twelve word phrase here to restore your vault."
}, },
"showHexData": {
"message": "Show Hex Data"
},
"showHexDataDescription": {
"message": "Select this to show the hex data field on the send screen"
},
"newPassword8Chars": { "newPassword8Chars": {
"message": "New Password (min 8 chars)" "message": "New Password (min 8 chars)"
}, },
@ -897,6 +915,12 @@
"sendTokens": { "sendTokens": {
"message": "Send Tokens" "message": "Send Tokens"
}, },
"sentEther": {
"message": "sent ether"
},
"sentTokens": {
"message": "sent tokens"
},
"separateEachWord": { "separateEachWord": {
"message": "Separate each word with a single space" "message": "Separate each word with a single space"
}, },
@ -910,6 +934,9 @@
"orderOneHere": { "orderOneHere": {
"message": "Order a Trezor or Ledger and keep your funds in cold storage" "message": "Order a Trezor or Ledger and keep your funds in cold storage"
}, },
"outgoing": {
"message": "Outgoing"
},
"searchTokens": { "searchTokens": {
"message": "Search Tokens" "message": "Search Tokens"
}, },
@ -973,6 +1000,9 @@
"sign": { "sign": {
"message": "Sign" "message": "Sign"
}, },
"signatureRequest": {
"message": "Signature Request"
},
"signed": { "signed": {
"message": "Signed" "message": "Signed"
}, },
@ -1025,7 +1055,7 @@
"message": "Test Faucet" "message": "Test Faucet"
}, },
"to": { "to": {
"message": "To: " "message": "To"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 to ETH via ShapeShift", "message": "$1 to ETH via ShapeShift",

@ -772,7 +772,7 @@
"message": "Probar Faucet" "message": "Probar Faucet"
}, },
"to": { "to": {
"message": "Para:" "message": "Para"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 a ETH via ShapeShift", "message": "$1 a ETH via ShapeShift",

@ -490,7 +490,7 @@
"message": "Sélectionner un service" "message": "Sélectionner un service"
}, },
"send": { "send": {
"message": "Envoyé" "message": "Envoyer"
}, },
"sendTokens": { "sendTokens": {
"message": "Envoyer des jetons" "message": "Envoyer des jetons"

File diff suppressed because it is too large Load Diff

@ -784,7 +784,7 @@
"message": "Тестовый кран" "message": "Тестовый кран"
}, },
"to": { "to": {
"message": "Получатель: " "message": "Получатель"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 в ETH через ShapeShift", "message": "$1 в ETH через ShapeShift",

@ -796,7 +796,7 @@
"message": "சதன" "message": "சதன"
}, },
"to": { "to": {
"message": "பநர: " "message": "பநர"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$ 1 மதல ETH வர வடிவம", "message": "$ 1 மதல ETH வர வடிவம",

@ -796,7 +796,7 @@
"message": "Test Musluğu" "message": "Test Musluğu"
}, },
"to": { "to": {
"message": "Kime: " "message": "Kime"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "ShapeShift üstünden $1'dan ETH'e", "message": "ShapeShift üstünden $1'dan ETH'e",

@ -71,7 +71,6 @@
"activeTab", "activeTab",
"webRequest", "webRequest",
"*://*.eth/", "*://*.eth/",
"*://*.test/",
"notifications" "notifications"
], ],
"web_accessible_resources": [ "web_accessible_resources": [

@ -256,6 +256,7 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi, showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi, unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi, showUnapprovedTx: triggerUi,
showWatchAssetUi: showWatchAssetUi,
// initial state // initial state
initState, initState,
// initial locale code // initial locale code
@ -451,9 +452,28 @@ function triggerUi () {
}) })
} }
/**
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
function showWatchAssetUi () {
triggerUi()
return new Promise(
(resolve) => {
var interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
}
)
}
// On first install, open a window to MetaMask website to how-it-works. // On first install, open a window to MetaMask website to how-it-works.
extension.runtime.onInstalled.addListener(function (details) { extension.runtime.onInstalled.addListener(function (details) {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
} }
}) })

@ -1,5 +1,6 @@
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize const normalizeAddress = require('eth-sig-util').normalize
const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend') const extend = require('xtend')
@ -14,6 +15,7 @@ class PreferencesController {
* @property {string} store.currentAccountTab Indicates the selected tab in the ui * @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists * @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type * @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature * user wishes to see that feature
@ -26,7 +28,9 @@ class PreferencesController {
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history', currentAccountTab: 'history',
accountTokens: {}, accountTokens: {},
assetImages: {},
tokens: [], tokens: [],
suggestedTokens: {},
useBlockie: false, useBlockie: false,
featureFlags: {}, featureFlags: {},
currentLocale: opts.initLangCode, currentLocale: opts.initLangCode,
@ -37,6 +41,7 @@ class PreferencesController {
this.diagnostics = opts.diagnostics this.diagnostics = opts.diagnostics
this.network = opts.network this.network = opts.network
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this.showWatchAssetUi = opts.showWatchAssetUi
this._subscribeProviderType() this._subscribeProviderType()
} }
// PUBLIC METHODS // PUBLIC METHODS
@ -51,6 +56,53 @@ class PreferencesController {
this.store.updateState({ useBlockie: val }) this.store.updateState({ useBlockie: val })
} }
getSuggestedTokens () {
return this.store.getState().suggestedTokens
}
getAssetImages () {
return this.store.getState().assetImages
}
addSuggestedERC20Asset (tokenOpts) {
this._validateERC20AssetParams(tokenOpts)
const suggested = this.getSuggestedTokens()
const { rawAddress, symbol, decimals, image } = tokenOpts
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals, image }
suggested[address] = newEntry
this.store.updateState({ suggestedTokens: suggested })
}
/**
* RPC engine middleware for requesting new asset added
*
* @param req
* @param res
* @param {Function} - next
* @param {Function} - end
*/
async requestWatchAsset (req, res, next, end) {
if (req.method === 'metamask_watchAsset') {
const { type, options } = req.params
switch (type) {
case 'ERC20':
const result = await this._handleWatchAssetERC20(options)
if (result instanceof Error) {
end(result)
} else {
res.result = result
end()
}
break
default:
end(new Error(`Asset of type ${type} not supported`))
}
} else {
next()
}
}
/** /**
* Getter for the `useBlockie` property * Getter for the `useBlockie` property
* *
@ -186,6 +238,13 @@ class PreferencesController {
return selected return selected
} }
removeSuggestedTokens () {
return new Promise((resolve, reject) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
})
}
/** /**
* Setter for the `selectedAddress` property * Setter for the `selectedAddress` property
* *
@ -232,11 +291,11 @@ class PreferencesController {
* @returns {Promise<array>} Promises the new array of AddedToken objects. * @returns {Promise<array>} Promises the new array of AddedToken objects.
* *
*/ */
async addToken (rawAddress, symbol, decimals) { async addToken (rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress) const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals } const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens const tokens = this.store.getState().tokens
const assetImages = this.getAssetImages()
const previousEntry = tokens.find((token, index) => { const previousEntry = tokens.find((token, index) => {
return token.address === address return token.address === address
}) })
@ -247,7 +306,8 @@ class PreferencesController {
} else { } else {
tokens.push(newEntry) tokens.push(newEntry)
} }
this._updateAccountTokens(tokens) assetImages[address] = image
this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens) return Promise.resolve(tokens)
} }
@ -260,8 +320,10 @@ class PreferencesController {
*/ */
removeToken (rawAddress) { removeToken (rawAddress) {
const tokens = this.store.getState().tokens const tokens = this.store.getState().tokens
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter(token => token.address !== rawAddress) const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this._updateAccountTokens(updatedTokens) delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages)
return Promise.resolve(updatedTokens) return Promise.resolve(updatedTokens)
} }
@ -322,7 +384,7 @@ class PreferencesController {
/** /**
* Returns an updated rpcList based on the passed url and the current list. * Returns an updated rpcList based on the passed url and the current list.
* The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise. * end of the list. The current list is modified and returned as a promise.
* *
* @param {string} _url The rpc url to add to the frequentRpcList. * @param {string} _url The rpc url to add to the frequentRpcList.
@ -338,7 +400,7 @@ class PreferencesController {
if (_url !== 'http://localhost:8545') { if (_url !== 'http://localhost:8545') {
rpcList.push(_url) rpcList.push(_url)
} }
if (rpcList.length > 2) { if (rpcList.length > 3) {
rpcList.shift() rpcList.shift()
} }
return Promise.resolve(rpcList) return Promise.resolve(rpcList)
@ -387,6 +449,7 @@ class PreferencesController {
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
/** /**
* Subscription to network provider type. * Subscription to network provider type.
* *
@ -405,10 +468,10 @@ class PreferencesController {
* @param {array} tokens Array of tokens to be updated. * @param {array} tokens Array of tokens to be updated.
* *
*/ */
_updateAccountTokens (tokens) { _updateAccountTokens (tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens }) this.store.updateState({ accountTokens, tokens, assetImages })
} }
/** /**
@ -438,6 +501,47 @@ class PreferencesController {
const tokens = accountTokens[selectedAddress][providerType] const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress } return { tokens, accountTokens, providerType, selectedAddress }
} }
/**
* Handle the suggestion of an ERC20 asset through `watchAsset`
* *
* @param {Promise} promise Promise according to addition of ERC20 token
*
*/
async _handleWatchAssetERC20 (options) {
const { address, symbol, decimals, image } = options
const rawAddress = address
try {
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
} catch (err) {
return err
}
const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts)
return this.showWatchAssetUi().then(() => {
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
return tokenAddresses.length > 0
})
}
/**
* Validates that the passed options for suggested token have all required properties.
*
* @param {Object} opts The options object to validate
* @throws {string} Throw a custom error indicating that address, symbol and/or decimals
* doesn't fulfill requirements
*
*/
_validateERC20AssetParams (opts) {
const { rawAddress, symbol, decimals } = opts
if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
}
if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
}
} }
module.exports = PreferencesController module.exports = PreferencesController

@ -43,10 +43,24 @@ class AccountTracker {
this._provider = opts.provider this._provider = opts.provider
this._query = pify(new EthQuery(this._provider)) this._query = pify(new EthQuery(this._provider))
this._blockTracker = opts.blockTracker this._blockTracker = opts.blockTracker
// subscribe to latest block
this._blockTracker.on('latest', this._updateForBlock.bind(this))
// blockTracker.currentBlock may be null // blockTracker.currentBlock may be null
this._currentBlockNumber = this._blockTracker.getCurrentBlock() this._currentBlockNumber = this._blockTracker.getCurrentBlock()
// bind function for easier listener syntax
this._updateForBlock = this._updateForBlock.bind(this)
}
start () {
// remove first to avoid double add
this._blockTracker.removeListener('latest', this._updateForBlock)
// add listener
this._blockTracker.addListener('latest', this._updateForBlock)
// fetch account balances
this._updateAccounts()
}
stop () {
// remove listener
this._blockTracker.removeListener('latest', this._updateForBlock)
} }
/** /**

@ -34,7 +34,7 @@ module.exports = function (provider) {
return { cancel: true } return { cancel: true }
} }
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']})
return { return {
remove () { remove () {

@ -67,6 +67,10 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {} const initState = opts.initState || {}
this.recordFirstTimeInfo(initState) this.recordFirstTimeInfo(initState)
// this keeps track of how many "controllerStream" connections are open
// the only thing that uses controller connections are open metamask UI instances
this.activeControllerConnections = 0
// platform-specific api // platform-specific api
this.platform = opts.platform this.platform = opts.platform
@ -88,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
initState: initState.PreferencesController, initState: initState.PreferencesController,
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
showWatchAssetUi: opts.showWatchAssetUi,
network: this.networkController, network: this.networkController,
}) })
@ -127,6 +132,14 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
}) })
// start and stop polling for balances based on activeControllerConnections
this.on('controllerConnectionChanged', (activeControllerConnections) => {
if (activeControllerConnections > 0) {
this.accountTracker.start()
} else {
this.accountTracker.stop()
}
})
// key mgmt // key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
@ -137,19 +150,7 @@ module.exports = class MetamaskController extends EventEmitter {
encryptor: opts.encryptor || undefined, encryptor: opts.encryptor || undefined,
}) })
// If only one account exists, make sure it is selected. this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
this.keyringController.memStore.subscribe((state) => {
const addresses = state.keyrings.reduce((res, keyring) => {
return res.concat(keyring.accounts)
}, [])
if (addresses.length === 1) {
const address = addresses[0]
this.preferencesController.setSelectedAddress(address)
}
// ensure preferences + identities controller know about all addresses
this.preferencesController.addAddresses(addresses)
this.accountTracker.syncWithAddresses(addresses)
})
// detect tokens controller // detect tokens controller
this.detectTokensController = new DetectTokensController({ this.detectTokensController = new DetectTokensController({
@ -386,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter {
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController), removeToken: nodeify(preferencesController.removeToken, preferencesController),
removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
@ -1209,11 +1211,19 @@ module.exports = class MetamaskController extends EventEmitter {
setupControllerConnection (outStream) { setupControllerConnection (outStream) {
const api = this.getApi() const api = this.getApi()
const dnode = Dnode(api) const dnode = Dnode(api)
// report new active controller connection
this.activeControllerConnections++
this.emit('controllerConnectionChanged', this.activeControllerConnections)
// connect dnode api to remote connection
pump( pump(
outStream, outStream,
dnode, dnode,
outStream, outStream,
(err) => { (err) => {
// report new active controller connection
this.activeControllerConnections--
this.emit('controllerConnectionChanged', this.activeControllerConnections)
// report any error
if (err) log.error(err) if (err) log.error(err)
} }
) )
@ -1242,6 +1252,7 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(createOriginMiddleware({ origin })) engine.push(createOriginMiddleware({ origin }))
engine.push(createLoggerMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin }))
engine.push(filterMiddleware) engine.push(filterMiddleware)
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
engine.push(createProviderMiddleware({ provider: this.provider })) engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection // setup connection
@ -1278,6 +1289,34 @@ module.exports = class MetamaskController extends EventEmitter {
) )
} }
/**
* Handle a KeyringController update
* @param {object} state the KC state
* @return {Promise<void>}
* @private
*/
async _onKeyringControllerUpdate (state) {
const {isUnlocked, keyrings} = state
const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
if (!addresses.length) {
return
}
// Ensure preferences + identities controller know about all addresses
this.preferencesController.addAddresses(addresses)
this.accountTracker.syncWithAddresses(addresses)
const wasLocked = !isUnlocked
if (wasLocked) {
const oldSelectedAddress = this.preferencesController.getSelectedAddress()
if (!addresses.includes(oldSelectedAddress)) {
const address = addresses[0]
await this.preferencesController.setSelectedAddress(address)
}
}
}
/** /**
* A method for emitting the full MetaMask state to all registered listeners. * A method for emitting the full MetaMask state to all registered listeners.
* @private * @private
@ -1424,6 +1463,7 @@ module.exports = class MetamaskController extends EventEmitter {
} }
} }
// TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
/** /**
* A method for recording whether the MetaMask user interface is open or not. * A method for recording whether the MetaMask user interface is open or not.
* @private * @private

@ -123,6 +123,7 @@
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}
}, },
"sidebar": {},
"transForward": true, "transForward": true,
"isLoading": false, "isLoading": false,
"warning": null, "warning": null,

@ -141,6 +141,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"modal": { "modal": {
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}

@ -162,6 +162,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"modal": { "modal": {
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}

@ -120,6 +120,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"modal": { "modal": {
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}

@ -48,6 +48,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"transForward": true, "transForward": true,
"isLoading": false, "isLoading": false,
"warning": null, "warning": null,

@ -22,6 +22,7 @@
"name": "Send Account 4" "name": "Send Account 4"
} }
}, },
"assetImages": {},
"unapprovedTxs": {}, "unapprovedTxs": {},
"currentCurrency": "USD", "currentCurrency": "USD",
"conversionRate": 1200.88200327, "conversionRate": 1200.88200327,
@ -141,6 +142,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"modal": { "modal": {
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}

@ -61,6 +61,7 @@
"name": "Address Book Account 1" "name": "Address Book Account 1"
} }
], ],
"assetImages": {},
"tokens": [], "tokens": [],
"transactions": {}, "transactions": {},
"selectedAddressTxList": [], "selectedAddressTxList": [],
@ -120,6 +121,7 @@
"accountDetail": { "accountDetail": {
"subview": "transactions" "subview": "transactions"
}, },
"sidebar": {},
"modal": { "modal": {
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}

@ -21,6 +21,7 @@
"name": "Account 4" "name": "Account 4"
} }
}, },
"assetImages": {},
"unapprovedTxs": {}, "unapprovedTxs": {},
"currentCurrency": "USD", "currentCurrency": "USD",
"conversionRate": 16.88200327, "conversionRate": 16.88200327,
@ -99,6 +100,7 @@
"accountExport": "none", "accountExport": "none",
"privateKey": "" "privateKey": ""
}, },
"sidebar": {},
"transForward": true, "transForward": true,
"isLoading": false, "isLoading": false,
"warning": null, "warning": null,

@ -118,6 +118,7 @@
"modalState": {}, "modalState": {},
"previousModalState": {} "previousModalState": {}
}, },
"sidebar": {},
"transForward": true, "transForward": true,
"isLoading": false, "isLoading": false,
"warning": null, "warning": null,

@ -32,6 +32,7 @@ function mapStateToProps (state) {
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab, currentAccountTab: state.metamask.currentAccountTab,
tokens: state.metamask.tokens, tokens: state.metamask.tokens,
suggestedTokens: state.metamask.suggestedTokens,
computedBalances: state.metamask.computedBalances, computedBalances: state.metamask.computedBalances,
} }
} }
@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () {
var account = props.accounts[selected] var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props const { network, conversionRate, currentCurrency } = props
if (Object.keys(props.suggestedTokens).length > 0) {
this.props.dispatch(actions.showAddSuggestedTokenPage())
}
return ( return (
h('.account-detail-section.full-flex-height', [ h('.account-detail-section.full-flex-height', [

@ -0,0 +1,202 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util')
const Copyable = require('./components/copyable')
const addressSummary = require('./util').addressSummary
module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen)
function mapStateToProps (state) {
return {
identities: state.metamask.identities,
suggestedTokens: state.metamask.suggestedTokens,
}
}
inherits(AddSuggestedTokenScreen, Component)
function AddSuggestedTokenScreen () {
this.state = {
warning: null,
}
Component.call(this)
}
AddSuggestedTokenScreen.prototype.render = function () {
const state = this.state
const props = this.props
const { warning } = state
const key = Object.keys(props.suggestedTokens)[0]
const { address, symbol, decimals } = props.suggestedTokens[key]
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('h2.page-subtitle', 'Add Suggested Token'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
h('div', [
h(Tooltip, {
position: 'top',
title: 'The contract of the actual token contract. Click for more info.',
}, [
h('a', {
style: { fontWeight: 'bold', paddingRight: '10px'},
href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
target: '_blank',
}, [
h('span', 'Token Contract Address '),
h('i.fa.fa-question-circle'),
]),
]),
]),
h('div', {
style: { display: 'flex' },
}, [
h(Copyable, {
value: ethUtil.toChecksumAddress(address),
}, [
h('span#token-address', {
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
display: 'flex',
},
}, addressSummary(address, 24, 4, false)),
]),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Symbol'),
]),
h('div', { style: {display: 'flex'} }, [
h('p#token_symbol', {
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}, symbol),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Decimals of Precision'),
]),
h('div', { style: {display: 'flex'} }, [
h('p#token_decimals', {
type: 'number',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}, decimals),
]),
h('button', {
style: {
alignSelf: 'center',
margin: '8px',
},
onClick: (event) => {
this.props.dispatch(actions.removeSuggestedTokens())
},
}, 'Cancel'),
h('button', {
style: {
alignSelf: 'center',
margin: '8px',
},
onClick: (event) => {
const valid = this.validateInputs({ address, symbol, decimals })
if (!valid) return
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
.then(() => {
this.props.dispatch(actions.removeSuggestedTokens())
})
},
}, 'Add'),
]),
]),
])
)
}
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
}
AddSuggestedTokenScreen.prototype.validateInputs = function (opts) {
let msg = ''
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = opts
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) {
msg += 'Address is invalid.'
}
const validDecimals = decimals >= 0 && decimals <= 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}
const symbolLen = symbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
msg += 'Symbol must be between 0 and 10 characters.'
}
const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
msg = 'Personal address detected. Input the token contract address.'
}
const isValid = validAddress && validDecimals && !ownAddress
if (!isValid) {
this.setState({
warning: msg,
})
} else {
this.setState({ warning: null })
}
return isValid
}

@ -196,7 +196,7 @@ AddTokenScreen.prototype.validateInputs = function () {
msg += 'Address is invalid.' msg += 'Address is invalid.'
} }
const validDecimals = decimals >= 0 && decimals < 36 const validDecimals = decimals >= 0 && decimals <= 36
if (!validDecimals) { if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. ' msg += 'Decimals must be at least 0, and not over 36. '
} }

@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views // other views
const ConfigScreen = require('./config') const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token') const AddTokenScreen = require('./add-token')
const AddSuggestedTokenScreen = require('./add-suggested-token')
const Import = require('./accounts/import') const Import = require('./accounts/import')
const InfoScreen = require('./info') const InfoScreen = require('./info')
const NewUiAnnouncement = require('./new-ui-annoucement') const NewUiAnnouncement = require('./new-ui-annoucement')
@ -74,6 +75,7 @@ function mapStateToProps (state) {
lostAccounts: state.metamask.lostAccounts, lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [], frequentRpcList: state.metamask.frequentRpcList || [],
featureFlags, featureFlags,
suggestedTokens: state.metamask.suggestedTokens,
// state needed to get account dropdown temporarily rendering from app bar // state needed to get account dropdown temporarily rendering from app bar
identities, identities,
@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering add-token screen from unlock screen.') log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'}) return h(AddTokenScreen, {key: 'add-token'})
case 'add-suggested-token':
log.debug('rendering add-suggested-token screen from unlock screen.')
return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'})
case 'config': case 'config':
log.debug('rendering config screen') log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'}) return h(ConfigScreen, {key: 'config'})

@ -350,11 +350,14 @@ module.exports = class AppBar extends Component {
} }
} }
renderCommonRpc (rpcList, {rpcTarget}) { renderCommonRpc (rpcList, provider) {
const {dispatch} = this.props const {dispatch} = this.props
const reversedRpcList = rpcList.slice().reverse()
return rpcList.map((rpc) => { return reversedRpcList.map((rpc) => {
if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) { const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
return null return null
} else { } else {
return h(DropdownMenuItem, { return h(DropdownMenuItem, {
@ -364,7 +367,7 @@ module.exports = class AppBar extends Component {
}, [ }, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'), h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc, rpc,
rpcTarget === rpc currentRpcTarget
? h('.check', '✓') ? h('.check', '✓')
: null, : null,
]) ])

23
package-lock.json generated

@ -16169,6 +16169,14 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
}, },
"json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=",
"requires": {
"string-convert": "^0.2.0"
}
},
"json3": { "json3": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@ -25028,6 +25036,16 @@
"xtend": "^4.0.1" "xtend": "^4.0.1"
} }
}, },
"react-media": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/react-media/-/react-media-1.8.0.tgz",
"integrity": "sha512-XcfqkDQj5/hmJod/kXUAZljJyMVkWrBWOkzwynAR8BXOGlbFLGBwezM0jQHtp2BrSymhf14/XrQrb3gGBnGK4g==",
"requires": {
"invariant": "^2.2.2",
"json2mq": "^0.2.0",
"prop-types": "^15.5.10"
}
},
"react-modal": { "react-modal": {
"version": "3.4.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.4.4.tgz", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.4.4.tgz",
@ -27885,6 +27903,11 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
}, },
"string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c="
},
"string-length": { "string-length": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz",

@ -57,7 +57,11 @@
[ [
"env", "env",
{ {
"debug": true "browsers": [
">0.25%",
"not ie 11",
"not op_mini all"
]
} }
], ],
"stage-0" "stage-0"
@ -143,7 +147,7 @@
"gulp-eslint": "^4.0.0", "gulp-eslint": "^4.0.0",
"gulp-sass": "^4.0.0", "gulp-sass": "^4.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"human-standard-token-abi": "^1.0.2", "human-standard-token-abi": "^2.0.0",
"idb-global": "^2.1.0", "idb-global": "^2.1.0",
"identicon.js": "^2.3.1", "identicon.js": "^2.3.1",
"iframe": "^1.0.0", "iframe": "^1.0.0",
@ -185,6 +189,7 @@
"react-dom": "^15.6.2", "react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0", "react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0", "react-markdown": "^3.0.0",
"react-media": "^1.8.0",
"react-redux": "^5.0.5", "react-redux": "^5.0.5",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-select": "^1.0.0", "react-select": "^1.0.0",
@ -246,8 +251,8 @@
"del": "^3.0.0", "del": "^3.0.0",
"dot-only-hunter": "^1.0.3", "dot-only-hunter": "^1.0.3",
"envify": "^4.0.0", "envify": "^4.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.4.4",
"enzyme-adapter-react-15": "^1.0.5", "enzyme-adapter-react-15": "^1.0.6",
"eslint-plugin-chai": "0.0.1", "eslint-plugin-chai": "0.0.1",
"eslint-plugin-json": "^1.2.0", "eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",

@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
assert.equal(txValues.length, 1) assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH') assert.equal(await txValues[0].getText(), '-1 ETH')
}) })
}) })

@ -225,19 +225,9 @@ describe('MetaMask', function () {
await delay(regularDelayMs) await delay(regularDelayMs)
} }
await clickWordAndWait(words[0]) for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[1]) await clickWordAndWait(words[i])
await clickWordAndWait(words[2]) }
await clickWordAndWait(words[3])
await clickWordAndWait(words[4])
await clickWordAndWait(words[5])
await clickWordAndWait(words[6])
await clickWordAndWait(words[7])
await clickWordAndWait(words[8])
await clickWordAndWait(words[9])
await clickWordAndWait(words[10])
await clickWordAndWait(words[11])
} catch (e) { } catch (e) {
if (count > 2) { if (count > 2) {
throw e throw e
@ -414,12 +404,12 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') { if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.tx-list-value')) const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000) await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
} }
}) })
}) })
@ -457,14 +447,11 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2) assert.equal(transactions.length, 2)
const txStatuses = await findElements(driver, By.css('.tx-list-status')) const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
}) })
}) })
@ -487,9 +474,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(regularDelayMs)
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`)) const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
await txListItem.click() await txListItem.click()
await delay(regularDelayMs) await delay(largeDelayMs)
}) })
it('displays the contract creation data', async () => { it('displays the contract creation data', async () => {
@ -511,13 +498,15 @@ describe('MetaMask', function () {
it('confirms a deploy contract transaction', async () => { it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(largeDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status')) driver.wait(async () => {
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account')) const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment') await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -538,9 +527,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(largeDelayMs) await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container')) await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value')) const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000) await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await txListValue.click() await txListValue.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -568,15 +557,17 @@ describe('MetaMask', function () {
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status')) driver.wait(async () => {
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 4
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000) await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account')) // const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText() // const firstTxAddress = await txAccounts[0].getText()
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/)) // assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
}) })
it('calls and confirms a contract method where ETH is received', async () => { it('calls and confirms a contract method where ETH is received', async () => {
@ -590,7 +581,7 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(regularDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item')) const txListItem = await findElement(driver, By.css('.transaction-list-item'))
await txListItem.click() await txListItem.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -598,18 +589,20 @@ describe('MetaMask', function () {
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status')) driver.wait(async () => {
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 5
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value')) const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000) await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp]) await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
}) })
it('renders the correct ETH balance', async () => { it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs) await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') { if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000) await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
@ -654,18 +647,17 @@ describe('MetaMask', function () {
await closeAllWindowHandlesExcept(driver, [extension, dapp]) await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs) await delay(regularDelayMs)
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(largeDelayMs)
}) })
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
await addToken.click() await addToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('picks the newly created Test token', async () => { it('picks the newly created Test token', async () => {
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]")) const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]"))
await addCustomToken.click() await addCustomToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -683,7 +675,7 @@ describe('MetaMask', function () {
}) })
it('renders the balance for the new token', async () => { it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/)) await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.ok(/^100\s*TST\s*$/.test(tokenAmount)) assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
@ -752,21 +744,25 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1) assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests // or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') { if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000) await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
} }
const txStatuses = await findElements(driver, By.css('.tx-list-status')) driver.wait(async () => {
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
assert.equal(await tx.getText(), 'Confirmed') return confirmedTxes.length === 1
}, 10000)
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
assert.equal(await tx.getText(), 'Sent Tokens')
}) })
}) })
@ -789,9 +785,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(largeDelayMs) await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container')) await findElements(driver, By.css('.transaction-list__pending-transactions'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value')) const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000) await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
await txListValue.click() await txListValue.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -838,25 +834,28 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) driver.wait(async () => {
assert.equal(transactions.length, 2) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 2
}, 10000)
const txValues = await findElements(driver, By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/)) await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status')) const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
const walletBalance = await findElement(driver, By.css('.wallet-balance')) const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click() await walletBalance.click()
const tokenListItems = await findElements(driver, By.css('.token-list-item')) const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click() await tokenListItems[0].click()
await delay(regularDelayMs)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests // or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') { if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount')) const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
assert.equal(await tokenBalanceAmount.getText(), '43') assert.equal(await tokenBalanceAmount.getText(), '43 TST')
} }
}) })
}) })
@ -880,9 +879,14 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(regularDelayMs)
const [txListItem] = await findElements(driver, By.css('.tx-list-item')) driver.wait(async () => {
const [txListValue] = await findElements(driver, By.css('.tx-list-value')) const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/)) return pendingTxes.length === 1
}, 10000)
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
await txListItem.click() await txListItem.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -953,10 +957,15 @@ describe('MetaMask', function () {
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const txValues = await findElements(driver, By.css('.tx-list-value')) driver.wait(async () => {
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/)) const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
const txStatuses = await findElements(driver, By.css('.tx-list-status')) return confirmedTxes.length === 3
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) }, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
}) })
}) })
@ -1006,9 +1015,69 @@ describe('MetaMask', function () {
}) })
it('renders the balance for the chosen token', async () => { it('renders the balance for the chosen token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/)) await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
describe('Stores custom RPC history', () => {
const customRpcUrls = [
'https://mainnet.infura.io/1',
'https://mainnet.infura.io/2',
'https://mainnet.infura.io/3',
'https://mainnet.infura.io/4',
]
customRpcUrls.forEach(customRpcUrl => {
it('creates custom RPC: ' + customRpcUrl, async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
await customRpcButton.click()
await delay(regularDelayMs)
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
await customRpcInput.clear()
await customRpcInput.sendKeys(customRpcUrl)
const customRpcSave = await findElement(driver, By.css('.settings__rpc-save-button'))
await customRpcSave.click()
await delay(largeDelayMs * 2)
})
})
it('selects another provider', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`))
await customRpcButton.click()
await delay(largeDelayMs * 2)
})
it('finds 3 recent RPCs in history', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
// oldest selected RPC is not found
await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`))
// only recent 3 are found and in correct order (most recent at the top)
const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`))
assert.equal(customRpcs.length, 3)
for (let i = 0; i < customRpcs.length; i++) {
const linkText = await customRpcs[i].getText()
const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1]
assert.notEqual(linkText.indexOf(rpcUrl), -1)
}
})
})
}) })

@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
$('button.btn-primary.btn--large')[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($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance') assert.ok(heroBalance, 'rendered hero balance')
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added') assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
// $('button.btn-primary--lg')[0].click() // $('button.btn-primary--lg')[0].click()
// Verify added token image // Verify added token image
heroBalance = await queryAsync($, '.hero-balance') heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance') assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added') assert.ok(heroBalance.find('.identicon')[0], 'token added')
} }

@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests') selectState.val('confirm sig requests')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable') const pendingRequestItem = $.find('.transaction-list-item')
if (pendingRequestItem[0]) { if (pendingRequestItem[0]) {
pendingRequestItem[0].click() pendingRequestItem[0].click()

@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
await timeout(1000) await timeout(1000)
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
await timeout(1000) await timeout(1000)
const txView = await queryAsync($, '.tx-view') const txView = await queryAsync($, '.transaction-view')
const heroBalance = await findAsync($(txView), '.hero-balance') const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
const fiatAmount = await findAsync($(heroBalance), '.fiat-amount') const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
assert.equal(fiatAmount[0].textContent, '₱102,707.97') assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
} }

@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
selectState.val('send new ui') selectState.val('send new ui')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button') const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
assert.ok(sendScreenButton[1], 'send screen button present') assert.ok(sendScreenButton[1], 'send screen button present')
sendScreenButton[1].click() sendScreenButton[1].click()
@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) {
selectState.val('send edit') selectState.val('send edit')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first() const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first()
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name') assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last() const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat') const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')

@ -29,26 +29,23 @@ async function runTxListItemsTest (assert, done) {
assert.ok(metamaskLogo[0], 'metamask logo present') assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click() metamaskLogo[0].click()
const txListItems = await queryAsync($, '.tx-list-item') const txListItems = await queryAsync($, '.transaction-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered') assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const unapprovedTx = txListItems[0]
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1] const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span') const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link') assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2] const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status') const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label') assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3] const unapprovedMsg = txListItems[3]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account') const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description') assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4] const failedTx = txListItems[4]
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status') const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label') assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5] const shapeShiftTx = txListItems[5]
@ -56,10 +53,10 @@ async function runTxListItemsTest (assert, done) {
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status') assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6] const confirmedTokenTx = txListItems[6]
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account') const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address') assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7] const rejectedTx = txListItems[7]
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status') const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label') assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
} }

@ -814,6 +814,77 @@ describe('MetaMaskController', function () {
}) })
}) })
describe('#_onKeyringControllerUpdate', function () {
it('should do nothing if there are no keyrings in state', async function () {
const addAddresses = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({keyrings: []})
assert.ok(addAddresses.notCalled)
assert.ok(syncWithAddresses.notCalled)
assert.deepEqual(metamaskController.getState(), oldState)
})
it('should update selected address if keyrings was locked', async function () {
const addAddresses = sinon.fake()
const getSelectedAddress = sinon.fake.returns('0x42')
const setSelectedAddress = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
getSelectedAddress,
setSelectedAddress,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({
isUnlocked: false,
keyrings: [{
accounts: ['0x1', '0x2'],
}],
})
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(setSelectedAddress.args, [['0x1']])
assert.deepEqual(metamaskController.getState(), oldState)
})
it('should NOT update selected address if already unlocked', async function () {
const addAddresses = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({
isUnlocked: true,
keyrings: [{
accounts: ['0x1', '0x2'],
}],
})
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(metamaskController.getState(), oldState)
})
})
}) })
function deferredPromise () { function deferredPromise () {

@ -1,6 +1,7 @@
const assert = require('assert') const assert = require('assert')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences') const PreferencesController = require('../../../../app/scripts/controllers/preferences')
const sinon = require('sinon')
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController let preferencesController
@ -339,5 +340,114 @@ describe('preferences controller', function () {
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network') assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
}) })
}) })
describe('on watchAsset', function () {
var stubNext, stubEnd, stubHandleWatchAssetERC20, asy, req, res
const sandbox = sinon.createSandbox()
beforeEach(() => {
req = {params: {}}
res = {}
asy = {next: () => {}, end: () => {}}
stubNext = sandbox.stub(asy, 'next')
stubEnd = sandbox.stub(asy, 'end').returns(0)
stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20')
})
after(() => {
sandbox.restore()
})
it('shouldn not do anything if method not corresponds', async function () {
const asy = {next: () => {}, end: () => {}}
var stubNext = sandbox.stub(asy, 'next')
var stubEnd = sandbox.stub(asy, 'end').returns(0)
req.method = 'metamask'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.notCalled(stubEnd)
sandbox.assert.called(stubNext)
})
it('should do something if method is supported', async function () {
const asy = {next: () => {}, end: () => {}}
var stubNext = sandbox.stub(asy, 'next')
var stubEnd = sandbox.stub(asy, 'end').returns(0)
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubNext)
})
it('should through error if method is supported but asset type is not', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubHandleWatchAssetERC20)
sandbox.assert.notCalled(stubNext)
assert.deepEqual(res, {})
})
it('should trigger handle add asset if type supported', async function () {
const asy = {next: () => {}, end: () => {}}
req.method = 'metamask_watchAsset'
req.params.type = 'ERC20'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubHandleWatchAssetERC20)
})
})
describe('on watchAsset of type ERC20', function () {
var req
const sandbox = sinon.createSandbox()
beforeEach(() => {
req = {params: {type: 'ERC20'}}
})
after(() => {
sandbox.restore()
})
it('should add suggested token', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {}
await preferencesController._handleWatchAssetERC20(req.params.options)
const suggested = preferencesController.getSuggestedTokens()
assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`)
assert.equal(suggested[address].address, address, 'set address correctly')
assert.equal(suggested[address].symbol, symbol, 'set symbol correctly')
assert.equal(suggested[address].decimals, decimals, 'set decimals correctly')
assert.equal(suggested[address].image, image, 'set image correctly')
})
it('should add token correctly if user confirms', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {
await preferencesController.addToken(address, symbol, decimals, image)
}
await preferencesController._handleWatchAssetERC20(req.params.options)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, `one token added`)
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
const assetImages = preferencesController.getAssetImages()
assert.ok(assetImages[address], `set image correctly`)
})
})
}) })

@ -1,33 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
// Main Views
const TxView = require('./components/tx-view')
const WalletView = require('./components/wallet-view')
module.exports = AccountAndTransactionDetails
inherits(AccountAndTransactionDetails, Component)
function AccountAndTransactionDetails () {
Component.call(this)
}
AccountAndTransactionDetails.prototype.render = function () {
return h('div.account-and-transaction-details', [
// wallet
h(WalletView, {
style: {
},
responsiveDisplayClassname: '.lap-visible',
}, [
]),
// transaction
h(TxView, {
style: {
},
}, [
]),
])
}

@ -227,11 +227,14 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
showConfigPage, showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
showAddTokenPage, showAddTokenPage,
showAddSuggestedTokenPage,
addToken, addToken,
addTokens, addTokens,
removeToken, removeToken,
updateTokens, updateTokens,
removeSuggestedTokens,
UPDATE_TOKENS: 'UPDATE_TOKENS', UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget, setRpcTarget: setRpcTarget,
setProviderType: setProviderType, setProviderType: setProviderType,
@ -1147,6 +1150,10 @@ function updateAndApproveTx (txData) {
return txData return txData
}) })
.catch((err) => {
dispatch(actions.hideLoadingIndication())
return Promise.reject(err)
})
} }
} }
@ -1589,11 +1596,18 @@ function showAddTokenPage (transitionForward = true) {
} }
} }
function addToken (address, symbol, decimals) { function showAddSuggestedTokenPage (transitionForward = true) {
return {
type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
value: transitionForward,
}
}
function addToken (address, symbol, decimals, image) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
background.addToken(address, symbol, decimals, (err, tokens) => { background.addToken(address, symbol, decimals, image, (err, tokens) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.displayWarning(err.message)) dispatch(actions.displayWarning(err.message))
@ -1643,6 +1657,27 @@ function addTokens (tokens) {
} }
} }
function removeSuggestedTokens () {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.removeSuggestedTokens((err, suggestedTokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
}
dispatch(actions.clearPendingTokens())
if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
return global.platform.closeCurrentWindow()
}
resolve(suggestedTokens)
})
})
.then(() => updateMetamaskStateFromBackground())
.then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens})))
}
}
function updateTokens (newTokens) { function updateTokens (newTokens) {
return { return {
type: actions.UPDATE_TOKENS, type: actions.UPDATE_TOKENS,
@ -1650,6 +1685,12 @@ function updateTokens (newTokens) {
} }
} }
function clearPendingTokens () {
return {
type: actions.CLEAR_PENDING_TOKENS,
}
}
function goBackToInitView () { function goBackToInitView () {
return { return {
type: actions.BACK_TO_INIT_MENU, type: actions.BACK_TO_INIT_MENU,
@ -1812,9 +1853,13 @@ function hideModal (payload) {
} }
} }
function showSidebar () { function showSidebar ({ transitionName, type }) {
return { return {
type: actions.SIDEBAR_OPEN, type: actions.SIDEBAR_OPEN,
value: {
transitionName,
type,
},
} }
} }
@ -2310,9 +2355,3 @@ function setPendingTokens (pendingTokens) {
payload: tokens, payload: tokens,
} }
} }
function clearPendingTokens () {
return {
type: actions.CLEAR_PENDING_TOKENS,
}
}

@ -15,10 +15,10 @@ const SendTransactionScreen = require('./components/send/send.container')
const ConfirmTransaction = require('./components/pages/confirm-transaction') const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu // slideout menu
const WalletView = require('./components/wallet-view') const Sidebar = require('./components/sidebars').default
// other views // other views
const Home = require('./components/pages/home') import Home from './components/pages/home'
const Authenticated = require('./components/pages/authenticated') const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized') const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings') const Settings = require('./components/pages/settings')
@ -26,11 +26,11 @@ const RestoreVaultPage = require('./components/pages/keychains/restore-vault').d
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token') const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
const CreateAccountPage = require('./components/pages/create-account') const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice') const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading-screen') const Loading = require('./components/loading-screen')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown') const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu') const AccountMenu = require('./components/account-menu')
@ -51,6 +51,7 @@ const {
RESTORE_VAULT_ROUTE, RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE, ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE, CONFIRM_ADD_TOKEN_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE, NEW_ACCOUNT_ROUTE,
SEND_ROUTE, SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE, CONFIRM_TRANSACTION_ROUTE,
@ -85,6 +86,7 @@ class App extends Component {
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 }),
h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
]) ])
@ -102,6 +104,7 @@ class App extends Component {
frequentRpcList, frequentRpcList,
currentView, currentView,
setMouseUserState, setMouseUserState,
sidebar,
} = this.props } = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ? const loadMessage = loadingMessage || isLoadingNetwork ?
@ -134,7 +137,12 @@ class App extends Component {
h(AppHeader), h(AppHeader),
// sidebar // sidebar
this.renderSidebar(), h(Sidebar, {
sidebarOpen: sidebar.isOpen,
hideSidebar: this.props.hideSidebar,
transitionName: sidebar.transitionName,
type: sidebar.type,
}),
// network dropdown // network dropdown
h(NetworkDropdown, { h(NetworkDropdown, {
@ -154,51 +162,6 @@ class App extends Component {
) )
} }
renderSidebar () {
return h('div', [
h('style', `
.sidebar-enter {
transition: transform 300ms ease-in-out;
transform: translateX(-100%);
}
.sidebar-enter.sidebar-enter-active {
transition: transform 300ms ease-in-out;
transform: translateX(0%);
}
.sidebar-leave {
transition: transform 200ms ease-out;
transform: translateX(0%);
}
.sidebar-leave.sidebar-leave-active {
transition: transform 200ms ease-out;
transform: translateX(-100%);
}
`),
h(ReactCSSTransitionGroup, {
transitionName: 'sidebar',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 200,
}, [
// A second instance of Walletview is used for non-mobile viewports
this.props.sidebarOpen ? h(WalletView, {
responsiveDisplayClassname: '.sidebar',
style: {},
}) : undefined,
]),
// overlay
// TODO: add onClick for overlay to close sidebar
this.props.sidebarOpen ? h('div.sidebar-overlay', {
style: {},
onClick: () => {
this.props.hideSidebar()
},
}, []) : undefined,
])
}
toggleMetamaskActive () { toggleMetamaskActive () {
if (!this.props.isUnlocked) { if (!this.props.isUnlocked) {
// currently inactive: redirect to password box // currently inactive: redirect to password box
@ -267,7 +230,7 @@ App.propTypes = {
provider: PropTypes.object, provider: PropTypes.object,
frequentRpcList: PropTypes.array, frequentRpcList: PropTypes.array,
currentView: PropTypes.object, currentView: PropTypes.object,
sidebarOpen: PropTypes.bool, sidebar: PropTypes.object,
alertOpen: PropTypes.bool, alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func, hideSidebar: PropTypes.func,
isMascara: PropTypes.bool, isMascara: PropTypes.bool,
@ -303,7 +266,7 @@ function mapStateToProps (state) {
const { appState, metamask } = state const { appState, metamask } = state
const { const {
networkDropdownOpen, networkDropdownOpen,
sidebarOpen, sidebar,
alertOpen, alertOpen,
alertMessage, alertMessage,
isLoading, isLoading,
@ -330,7 +293,7 @@ function mapStateToProps (state) {
return { return {
// state from plugin // state from plugin
networkDropdownOpen, networkDropdownOpen,
sidebarOpen, sidebar,
alertOpen, alertOpen,
alertMessage, alertMessage,
isLoading, isLoading,

@ -4,8 +4,8 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const TokenBalance = require('./token-balance') const TokenBalance = require('./token-balance')
const Identicon = require('./identicon') const Identicon = require('./identicon')
const currencyFormatter = require('currency-formatter') import CurrencyDisplay from './currency-display'
const currencies = require('currency-formatter/currencies') const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance, generateBalanceObject } = require('../util') const { formatBalance, generateBalanceObject } = require('../util')
@ -20,8 +20,9 @@ function mapStateToProps (state) {
return { return {
account, account,
network, network,
conversionRate: state.metamask.conversionRate, conversionRate: conversionRateSelector(state),
currentCurrency: state.metamask.currentCurrency, currentCurrency: getCurrentCurrency(state),
assetImages: getAssetImages(state),
} }
} }
@ -32,7 +33,9 @@ function BalanceComponent () {
BalanceComponent.prototype.render = function () { BalanceComponent.prototype.render = function () {
const props = this.props const props = this.props
const { token, network } = props const { token, network, assetImages } = props
const address = token && token.address
const image = assetImages && address ? assetImages[token.address] : undefined
return h('div.balance-container', {}, [ return h('div.balance-container', {}, [
@ -43,8 +46,9 @@ BalanceComponent.prototype.render = function () {
// }), // }),
h(Identicon, { h(Identicon, {
diameter: 50, diameter: 50,
address: token && token.address, address,
network, network,
image,
}), }),
token ? this.renderTokenBalance() : this.renderBalance(), token ? this.renderTokenBalance() : this.renderBalance(),
@ -80,38 +84,12 @@ BalanceComponent.prototype.renderBalance = function () {
style: {}, style: {},
}, this.getTokenBalance(formattedBalance, shorten)), }, this.getTokenBalance(formattedBalance, shorten)),
showFiat ? this.renderFiatValue(formattedBalance) : null, showFiat && h(CurrencyDisplay, {
value: balanceValue,
}),
]) ])
} }
BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
const { conversionRate, currentCurrency } = this.props
const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
}
BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
if (shouldNotRenderFiat) return null
const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
? currencyFormatter.format(Number(fiatDisplayNumber), {
code: upperCaseFiatSuffix,
})
: `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
return h('div.fiat-amount', {
style: {},
}, display)
}
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)

@ -1,267 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
BuyButtonSubview.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps)(BuyButtonSubview)
function mapStateToProps (state) {
return {
identity: state.appState.identity,
account: state.metamask.accounts[state.appState.buyView.buyAddress],
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,
provider: state.metamask.provider,
context: state.appState.currentView.context,
isSubLoading: state.appState.isSubLoading,
}
}
inherits(BuyButtonSubview, Component)
function BuyButtonSubview () {
Component.call(this)
}
BuyButtonSubview.prototype.render = function () {
return (
h('div', {
style: {
width: '100%',
},
}, [
this.headerSubview(),
this.primarySubview(),
])
)
}
BuyButtonSubview.prototype.headerSubview = function () {
const props = this.props
const isLoading = props.isSubLoading
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
// header bar (back button, label)
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.backButtonContext.bind(this),
style: {
position: 'absolute',
left: '10px',
},
}),
h('h2.text-transform-uppercase.flex-center', {
style: {
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('depositEth')),
]),
// loading indication
h('div', {
style: {
position: 'absolute',
top: '57vh',
left: '49vw',
},
}, [
isLoading && h(Loading),
]),
// account panel
h('div', {
style: {
width: '80%',
},
}, [
h(AccountPanel, {
showFullAddress: true,
identity: props.identity,
account: props.account,
}),
]),
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('h3.text-transform-uppercase.flex-center', {
style: {
paddingLeft: '15px',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('selectService')),
]),
])
)
}
BuyButtonSubview.prototype.primarySubview = function () {
const props = this.props
const network = props.network
switch (network) {
case 'loading':
return
case '1':
return this.mainnetSubview()
// Ropsten, Rinkeby, Kovan
case '3':
case '4':
case '42':
const networkName = getNetworkDisplayName(network)
const label = `${networkName} ${this.context.t('testFaucet')}`
return (
h('div.flex-column', {
style: {
alignItems: 'center',
margin: '20px 50px',
},
}, [
h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth({ network })),
style: {
marginTop: '15px',
},
}, label),
// Kovan only: Dharma loans beta
network === '42' ? (
h('button.text-transform-uppercase', {
onClick: () => this.navigateTo('https://borrow.dharma.io/'),
style: {
marginTop: '15px',
},
}, this.context.t('borrowDharma'))
) : null,
])
)
default:
return (
h('h2.error', this.context.t('unknownNetworkId'))
)
}
}
BuyButtonSubview.prototype.mainnetSubview = function () {
const props = this.props
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
h('.flex-row.selected-exchange', {
style: {
position: 'relative',
right: '35px',
marginTop: '20px',
marginBottom: '20px',
},
}, [
h(RadioList, {
defaultFocus: props.buyView.subview,
labels: [
'Coinbase',
'ShapeShift',
],
subtext: {
'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
'ShapeShift': this.context.t('crypto'),
},
onClick: this.radioHandler.bind(this),
}),
]),
h('h3.text-transform-uppercase', {
style: {
paddingLeft: '15px',
fontFamily: 'Montserrat Light',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, props.buyView.subview),
this.formVersionSubview(),
])
)
}
BuyButtonSubview.prototype.formVersionSubview = function () {
const network = this.props.network
if (network === '1') {
if (this.props.buyView.formView.coinbase) {
return h(CoinbaseForm, this.props)
} else if (this.props.buyView.formView.shapeshift) {
return h(ShapeshiftForm, this.props)
}
}
}
BuyButtonSubview.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}
BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage({transForward: false}))
} else {
this.props.dispatch(actions.goHome())
}
}
BuyButtonSubview.prototype.radioHandler = function (event) {
switch (event.target.title) {
case 'Coinbase':
return this.props.dispatch(actions.coinBaseSubview())
case 'ShapeShift':
return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
}
}

@ -18,6 +18,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle: PropTypes.bool, hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
summaryComponent: PropTypes.node, summaryComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@ -60,6 +61,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle, hideSubtitle,
identiconAddress, identiconAddress,
nonce, nonce,
assetImage,
summaryComponent, summaryComponent,
detailsComponent, detailsComponent,
dataComponent, dataComponent,
@ -85,6 +87,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle={hideSubtitle} hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
nonce={nonce} nonce={nonce}
assetImage={assetImage}
/> />
) )
} }

@ -11,7 +11,9 @@ const ConfirmPageContainerError = (props, context) => {
src="/images/alert-red.svg" src="/images/alert-red.svg"
className="confirm-page-container-error__icon" className="confirm-page-container-error__icon"
/> />
{ `ALERT: ${error}` } <div className="confirm-page-container-error__text">
{ `ALERT: ${error}` }
</div>
</div> </div>
) )
} }

@ -1,5 +1,5 @@
.confirm-page-container-error { .confirm-page-container-error {
height: 32px; min-height: 32px;
border: 1px solid $monzo; border: 1px solid $monzo;
color: $monzo; color: $monzo;
background: lighten($monzo, 56%); background: lighten($monzo, 56%);
@ -8,10 +8,14 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding-left: 16px; padding: 8px 16px;
&__icon { &__icon {
margin-right: 8px; margin-right: 8px;
flex: 0 0 auto; flex: 0 0 auto;
} }
&__text {
overflow: auto;
}
} }

@ -4,7 +4,7 @@ import classnames from 'classnames'
import Identicon from '../../../identicon' import Identicon from '../../../identicon'
const ConfirmPageContainerSummary = props => { const ConfirmPageContainerSummary = props => {
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props
return ( return (
<div className={classnames('confirm-page-container-summary', className)}> <div className={classnames('confirm-page-container-summary', className)}>
@ -27,6 +27,7 @@ const ConfirmPageContainerSummary = props => {
className="confirm-page-container-summary__identicon" className="confirm-page-container-summary__identicon"
diameter={36} diameter={36}
address={identiconAddress} address={identiconAddress}
image={assetImage}
/> />
) )
} }
@ -51,6 +52,7 @@ ConfirmPageContainerSummary.propTypes = {
className: PropTypes.string, className: PropTypes.string,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
} }
export default ConfirmPageContainerSummary export default ConfirmPageContainerSummary

@ -38,6 +38,7 @@ export default class ConfirmPageContainer extends Component {
detailsComponent: PropTypes.node, detailsComponent: PropTypes.node,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
summaryComponent: PropTypes.node, summaryComponent: PropTypes.node,
warning: PropTypes.string, warning: PropTypes.string,
// Footer // Footer
@ -70,8 +71,10 @@ export default class ConfirmPageContainer extends Component {
onSubmit, onSubmit,
identiconAddress, identiconAddress,
nonce, nonce,
assetImage,
warning, warning,
} = this.props } = this.props
const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
return ( return (
<div className="page-container"> <div className="page-container">
@ -84,6 +87,7 @@ export default class ConfirmPageContainer extends Component {
senderAddress={fromAddress} senderAddress={fromAddress}
recipientName={toName} recipientName={toName}
recipientAddress={toAddress} recipientAddress={toAddress}
assetImage={renderAssetImage ? assetImage : undefined}
/> />
</ConfirmPageContainerHeader> </ConfirmPageContainerHeader>
{ {
@ -101,6 +105,7 @@ export default class ConfirmPageContainer extends Component {
errorKey={errorKey} errorKey={errorKey}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
nonce={nonce} nonce={nonce}
assetImage={assetImage}
warning={warning} warning={warning}
/> />
) )

@ -0,0 +1,26 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { ETH } from '../../constants/common'
export default class CurrencyDisplay extends PureComponent {
static propTypes = {
className: PropTypes.string,
displayValue: PropTypes.string,
prefix: PropTypes.string,
currency: PropTypes.oneOf([ETH]),
}
render () {
const { className, displayValue, prefix } = this.props
const text = `${prefix || ''}${displayValue}`
return (
<div
className={className}
title={text}
>
{ text }
</div>
)
}
}

@ -0,0 +1,19 @@
import { connect } from 'react-redux'
import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { value, numberOfDecimals = 2, currency } = ownProps
const { metamask: { currentCurrency, conversionRate } } = state
const toCurrency = currency || currentCurrency
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
const formattedValue = formatCurrency(convertedValue, toCurrency)
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
return {
displayValue,
}
}
export default connect(mapStateToProps)(CurrencyDisplay)

@ -0,0 +1 @@
export { default } from './currency-display.container'

@ -0,0 +1,27 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CurrencyDisplay from '../currency-display.component'
describe('CurrencyDisplay Component', () => {
it('should render text with a className', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '$123.45')
})
it('should render text with a prefix', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
prefix="-"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '-$123.45')
})
})

@ -0,0 +1,61 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../currency-display.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('CurrencyDisplay container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
},
}
const tests = [
{
props: {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x2386f26fc10000',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
},
result: {
displayValue: '1.266 ETH',
},
},
]
tests.forEach(({ props, result }) => {
assert.deepEqual(mapStateToProps(mockState, props), result)
})
})
})
})

@ -1,60 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = RadioList
inherits(RadioList, Component)
function RadioList () {
Component.call(this)
}
RadioList.prototype.render = function () {
const props = this.props
const activeClass = '.custom-radio-selected'
const inactiveClass = '.custom-radio-inactive'
const {
labels,
defaultFocus,
} = props
return (
h('.flex-row', {
style: {
fontSize: '12px',
},
}, [
h('.flex-column.custom-radios', {
style: {
marginRight: '5px',
},
},
labels.map((lable, i) => {
let isSelcted = (this.state !== null)
isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
return h(isSelcted ? activeClass : inactiveClass, {
title: lable,
onClick: (event) => {
this.setState({selected: event.target.title})
props.onClick(event)
},
})
})
),
h('.text', {},
labels.map((lable) => {
if (props.subtext) {
return h('.flex-row', {}, [
h('.radio-titles', lable),
h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
])
} else {
return h('.radio-titles', lable)
}
})
),
])
)
}

@ -459,7 +459,7 @@ const mapDispatchToProps = (dispatch) => {
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
keyrings: state.metamask.keyrings, keyrings: state.metamask.keyrings,
sidebarOpen: state.appState.sidebarOpen, sidebarOpen: state.appState.sidebar.isOpen,
} }
} }

@ -272,10 +272,12 @@ NetworkDropdown.prototype.getNetworkName = function () {
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props const props = this.props
const rpcTarget = provider.rpcTarget const reversedRpcList = rpcList.slice().reverse()
return rpcList.map((rpc) => { return reversedRpcList.map((rpc) => {
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
return null return null
} else { } else {
return h( return h(
@ -291,11 +293,11 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
}, },
}, },
[ [
rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h('i.fa.fa-question-circle.fa-med.menu-icon-circle'), h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
h('span.network-name-item', { h('span.network-name-item', {
style: { style: {
color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b', color: currentRpcTarget ? '#ffffff' : '#9b9b9b',
}, },
}, rpc), }, rpc),
] ]

@ -26,36 +26,42 @@ function mapStateToProps (state) {
IdenticonComponent.prototype.render = function () { IdenticonComponent.prototype.render = function () {
var props = this.props var props = this.props
const { className = '', address } = props const { className = '', address, image } = props
var diameter = props.diameter || this.defaultDiameter var diameter = props.diameter || this.defaultDiameter
const style = {
return address height: diameter,
? ( width: diameter,
h('div', { borderRadius: diameter / 2,
className: `${className} identicon`, }
key: 'identicon-' + address, if (image) {
style: { return h('img', {
display: 'flex', className: `${className} identicon`,
flexShrink: 0, src: image,
alignItems: 'center', style: {
justifyContent: 'center', ...style,
height: diameter, },
width: diameter, })
borderRadius: diameter / 2, } else if (address) {
overflow: 'hidden', return h('div', {
}, className: `${className} identicon`,
}) key: 'identicon-' + address,
) style: {
: ( display: 'flex',
h('img.balance-icon', { flexShrink: 0,
src: './images/eth_logo.svg', alignItems: 'center',
style: { justifyContent: 'center',
height: diameter, ...style,
width: diameter, overflow: 'hidden',
borderRadius: diameter / 2, },
}, })
}) } else {
) return h('img.balance-icon', {
src: './images/eth_logo.svg',
style: {
...style,
},
})
}
} }
IdenticonComponent.prototype.componentDidMount = function () { IdenticonComponent.prototype.componentDidMount = function () {

@ -1,23 +1,39 @@
@import './app-header/index';
@import './button-group/index'; @import './button-group/index';
@import './export-text-container/index'; @import './confirm-page-container/index';
@import './selected-account/index'; @import './export-text-container/index';
@import './info-box/index'; @import './info-box/index';
@import './network-display/index'; @import './menu-bar/index';
@import './confirm-page-container/index'; @import './modals/index';
@import './network-display/index';
@import './page-container/index'; @import './page-container/index';
@import './pages/index'; @import './pages/index';
@import './modals/index'; @import './selected-account/index';
@import './sender-to-recipient/index'; @import './sender-to-recipient/index';
@import './tabs/index'; @import './tabs/index';
@import './transaction-view/index';
@import './transaction-view-balance/index';
@import './transaction-list/index';
@import './transaction-list-item/index';
@import './transaction-status/index';
@import './app-header/index'; @import './app-header/index';
@import './sidebars/index';

@ -0,0 +1 @@
export { default } from './menu-bar.container'

@ -0,0 +1,23 @@
.menu-bar {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex: 0 0 auto;
margin-bottom: 16px;
padding: 5px;
border-bottom: 1px solid #e5e5e5;
&__sidebar-button {
font-size: 1.25rem;
cursor: pointer;
padding: 10px;
}
&__open-in-browser {
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
}
}

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Tooltip from '../tooltip'
import SelectedAccount from '../selected-account'
export default class MenuBar extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
sidebarOpen: PropTypes.bool,
showSidebar: PropTypes.func,
}
render () {
const { t } = this.context
const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
return (
<div className="menu-bar">
<Tooltip
title={t('menu')}
position="bottom"
>
<div
className="fa fa-bars menu-bar__sidebar-button"
onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
/>
</Tooltip>
<SelectedAccount />
{
!isMascara && (
<Tooltip
title={t('openInTab')}
position="bottom"
>
<div
className="menu-bar__open-in-browser"
onClick={() => global.platform.openExtensionInBrowser()}
>
<img src="images/popout.svg" />
</div>
</Tooltip>
)
}
</div>
)
}
}

@ -0,0 +1,26 @@
import { connect } from 'react-redux'
import MenuBar from './menu-bar.component'
import { showSidebar, hideSidebar } from '../../actions'
const mapStateToProps = state => {
const { appState: { sidebar: { isOpen }, isMascara } } = state
return {
sidebarOpen: isOpen,
isMascara,
}
}
const mapDispatchToProps = dispatch => {
return {
showSidebar: () => {
dispatch(showSidebar({
transitionName: 'sidebar-right',
type: 'wallet-view',
}))
},
hideSidebar: () => dispatch(hideSidebar()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)

@ -61,7 +61,7 @@ AccountDetailsModal.prototype.render = function () {
let exportPrivateKeyFeatureEnabled = true let exportPrivateKeyFeatureEnabled = true
// This feature is disabled for hardware wallets // This feature is disabled for hardware wallets
if (keyring.type.search('Hardware') !== -1) { if (keyring && keyring.type.search('Hardware') !== -1) {
exportPrivateKeyFeatureEnabled = false exportPrivateKeyFeatureEnabled = false
} }

@ -7,9 +7,9 @@ const actions = require('../../actions')
const { getSelectedIdentity } = require('../../selectors') const { getSelectedIdentity } = require('../../selectors')
const Identicon = require('../identicon') const Identicon = require('../identicon')
function mapStateToProps (state) { function mapStateToProps (state, ownProps) {
return { return {
selectedIdentity: getSelectedIdentity(state), selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
} }
} }

@ -1,3 +1,4 @@
const log = require('loglevel')
const Component = require('react').Component const Component = require('react').Component
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const h = require('react-hyperscript') const h = require('react-hyperscript')
@ -11,19 +12,33 @@ const ReadOnlyInput = require('../readonly-input')
const copyToClipboard = require('copy-to-clipboard') const copyToClipboard = require('copy-to-clipboard')
const { checksumAddress } = require('../../util') const { checksumAddress } = require('../../util')
function mapStateToProps (state) { function mapStateToPropsFactory () {
return { let selectedIdentity = null
warning: state.appState.warning, return function mapStateToProps (state) {
privateKey: state.appState.accountDetail.privateKey, // We should **not** change the identity displayed here even if it changes from underneath us.
network: state.metamask.network, // If we do, we will be showing the user one private key and a **different** address and name.
selectedIdentity: getSelectedIdentity(state), // Note that the selected identity **will** change from underneath us when we unlock the keyring
previousModalState: state.appState.modal.previousModalState.name, // which is the expected behavior that we are side-stepping.
selectedIdentity = selectedIdentity || getSelectedIdentity(state)
return {
warning: state.appState.warning,
privateKey: state.appState.accountDetail.privateKey,
network: state.metamask.network,
selectedIdentity,
previousModalState: state.appState.modal.previousModalState.name,
}
} }
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)), exportAccount: (password, address) => {
return dispatch(actions.exportAccount(password, address))
.then((res) => {
dispatch(actions.hideWarning())
return res
})
},
showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })), showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
hideModal: () => dispatch(actions.hideModal()), hideModal: () => dispatch(actions.hideModal()),
} }
@ -36,6 +51,7 @@ function ExportPrivateKeyModal () {
this.state = { this.state = {
password: '', password: '',
privateKey: null, privateKey: null,
showWarning: true,
} }
} }
@ -43,14 +59,18 @@ ExportPrivateKeyModal.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal) module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) { ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
const { exportAccount } = this.props const { exportAccount } = this.props
exportAccount(password, address) exportAccount(password, address)
.then(privateKey => this.setState({ privateKey })) .then(privateKey => this.setState({
privateKey,
showWarning: false,
}))
.catch((e) => log.error(e))
} }
ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) { ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
@ -110,9 +130,13 @@ ExportPrivateKeyModal.prototype.render = function () {
} = this.props } = this.props
const { name, address } = selectedIdentity const { name, address } = selectedIdentity
const { privateKey } = this.state const {
privateKey,
showWarning,
} = this.state
return h(AccountModalContainer, { return h(AccountModalContainer, {
selectedIdentity,
showBackButton: previousModalState === 'ACCOUNT_DETAILS', showBackButton: previousModalState === 'ACCOUNT_DETAILS',
backButtonAction: () => showAccountDetailModal(), backButtonAction: () => showAccountDetailModal(),
}, [ }, [
@ -134,7 +158,7 @@ ExportPrivateKeyModal.prototype.render = function () {
this.renderPasswordInput(privateKey), this.renderPasswordInput(privateKey),
!warning ? null : h('span.private-key-password-error', warning), showWarning && warning ? h('span.private-key-password-error', warning) : null,
]), ]),
h('div.private-key-password-warning', this.context.t('privateKeyWarning')), h('div.private-key-password-warning', this.context.t('privateKeyWarning')),

@ -10,6 +10,7 @@ function mapStateToProps (state) {
return { return {
network: state.metamask.network, network: state.metamask.network,
token: state.appState.modal.modalState.props.token, token: state.appState.modal.modalState.props.token,
assetImages: state.metamask.assetImages,
} }
} }
@ -40,8 +41,9 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmat
HideTokenConfirmationModal.prototype.render = function () { HideTokenConfirmationModal.prototype.render = function () {
const { token, network, hideToken, hideModal } = this.props const { token, network, hideToken, hideModal, assetImages } = this.props
const { symbol, address } = token const { symbol, address } = token
const image = assetImages[address]
return h('div.hide-token-confirmation', {}, [ return h('div.hide-token-confirmation', {}, [
h('div.hide-token-confirmation__container', { h('div.hide-token-confirmation__container', {
@ -55,6 +57,7 @@ HideTokenConfirmationModal.prototype.render = function () {
diameter: 45, diameter: 45,
address, address,
network, network,
image,
}), }),
h('div.hide-token-confirmation__symbol', {}, symbol), h('div.hide-token-confirmation__symbol', {}, symbol),

@ -109,7 +109,7 @@
&--selected { &--selected {
color: $curious-blue; color: $curious-blue;
border-bottom: 3px solid $curious-blue; border-bottom: 2px solid $curious-blue;
} }
} }

@ -1,8 +1,8 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class PageContainerHeader extends Component { export default class PageContainerHeader extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
subtitle: PropTypes.string, subtitle: PropTypes.string,
@ -11,8 +11,18 @@ export default class PageContainerHeader extends Component {
onBackButtonClick: PropTypes.func, onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object, backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string, backButtonString: PropTypes.string,
children: PropTypes.node, tabs: PropTypes.node,
}; }
renderTabs () {
const { tabs } = this.props
return tabs && (
<ul className="page-container__tabs">
{ tabs }
</ul>
)
}
renderHeaderRow () { renderHeaderRow () {
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
@ -31,15 +41,18 @@ export default class PageContainerHeader extends Component {
} }
render () { render () {
const { title, subtitle, onClose, children } = this.props const { title, subtitle, onClose, tabs } = this.props
return ( return (
<div className="page-container__header"> <div className={
classnames(
'page-container__header',
{ 'page-container__header--no-padding-bottom': Boolean(tabs) }
)
}>
{ this.renderHeaderRow() } { this.renderHeaderRow() }
{ children }
{ {
title && <div className="page-container__title"> title && <div className="page-container__title">
{ title } { title }
@ -59,6 +72,7 @@ export default class PageContainerHeader extends Component {
/> />
} }
{ this.renderTabs() }
</div> </div>
) )
} }

@ -1,30 +1,82 @@
import React, { Component } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PageContainerHeader from './page-container-header' import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer' import PageContainerFooter from './page-container-footer'
export default class PageContainer extends Component { export default class PageContainer extends PureComponent {
static propTypes = { static propTypes = {
// PageContainerHeader props // PageContainerHeader props
title: PropTypes.string.isRequired, backButtonString: PropTypes.string,
subtitle: PropTypes.string, backButtonStyles: PropTypes.object,
onBackButtonClick: PropTypes.func,
onClose: PropTypes.func, onClose: PropTypes.func,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func, subtitle: PropTypes.string,
backButtonStyles: PropTypes.object, title: PropTypes.string.isRequired,
backButtonString: PropTypes.string, // Tabs-related props
defaultActiveTabIndex: PropTypes.number,
tabsComponent: PropTypes.node,
// Content props // Content props
ContentComponent: PropTypes.func, contentComponent: PropTypes.node,
contentComponentProps: PropTypes.object,
// PageContainerFooter props // PageContainerFooter props
onCancel: PropTypes.func,
cancelText: PropTypes.string, cancelText: PropTypes.string,
disabled: PropTypes.bool,
onCancel: PropTypes.func,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
submitText: PropTypes.string, submitText: PropTypes.string,
disabled: PropTypes.bool, }
};
state = {
activeTabIndex: this.props.defaultActiveTabIndex || 0,
}
handleTabClick (activeTabIndex) {
this.setState({ activeTabIndex })
}
renderTabs () {
const { tabsComponent } = this.props
if (!tabsComponent) {
return
}
const numberOfTabs = React.Children.count(tabsComponent.props.children)
return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
return child && React.cloneElement(child, {
onClick: index => this.handleTabClick(index),
tabIndex,
isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
key: tabIndex,
className: 'page-container__tab',
activeClassName: 'page-container__tab--selected',
})
})
}
renderActiveTabContent () {
const { tabsComponent } = this.props
const { children } = tabsComponent.props
const { activeTabIndex } = this.state
return children[activeTabIndex]
? children[activeTabIndex].props.children
: children.props.children
}
renderContent () {
const { contentComponent, tabsComponent } = this.props
if (contentComponent) {
return contentComponent
} else if (tabsComponent) {
return this.renderActiveTabContent()
} else {
return null
}
}
render () { render () {
const { const {
@ -35,8 +87,6 @@ export default class PageContainer extends Component {
onBackButtonClick, onBackButtonClick,
backButtonStyles, backButtonStyles,
backButtonString, backButtonString,
ContentComponent,
contentComponentProps,
onCancel, onCancel,
cancelText, cancelText,
onSubmit, onSubmit,
@ -54,9 +104,10 @@ export default class PageContainer extends Component {
onBackButtonClick={onBackButtonClick} onBackButtonClick={onBackButtonClick}
backButtonStyles={backButtonStyles} backButtonStyles={backButtonStyles}
backButtonString={backButtonString} backButtonString={backButtonString}
tabs={this.renderTabs()}
/> />
<div className="page-container__content"> <div className="page-container__content">
<ContentComponent { ...contentComponentProps } /> { this.renderContent() }
</div> </div>
<PageContainerFooter <PageContainerFooter
onCancel={onCancel} onCancel={onCancel}
@ -68,5 +119,4 @@ export default class PageContainer extends Component {
</div> </div>
) )
} }
} }

@ -1,14 +1,14 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import classnames from 'classnames'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ethUtil from 'ethereumjs-util' import ethUtil from 'ethereumjs-util'
import { checkExistingAddresses } from './util' import { checkExistingAddresses } from './util'
import { tokenInfoGetter } from '../../../token-util' import { tokenInfoGetter } from '../../../token-util'
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button'
import TextField from '../../text-field' import TextField from '../../text-field'
import TokenList from './token-list' import TokenList from './token-list'
import TokenSearch from './token-search' import TokenSearch from './token-search'
import PageContainer from '../../page-container'
import { Tabs, Tab } from '../../tabs'
const emptyAddr = '0x0000000000000000000000000000000000000000' const emptyAddr = '0x0000000000000000000000000000000000000000'
const SEARCH_TAB = 'SEARCH' const SEARCH_TAB = 'SEARCH'
@ -206,7 +206,7 @@ class AddToken extends Component {
const validDecimals = customDecimals !== null && const validDecimals = customDecimals !== null &&
customDecimals !== '' && customDecimals !== '' &&
customDecimals >= 0 && customDecimals >= 0 &&
customDecimals < 36 customDecimals <= 36
let customDecimalsError = null let customDecimalsError = null
if (!validDecimals) { if (!validDecimals) {
@ -285,65 +285,33 @@ class AddToken extends Component {
) )
} }
renderTabs () {
return (
<Tabs>
<Tab name={this.context.t('search')}>
{ this.renderSearchToken() }
</Tab>
<Tab name={this.context.t('customToken')}>
{ this.renderCustomTokenForm() }
</Tab>
</Tabs>
)
}
render () { render () {
const { displayedTab } = this.state
const { history, clearPendingTokens } = this.props const { history, clearPendingTokens } = this.props
return ( return (
<div className="page-container"> <PageContainer
<div className="page-container__header page-container__header--no-padding-bottom"> title={this.context.t('addTokens')}
<div className="page-container__title"> tabsComponent={this.renderTabs()}
{ this.context.t('addTokens') } onSubmit={() => this.handleNext()}
</div> disabled={this.hasError() || !this.hasSelected()}
<div className="page-container__tabs"> onCancel={() => {
<div clearPendingTokens()
className={classnames('page-container__tab', { history.push(DEFAULT_ROUTE)
'page-container__tab--selected': displayedTab === SEARCH_TAB, }}
})} />
onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
>
{ this.context.t('search') }
</div>
<div
className={classnames('page-container__tab', {
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
})}
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
>
{ this.context.t('customToken') }
</div>
</div>
</div>
<div className="page-container__content">
{
displayedTab === CUSTOM_TOKEN_TAB
? this.renderCustomTokenForm()
: this.renderSearchToken()
}
</div>
<div className="page-container__footer">
<Button
type="default"
large
className="page-container__footer-button"
onClick={() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('cancel') }
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => this.handleNext()}
disabled={this.hasError() || !this.hasSelected()}
>
{ this.context.t('next') }
</Button>
</div>
</div>
) )
} }
} }

@ -0,0 +1,126 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE } from '../../../routes'
import Button from '../../button'
import Identicon from '../../../components/identicon'
import TokenBalance from '../../token-balance'
export default class ConfirmAddSuggestedToken extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
clearPendingTokens: PropTypes.func,
addToken: PropTypes.func,
pendingTokens: PropTypes.object,
removeSuggestedTokens: PropTypes.func,
}
componentDidMount () {
const { pendingTokens = {}, history } = this.props
if (Object.keys(pendingTokens).length === 0) {
history.push(DEFAULT_ROUTE)
}
}
getTokenName (name, symbol) {
return typeof name === 'undefined'
? symbol
: `${name} (${symbol})`
}
render () {
const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props
const pendingTokenKey = Object.keys(pendingTokens)[0]
const pendingToken = pendingTokens[pendingTokenKey]
return (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{ this.context.t('addSuggestedTokens') }
</div>
<div className="page-container__subtitle">
{ this.context.t('likeToAddTokens') }
</div>
</div>
<div className="page-container__content">
<div className="confirm-add-token">
<div className="confirm-add-token__header">
<div className="confirm-add-token__token">
{ this.context.t('token') }
</div>
<div className="confirm-add-token__balance">
{ this.context.t('balance') }
</div>
</div>
<div className="confirm-add-token__token-list">
{
Object.entries(pendingTokens)
.map(([ address, token ]) => {
const { name, symbol, image } = token
return (
<div
className="confirm-add-token__token-list-item"
key={address}
>
<div className="confirm-add-token__token confirm-add-token__data">
<Identicon
className="confirm-add-token__token-icon"
diameter={48}
address={address}
image={image}
/>
<div className="confirm-add-token__name">
{ this.getTokenName(name, symbol) }
</div>
</div>
<div className="confirm-add-token__balance">
<TokenBalance token={token} />
</div>
</div>
)
})
}
</div>
</div>
</div>
<div className="page-container__footer">
<Button
type="default"
large
className="page-container__footer-button"
onClick={() => {
removeSuggestedTokens()
.then(() => {
history.push(DEFAULT_ROUTE)
})
}}
>
{ this.context.t('cancel') }
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => {
addToken(pendingToken)
.then(() => {
removeSuggestedTokens()
.then(() => {
history.push(DEFAULT_ROUTE)
})
})
}}
>
{ this.context.t('addToken') }
</Button>
</div>
</div>
)
}
}

@ -0,0 +1,29 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
import { withRouter } from 'react-router-dom'
const extend = require('xtend')
const { addToken, removeSuggestedTokens } = require('../../../actions')
const mapStateToProps = ({ metamask }) => {
const { pendingTokens, suggestedTokens } = metamask
const params = extend(pendingTokens, suggestedTokens)
return {
pendingTokens: params,
}
}
const mapDispatchToProps = dispatch => {
return {
addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)),
removeSuggestedTokens: () => dispatch(removeSuggestedTokens()),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmAddSuggestedToken)

@ -0,0 +1,2 @@
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'
module.exports = ConfirmAddSuggestedToken

@ -2,8 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button' import Button from '../../button'
import Identicon from '../../../components/identicon' import Identicon from '../../identicon'
import TokenBalance from './token-balance' import TokenBalance from '../../token-balance'
export default class ConfirmAddToken extends Component { export default class ConfirmAddToken extends Component {
static contextTypes = { static contextTypes = {

@ -1,2 +0,0 @@
import TokenBalance from './token-balance.container'
module.exports = TokenBalance

@ -1,16 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class TokenBalance extends Component {
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
error: PropTypes.string,
}
render () {
return (
<div className="hide-text-overflow">{ this.props.string }</div>
)
}
}

@ -38,6 +38,7 @@ export default class ConfirmTransactionBase extends Component {
isTxReprice: PropTypes.bool, isTxReprice: PropTypes.bool,
methodData: PropTypes.object, methodData: PropTypes.object,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
sendTransaction: PropTypes.func, sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func, showCustomizeGasModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func, showTransactionConfirmedModal: PropTypes.func,
@ -73,6 +74,7 @@ export default class ConfirmTransactionBase extends Component {
state = { state = {
submitting: false, submitting: false,
submitError: null,
} }
componentDidUpdate () { componentDidUpdate () {
@ -268,7 +270,7 @@ export default class ConfirmTransactionBase extends Component {
return return
} }
this.setState({ submitting: true }) this.setState({ submitting: true, submitError: null })
if (onSubmit) { if (onSubmit) {
Promise.resolve(onSubmit(txData)) Promise.resolve(onSubmit(txData))
@ -280,7 +282,9 @@ export default class ConfirmTransactionBase extends Component {
this.setState({ submitting: false }) this.setState({ submitting: false })
history.push(DEFAULT_ROUTE) history.push(DEFAULT_ROUTE)
}) })
.catch(() => this.setState({ submitting: false })) .catch(error => {
this.setState({ submitting: false, submitError: error.message })
})
} }
} }
@ -307,9 +311,10 @@ export default class ConfirmTransactionBase extends Component {
contentComponent, contentComponent,
onEdit, onEdit,
nonce, nonce,
assetImage,
warning, warning,
} = this.props } = this.props
const { submitting } = this.state const { submitting, submitError } = this.state
const { name } = methodData const { name } = methodData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
@ -331,8 +336,9 @@ export default class ConfirmTransactionBase extends Component {
dataComponent={this.renderData()} dataComponent={this.renderData()}
contentComponent={contentComponent} contentComponent={contentComponent}
nonce={nonce} nonce={nonce}
assetImage={assetImage}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
errorMessage={errorMessage} errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey} errorKey={propsErrorKey || errorKey}
warning={warning} warning={warning}
disabled={!propsValid || !valid || submitting} disabled={!propsValid || !valid || submitting}

@ -52,8 +52,9 @@ const mapStateToProps = (state, props) => {
accounts, accounts,
selectedAddress, selectedAddress,
selectedAddressTxList, selectedAddressTxList,
assetImages,
} = metamask } = metamask
const assetImage = assetImages[txParamsToAddress]
const { balance } = accounts[selectedAddress] const { balance } = accounts[selectedAddress]
const { name: fromName } = identities[selectedAddress] const { name: fromName } = identities[selectedAddress]
const toAddress = propsToAddress || txParamsToAddress const toAddress = propsToAddress || txParamsToAddress
@ -88,6 +89,7 @@ const mapStateToProps = (state, props) => {
conversionRate, conversionRate,
transactionStatus, transactionStatus,
nonce, nonce,
assetImage,
} }
} }

@ -12,25 +12,27 @@ import {
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
} from '../../../routes' } from '../../../routes'
import { isConfirmDeployContract } from './confirm-transaction-switch.util' import { isConfirmDeployContract } from '../../../helpers/transactions.util'
import { import {
TOKEN_METHOD_TRANSFER, TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE, TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM, TOKEN_METHOD_TRANSFER_FROM,
} from './confirm-transaction-switch.constants' } from '../../../constants/transactions'
export default class ConfirmTransactionSwitch extends Component { export default class ConfirmTransactionSwitch extends Component {
static propTypes = { static propTypes = {
txData: PropTypes.object, txData: PropTypes.object,
methodData: PropTypes.object, methodData: PropTypes.object,
fetchingMethodData: PropTypes.bool, fetchingData: PropTypes.bool,
isEtherTransaction: PropTypes.bool,
} }
redirectToTransaction () { redirectToTransaction () {
const { const {
txData, txData,
methodData: { name }, methodData: { name },
fetchingMethodData, fetchingData,
isEtherTransaction,
} = this.props } = this.props
const { id, txParams: { data } = {} } = txData const { id, txParams: { data } = {} } = txData
@ -39,10 +41,15 @@ export default class ConfirmTransactionSwitch extends Component {
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />
} }
if (fetchingMethodData) { if (fetchingData) {
return <Loading /> return <Loading />
} }
if (isEtherTransaction) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
return <Redirect to={{ pathname }} />
}
if (data) { if (data) {
const methodName = name && name.toLowerCase() const methodName = name && name.toLowerCase()

@ -1,3 +0,0 @@
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'

@ -6,14 +6,16 @@ const mapStateToProps = state => {
confirmTransaction: { confirmTransaction: {
txData, txData,
methodData, methodData,
fetchingMethodData, fetchingData,
toSmartContract,
}, },
} = state } = state
return { return {
txData, txData,
methodData, methodData,
fetchingMethodData, fetchingData,
isEtherTransaction: !toSmartContract,
} }
} }

@ -1,239 +0,0 @@
const { Component } = require('react')
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const { Redirect, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
const actions = require('../../actions')
const log = require('loglevel')
// init
const NewKeyChainScreen = require('../../new-keychain')
// mascara
const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
// accounts
const MainContainer = require('../../main-container')
// other views
const BuyView = require('../../components/buy-button-subview')
const QrView = require('../../components/qr-code')
// Routes
const {
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
} = require('../../routes')
const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
class Home extends Component {
componentDidMount () {
const {
history,
unconfirmedTransactionsCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () {
log.debug('rendering primary')
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
currentView,
activeAddress,
seedWords,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return h(Redirect, {
to: {
pathname: NOTICE_ROUTE,
},
})
}
// seed words
if (seedWords) {
log.debug('rendering seed words')
return h(Redirect, {
to: {
pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
},
})
}
if (forgottenPassword) {
log.debug('rendering restore vault screen')
return h(Redirect, {
to: {
pathname: RESTORE_VAULT_ROUTE,
},
})
}
// show current view
switch (currentView.name) {
case 'accountDetail':
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
default:
log.debug('rendering default, account detail screen')
return h(MainContainer, {key: 'account-detail'})
}
}
}
Home.propTypes = {
currentCurrency: PropTypes.string,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
isUnlocked: PropTypes.bool,
networkDropdownOpen: PropTypes.bool,
history: PropTypes.object,
dispatch: PropTypes.func,
selectedAddress: PropTypes.string,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
isInitialized: PropTypes.bool,
forgottenPassword: PropTypes.bool,
activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
isMouseUser: PropTypes.bool,
t: PropTypes.func,
unconfirmedTransactionsCount: PropTypes.number,
}
function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
} = appState
const {
accounts,
address,
isInitialized,
noActiveNotices,
seedWords,
unapprovedTxs,
nextUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
} = metamask
const selected = address || Object.keys(accounts)[0]
return {
// state from plugin
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
noActiveNotices,
isInitialized,
isUnlocked: state.metamask.isUnlocked,
selectedAddress: state.metamask.selectedAddress,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords,
unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
menuOpen: state.appState.menuOpen,
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser,
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
Qr: state.appState.Qr,
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
// state needed to get account dropdown temporarily rendering from app bar
selected,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps)
)(Home)

@ -0,0 +1,77 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Media from 'react-media'
import { Redirect } from 'react-router-dom'
import WalletView from '../../wallet-view'
import TransactionView from '../../transaction-view'
import {
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
} from '../../../routes'
export default class Home extends PureComponent {
static propTypes = {
history: PropTypes.object,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
forgottenPassword: PropTypes.bool,
seedWords: PropTypes.string,
suggestedTokens: PropTypes.object,
unconfirmedTransactionsCount: PropTypes.number,
}
componentDidMount () {
const {
history,
suggestedTokens = {},
unconfirmedTransactionsCount = 0,
} = this.props
// suggested new tokens
if (Object.keys(suggestedTokens).length > 0) {
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
}
if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () {
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return <Redirect to={{ pathname: NOTICE_ROUTE }} />
}
// seed words
if (seedWords) {
return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
}
if (forgottenPassword) {
return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
}
return (
<div className="main-container">
<div className="account-and-transaction-details">
<Media
query="(min-width: 576px)"
render={() => <WalletView />}
/>
<TransactionView />
</div>
</div>
)
}
}

@ -0,0 +1,30 @@
import Home from './home.component'
import { compose } from 'recompose'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction'
const mapStateToProps = state => {
const { metamask, appState } = state
const {
noActiveNotices,
lostAccounts,
seedWords,
suggestedTokens,
} = metamask
const { forgottenPassword } = appState
return {
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
suggestedTokens,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
export default compose(
withRouter,
connect(mapStateToProps)
)(Home)

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

@ -66,6 +66,30 @@ class Settings extends Component {
]) ])
} }
renderHexDataOptIn () {
const { metamask: { featureFlags: { sendHexData } }, setHexDataFeatureFlag } = this.props
return h('div.settings__content-row', [
h('div.settings__content-item', [
h('span', this.context.t('showHexData')),
h(
'div.settings__content-description',
this.context.t('showHexDataDescription')
),
]),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h(ToggleButton, {
value: sendHexData,
onToggle: (value) => setHexDataFeatureFlag(!value),
activeLabel: '',
inactiveLabel: '',
}),
]),
]),
])
}
renderCurrentConversion () { renderCurrentConversion () {
const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
@ -307,6 +331,7 @@ class Settings extends Component {
!isMascara && this.renderOldUI(), !isMascara && this.renderOldUI(),
this.renderResetAccount(), this.renderResetAccount(),
this.renderBlockieOptIn(), this.renderBlockieOptIn(),
this.renderHexDataOptIn(),
]) ])
) )
} }
@ -315,6 +340,7 @@ class Settings extends Component {
Settings.propTypes = { Settings.propTypes = {
metamask: PropTypes.object, metamask: PropTypes.object,
setUseBlockie: PropTypes.func, setUseBlockie: PropTypes.func,
setHexDataFeatureFlag: PropTypes.func,
setCurrentCurrency: PropTypes.func, setCurrentCurrency: PropTypes.func,
setRpcTarget: PropTypes.func, setRpcTarget: PropTypes.func,
displayWarning: PropTypes.func, displayWarning: PropTypes.func,
@ -349,6 +375,9 @@ const mapDispatchToProps = dispatch => {
setFeatureFlagToBeta: () => { setFeatureFlagToBeta: () => {
return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
}, },
setHexDataFeatureFlag: (featureFlagShowState) => {
return dispatch(actions.setFeatureFlag('sendHexData', featureFlagShowState))
},
showResetAccountConfirmationModal: () => { showResetAccountConfirmationModal: () => {
return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' })) return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))
}, },

@ -1,56 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const AccountPanel = require('./account-panel')
PendingMsgDetails.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(PendingMsgDetails)
inherits(PendingMsgDetails, Component)
function PendingMsgDetails () {
Component.call(this)
}
PendingMsgDetails.prototype.render = function () {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
// message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-column.flex-space-between', [
h('label.font-small.allcaps', this.context.t('message')),
h('span.font-small', msgParams.data),
]),
]),
])
)
}

@ -1,73 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const PendingTxDetails = require('./pending-msg-details')
const connect = require('react-redux').connect
PendingMsg.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(PendingMsg)
inherits(PendingMsg, Component)
function PendingMsg () {
Component.call(this)
}
PendingMsg.prototype.render = function () {
var state = this.props
var msgData = state.txData
return (
h('div', {
key: msgData.id,
style: {
maxWidth: '350px',
},
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, this.context.t('signMessage')),
h('.error', {
style: {
margin: '10px',
},
}, [
this.context.t('signNotice'),
h('a', {
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
style: { color: 'rgb(247, 134, 28)' },
onClick: (event) => {
event.preventDefault()
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
global.platform.openWindow({ url })
},
}, this.context.t('readMore')),
]),
// message details
h(PendingTxDetails, state),
// sign + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelMessage,
}, this.context.t('cancel')),
h('button', {
onClick: state.signMessage,
}, this.context.t('sign')),
]),
])
)
}

@ -12,6 +12,7 @@ export default class SendContent extends Component {
static propTypes = { static propTypes = {
updateGas: PropTypes.func, updateGas: PropTypes.func,
scanQrCode: PropTypes.func, scanQrCode: PropTypes.func,
showHexData: PropTypes.bool,
}; };
render () { render () {
@ -25,7 +26,7 @@ export default class SendContent extends Component {
/> />
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow /> <SendGasRow />
<SendHexDataRow /> { this.props.showHexData ? <SendHexDataRow /> : null }
</div> </div>
</PageContainerContent> </PageContainerContent>
) )

@ -48,7 +48,7 @@ export default class SendToRow extends Component {
return ( return (
<SendRowWrapper <SendRowWrapper
errorType={'to'} errorType={'to'}
label={`${this.context.t('to')}`} label={`${this.context.t('to')}: `}
showError={inError} showError={inError}
> >
<EnsInput <EnsInput

@ -102,7 +102,7 @@ describe('SendToRow Component', function () {
assert.equal(errorType, 'to') assert.equal(errorType, 'to')
assert.equal(label, 'to_t') assert.equal(label, 'to_t: ')
assert.equal(showError, false) assert.equal(showError, false)
}) })

@ -8,12 +8,13 @@ import SendAmountRow from '../send-amount-row/send-amount-row.container'
import SendFromRow from '../send-from-row/send-from-row.container' import SendFromRow from '../send-from-row/send-from-row.container'
import SendGasRow from '../send-gas-row/send-gas-row.container' import SendGasRow from '../send-gas-row/send-gas-row.container'
import SendToRow from '../send-to-row/send-to-row.container' import SendToRow from '../send-to-row/send-to-row.container'
import SendHexDataRow from '../send-hex-data-row/send-hex-data-row.container'
describe('SendContent Component', function () { describe('SendContent Component', function () {
let wrapper let wrapper
beforeEach(() => { beforeEach(() => {
wrapper = shallow(<SendContent />) wrapper = shallow(<SendContent showHexData={true} />)
}) })
describe('render', () => { describe('render', () => {
@ -33,6 +34,17 @@ describe('SendContent Component', function () {
assert(PageContainerContentChild.childAt(1).is(SendToRow)) assert(PageContainerContentChild.childAt(1).is(SendToRow))
assert(PageContainerContentChild.childAt(2).is(SendAmountRow)) assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
assert(PageContainerContentChild.childAt(3).is(SendGasRow)) assert(PageContainerContentChild.childAt(3).is(SendGasRow))
assert(PageContainerContentChild.childAt(4).is(SendHexDataRow))
})
it('should not render the SendHexDataRow if props.showHexData is false', () => {
wrapper.setProps({ showHexData: false })
const PageContainerContentChild = wrapper.find(PageContainerContent).children()
assert(PageContainerContentChild.childAt(0).is(SendFromRow))
assert(PageContainerContentChild.childAt(1).is(SendToRow))
assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
assert(PageContainerContentChild.childAt(3).is(SendGasRow))
assert.equal(PageContainerContentChild.childAt(4).exists(), false)
}) })
}) })
}) })

@ -193,7 +193,7 @@ export default class SendTransactionScreen extends PersistentForm {
} }
render () { render () {
const { history } = this.props const { history, showHexData } = this.props
return ( return (
<div className="page-container"> <div className="page-container">
@ -201,6 +201,7 @@ export default class SendTransactionScreen extends PersistentForm {
<SendContent <SendContent
updateGas={(updateData) => this.updateGas(updateData)} updateGas={(updateData) => this.updateGas(updateData)}
scanQrCode={_ => this.props.scanQrCode()} scanQrCode={_ => this.props.scanQrCode()}
showHexData={showHexData}
/> />
<SendFooter history={history}/> <SendFooter history={history}/>
</div> </div>

@ -18,6 +18,7 @@ import {
getSelectedTokenToFiatRate, getSelectedTokenToFiatRate,
getSendAmount, getSendAmount,
getSendEditingTransactionId, getSendEditingTransactionId,
getSendHexDataFeatureFlagState,
getSendFromObject, getSendFromObject,
getSendTo, getSendTo,
getTokenBalance, getTokenBalance,
@ -64,6 +65,7 @@ function mapStateToProps (state) {
recentBlocks: getRecentBlocks(state), recentBlocks: getRecentBlocks(state),
selectedAddress: getSelectedAddress(state), selectedAddress: getSelectedAddress(state),
selectedToken: getSelectedToken(state), selectedToken: getSelectedToken(state),
showHexData: getSendHexDataFeatureFlagState(state),
to: getSendTo(state), to: getSendTo(state),
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state), tokenContract: getSelectedTokenContract(state),

@ -34,6 +34,7 @@ const selectors = {
getSelectedTokenToFiatRate, getSelectedTokenToFiatRate,
getSendAmount, getSendAmount,
getSendHexData, getSendHexData,
getSendHexDataFeatureFlagState,
getSendEditingTransactionId, getSendEditingTransactionId,
getSendErrors, getSendErrors,
getSendFrom, getSendFrom,
@ -216,6 +217,10 @@ function getSendHexData (state) {
return state.metamask.send.data return state.metamask.send.data
} }
function getSendHexDataFeatureFlagState (state) {
return state.metamask.featureFlags.sendHexData
}
function getSendEditingTransactionId (state) { function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId return state.metamask.send.editingTransactionId
} }

@ -47,6 +47,7 @@ describe('Send Component', function () {
recentBlocks={['mockBlock']} recentBlocks={['mockBlock']}
selectedAddress={'mockSelectedAddress'} selectedAddress={'mockSelectedAddress'}
selectedToken={'mockSelectedToken'} selectedToken={'mockSelectedToken'}
showHexData={true}
tokenBalance={'mockTokenBalance'} tokenBalance={'mockTokenBalance'}
tokenContract={'mockTokenContract'} tokenContract={'mockTokenContract'}
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal} updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
@ -328,5 +329,9 @@ describe('Send Component', function () {
} }
) )
}) })
it('should pass showHexData to SendContent', () => {
assert.equal(wrapper.find(SendContent).props().showHexData, true)
})
}) })
}) })

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

Loading…
Cancel
Save