Merge branch 'develop' into WatchTokenFeature

feature/default_network_editable
Esteban Miño 6 years ago committed by GitHub
commit 3f57d5f66b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 5
      README.md
  3. 3
      app/_locales/en/messages.json
  4. 47
      app/phishing.html
  5. 2
      app/scripts/controllers/detect-tokens.js
  6. 94
      app/scripts/controllers/preferences.js
  7. 3
      app/scripts/metamask-controller.js
  8. 40
      app/scripts/migrations/028.js
  9. 1
      app/scripts/migrations/index.js
  10. 1
      mascara/src/app/first-time/confirm-seed-screen.js
  11. 9
      mascara/src/app/first-time/import-seed-phrase-screen.js
  12. 20
      mascara/src/app/first-time/index.js
  13. 86
      old-ui/app/account-qr.js
  14. 461
      old-ui/app/app.js
  15. 432
      old-ui/app/components/app-bar.js
  16. 79
      old-ui/app/components/qr-code.js
  17. 4
      old-ui/app/components/shapeshift-form.js
  18. 8
      old-ui/app/components/transaction-list-item.js
  19. 126
      old-ui/app/css/index.css
  20. 85
      old-ui/app/new-ui-annoucement.js
  21. 663
      package-lock.json
  22. 4
      package.json
  23. 5
      test/e2e/beta/contract-test/contract.js
  24. 3
      test/e2e/beta/contract-test/index.html
  25. 67
      test/e2e/beta/from-import-beta-ui.spec.js
  26. 170
      test/e2e/beta/metamask-beta-ui.spec.js
  27. 4
      test/e2e/beta/run-all.sh
  28. 15
      test/e2e/metamask.spec.js
  29. 5
      test/integration/lib/first-time.js
  30. 21
      test/unit/app/controllers/detect-tokens-test.js
  31. 158
      test/unit/app/controllers/preferences-controller-test.js
  32. 3
      ui/app/actions.js
  33. 6
      ui/app/components/confirm-page-container/confirm-page-container.component.js
  34. 1
      ui/app/components/dropdowns/components/dropdown.js
  35. 3
      ui/app/components/dropdowns/network-dropdown.js
  36. 21
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  37. 8
      ui/app/components/tx-list-item.js
  38. 1
      ui/app/css/itcss/components/network.scss

@ -2,6 +2,10 @@
## Current Master
- [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network.
## 4.9.0 Tue Aug 07 2018
- Add new tokens auto detection
- Remove rejected transactions from transaction history
- Add Trezor Support

@ -27,8 +27,9 @@ If you're a web dapp developer, we've got two types of guides for you:
## Building locally
- Install [Node.js](https://nodejs.org/en/) version 8.11.3 and npm version 6.1.0
- Install dependencies:
- If you are using nvm (recommended) running `nvm use` will automatically choose the right node version for you.
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
- Select npm 6.1.0: ```npm install -g npm@6.1.0```
- Install dependencies: ```npm install```
- Install gulp globally with `npm install -g gulp-cli`.
- Build the project to the `./dist/` folder with `gulp build`.
- Optionally, to rebuild on file changes, run `gulp dev`.

@ -513,6 +513,9 @@
"invalidRPC": {
"message": "Invalid RPC URI"
},
"invalidSeedPhrase": {
"message": "Invalid seed phrase"
},
"jsonFail": {
"message": "Something went wrong. Please make sure your JSON file is properly formatted."
},

@ -3,26 +3,29 @@
<html>
<head>
<title>Phishing Warning</title>
<title>Dangerous Website Warning</title>
<style>
body {
background: #c50000;
padding: 50px;
display: flex;
justify-content: center;
font-family: sans-serif;
}
.centered {
display: flex;
flex-direction: column;
justify-content: center;
color: white;
max-width: 600px;
}
a {
color: white;
}
body {
background: #c50000;
padding: 50px;
display: flex;
justify-content: center;
font-family: sans-serif;
}
.centered {
display: flex;
flex-direction: column;
justify-content: center;
color: white;
max-width: 600px;
}
a {
color: white;
}
</style>
<script>
@ -50,10 +53,10 @@ a {
<img src="/images/ethereum-metamask-chrome.png" style="width:100%">
<h3>ATTENTION</h3>
<p>MetaMask believes this domain to have malicious intent and has prevented you from interacting with it.</p>
<p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>.</p>
<p>You can turn MetaMask off to interact with this site, but it's advised not to.</p>
<p>If you think this domain is incorrectly flagged, <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>.</p>
<p>MetaMask believes this domain could currently compromise your security and has prevented you from interacting with it.</p>
<p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>. This includes outright malicious websites and legitimate websites that have been compromised by a malicious actor.</p>
<p>You can turn MetaMask off to interact with this site, but it is advised not to.</p>
<p>If you think this domain is incorrectly flagged or if a blocked legitimate website has resolved its security issues, <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>.</p>
</div>
</body>

@ -85,7 +85,7 @@ class DetectTokensController {
set preferences (preferences) {
if (!preferences) { return }
this._preferences = preferences
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress

@ -14,6 +14,7 @@ class PreferencesController {
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @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 {object} store.accountTokens The tokens stored per account and then per network type
* @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
* user wishes to see that feature
@ -25,6 +26,7 @@ class PreferencesController {
const initState = extend({
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
tokens: [],
suggestedTokens: {},
useBlockie: false,
@ -35,9 +37,10 @@ class PreferencesController {
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
this.showAddTokenUi = opts.showAddTokenUi
this._subscribeProviderType()
}
// PUBLIC METHODS
@ -121,12 +124,19 @@ class PreferencesController {
*/
setAddresses (addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
this.store.updateState({ identities })
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
}
/**
@ -137,11 +147,13 @@ class PreferencesController {
*/
removeAddress (address) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
}
delete identities[address]
this.store.updateState({ identities })
delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
// If the selected account is no longer valid,
// select an arbitrary other account:
@ -161,14 +173,17 @@ class PreferencesController {
*/
addAddresses (addresses) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) return
// add missing identity
const identityCount = Object.keys(identities).length
accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
this.store.updateState({ identities })
this.store.updateState({ identities, accountTokens })
}
/*
@ -226,15 +241,15 @@ class PreferencesController {
* Setter for the `selectedAddress` property
*
* @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined
* @returns {Promise<void>} Promise resolves with tokens
*
*/
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)
this.store.updateState({ selectedAddress: address })
resolve()
})
const address = normalizeAddress(_address)
this._updateTokens(address)
this.store.updateState({ selectedAddress: address })
const tokens = this.store.getState().tokens
return Promise.resolve(tokens)
}
/**
@ -283,9 +298,7 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
this.store.updateState({ tokens })
this._updateAccountTokens(tokens)
return Promise.resolve(tokens)
}
@ -298,10 +311,8 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this.store.updateState({ tokens: updatedTokens })
this._updateAccountTokens(updatedTokens)
return Promise.resolve(updatedTokens)
}
@ -443,6 +454,57 @@ class PreferencesController {
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 18 || numDecimals < 0) throw new Error(`Invalid decimals ${decimals}`)
if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
}
/**
* Updates `tokens` of current account and network.
*
* @param {string} selectedAddress Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
}

@ -88,6 +88,7 @@ module.exports = class MetamaskController extends EventEmitter {
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
showAddTokenUi: opts.showAddTokenUi,
network: this.networkController,
})
// currency controller
@ -1439,7 +1440,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* A method for activating the retrieval of price data and auto detect tokens,
* A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.

@ -0,0 +1,40 @@
// next version number
const version = 28
/*
normalizes txParams on unconfirmed txs
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.PreferencesController) {
if (newState.PreferencesController.tokens) {
const identities = newState.TransactionController.identities
const tokens = newState.PreferencesController.tokens
newState.PreferencesController.accountTokens = {}
for (const identity in identities) {
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
}
newState.PreferencesController.tokens = []
}
}
return newState
}

@ -38,4 +38,5 @@ module.exports = [
require('./025'),
require('./026'),
require('./027'),
require('./028'),
]

@ -106,6 +106,7 @@ class ConfirmSeedScreen extends Component {
key={i}
className={classnames('backup-phrase__confirm-seed-option', {
'backup-phrase__confirm-seed-option--selected': isSelected,
'backup-phrase__confirm-seed-option--unselected': !isSelected,
})}
onClick={() => {
if (!isSelected) {

@ -1,3 +1,4 @@
import {validateMnemonic} from 'bip39'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
@ -39,8 +40,12 @@ class ImportSeedPhraseScreen extends Component {
handleSeedPhraseChange (seedPhrase) {
let seedPhraseError = null
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
seedPhraseError = this.context.t('seedPhraseReq')
if (seedPhrase) {
if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
seedPhraseError = this.context.t('seedPhraseReq')
} else if (!validateMnemonic(seedPhrase)) {
seedPhraseError = this.context.t('invalidSeedPhrase')
}
}
this.setState({ seedPhrase, seedPhraseError })

@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import { withRouter, Switch, Route } from 'react-router-dom'
import { compose } from 'recompose'
import classnames from 'classnames'
import CreatePasswordScreen from './create-password-screen'
import UniqueImageScreen from './unique-image-screen'
@ -44,28 +43,9 @@ class FirstTimeFlow extends Component {
noActiveNotices: false,
};
renderAppBar () {
const { welcomeScreenSeen } = this.props
return (
<div className="alpha-warning__container">
<h2 className={classnames({
'alpha-warning': welcomeScreenSeen,
'alpha-warning-welcome-screen': !welcomeScreenSeen,
})}
>
Please be aware that this version is still under development
</h2>
</div>
)
}
render () {
const { isPopup } = this.props
return (
<div className="flex-column flex-grow">
{ !isPopup && this.renderAppBar() }
<div className="first-time-flow">
<Switch>
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />

@ -0,0 +1,86 @@
const PropTypes = require('prop-types')
const {PureComponent} = require('react')
const h = require('react-hyperscript')
const {qrcode: qrCode} = require('qrcode-npm')
const {connect} = require('react-redux')
const {isHexPrefixed} = require('ethereumjs-util')
const actions = require('../../ui/app/actions')
const CopyButton = require('./components/copyButton')
class AccountQrScreen extends PureComponent {
static defaultProps = {
warning: null,
}
static propTypes = {
dispatch: PropTypes.func.isRequired,
buyView: PropTypes.any.isRequired,
Qr: PropTypes.object.isRequired,
selectedAddress: PropTypes.string.isRequired,
warning: PropTypes.node,
}
render () {
const {dispatch, Qr, selectedAddress, warning} = this.props
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
const qrImage = qrCode(4, 'M')
qrImage.addData(address)
qrImage.make()
return h('div.flex-column.full-width', {
style: {
alignItems: 'center',
boxSizing: 'border-box',
padding: '50px',
},
}, [
h('div.flex-row.full-width', {
style: {
alignItems: 'flex-start',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick () {
dispatch(actions.backToAccountDetail(selectedAddress))
},
}),
]),
h('div.qr-header', Qr.message),
warning && h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
}, [
this.props.warning,
]),
h('div#qr-container.flex-column', {
style: {
marginTop: '25px',
marginBottom: '15px',
},
dangerouslySetInnerHTML: {
__html: qrImage.createTableTag(4),
},
}),
h('div.flex-row.full-width', [
h('h3.ellip-address.grow-tenx', Qr.data),
h(CopyButton, {
value: Qr.data,
}),
]),
])
}
}
function mapStateToProps (state) {
return {
Qr: state.appState.Qr,
buyView: state.appState.buyView,
warning: state.appState.warning,
}
}
module.exports = connect(mapStateToProps)(AccountQrScreen)

@ -14,6 +14,7 @@ const NewKeyChainScreen = require('./new-keychain')
const UnlockScreen = require('./unlock')
// accounts
const AccountDetailScreen = require('./account-detail')
const AccountQrScreen = require('./account-qr')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// notice
@ -25,17 +26,13 @@ const AddTokenScreen = require('./add-token')
const AddSuggestedTokenScreen = require('./add-suggested-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const NewUiAnnouncement = require('./new-ui-annoucement')
const AppBar = require('./components/app-bar')
const Loading = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkIndicator = require('./components/network')
const BuyView = require('./components/buy-button-subview')
const QrView = require('./components/qr-code')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
module.exports = connect(mapStateToProps)(App)
@ -88,13 +85,29 @@ function mapStateToProps (state) {
}
App.prototype.render = function () {
var props = this.props
const { isLoading, loadingMessage, transForward, network } = props
const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
`Connecting to ${this.getNetworkName()}` : null
const {
currentView,
dispatch,
isLoading,
loadingMessage,
transForward,
network,
featureFlags,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork
? `Connecting to ${this.getNetworkName()}`
: null
log.debug('Main ui render function')
if (!featureFlags.skipAnnounceBetaUI) {
return (
h(NewUiAnnouncement, {
dispatch,
})
)
}
return (
h('.flex-column.full-height', {
style: {
@ -104,12 +117,9 @@ App.prototype.render = function () {
alignItems: 'center',
},
}, [
// app bar
this.renderAppBar(),
this.renderNetworkDropdown(),
this.renderDropdown(),
h(AppBar, {
...this.props,
}),
this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
// panel content
@ -123,299 +133,6 @@ App.prototype.render = function () {
])
)
}
App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const state = this.state || {}
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('.full-width', {
height: '38px',
}, [
h('.app-header.flex-row.flex-space-between', {
style: {
alignItems: 'center',
visibility: props.isUnlocked ? 'visible' : 'none',
background: props.isUnlocked ? 'white' : 'none',
height: '38px',
position: 'relative',
zIndex: 12,
},
}, [
h('div.left-menu-section', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
// mini logo
h('img', {
height: 24,
width: 24,
src: './images/icon-128.png',
}),
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
props.isUnlocked && h('div', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
props.isUnlocked && h(AccountDropdowns, {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
selected: this.props.selectedAddress,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),
// hamburger
props.isUnlocked && h(SandwichExpando, {
className: 'sandwich-expando',
width: 16,
barHeight: 2,
padding: 0,
isOpen: state.isMainMenuOpen,
color: 'rgb(247,146,30)',
onClick: () => {
this.setState({
isMainMenuOpen: !state.isMainMenuOpen,
})
},
}),
]),
]),
])
)
}
App.prototype.renderNetworkDropdown = function () {
const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
const rpcList = props.frequentRpcList
const state = this.state || {}
const isOpen = state.isNetworkMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = [
classList.contains('menu-icon'),
classList.contains('network-name'),
classList.contains('network-indicator'),
].filter(bool => bool).length === 0
// classes from three constituent nodes of the toggle element
if (isNotToggleElement) {
this.setState({ isNetworkMenuOpen: false })
}
},
zIndex: 11,
style: {
position: 'absolute',
left: '2px',
top: '36px',
},
innerStyle: {
padding: '2px 16px 2px 0px',
},
}, [
h(
DropdownMenuItem,
{
key: 'main',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('mainnet')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.diamond'),
'Main Ethereum Network',
providerType === 'mainnet' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'ropsten',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('ropsten')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.red-dot'),
'Ropsten Test Network',
providerType === 'ropsten' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'kovan',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('kovan')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.hollow-diamond'),
'Kovan Test Network',
providerType === 'kovan' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'rinkeby',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('rinkeby')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.golden-square'),
'Rinkeby Test Network',
providerType === 'rinkeby' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'default',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('localhost')),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Localhost 8545',
activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null,
]
),
this.renderCustomOption(props.provider),
this.renderCommonRpc(rpcList, props.provider),
h(
DropdownMenuItem,
{
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => this.props.dispatch(actions.showConfigPage()),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Custom RPC',
activeNetwork === 'custom' ? h('.check', '✓') : null,
]
),
])
}
App.prototype.renderDropdown = function () {
const state = this.state || {}
const isOpen = state.isMainMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen: isOpen,
zIndex: 11,
onClickOutside: (event) => {
const classList = event.target.classList
const parentClassList = event.target.parentElement.classList
const isToggleElement = classList.contains('sandwich-expando') ||
parentClassList.contains('sandwich-expando')
if (isOpen && !isToggleElement) {
this.setState({ isMainMenuOpen: false })
}
},
style: {
position: 'absolute',
right: '2px',
top: '38px',
},
innerStyle: {},
}, [
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
}, 'Settings'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.lockMetamask()) },
}, 'Log Out'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showInfoPage()) },
}, 'Info/Help'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => {
this.props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
},
}, 'Try Beta!'),
])
}
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
@ -427,25 +144,6 @@ App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork,
})
}
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
return (
h('.flex-row', {
key: 'leftArrow',
style: style,
onClick: () => props.dispatch(actions.goBackToInitView()),
}, [
h('i.fa.fa-arrow-left.cursor-pointer'),
justArrow ? null : h('div.cursor-pointer', {
style: {
marginLeft: '3px',
},
onClick: () => props.dispatch(actions.goBackToInitView()),
}, 'BACK'),
])
)
}
App.prototype.renderPrimary = function () {
log.debug('rendering primary')
var props = this.props
@ -467,22 +165,6 @@ App.prototype.renderPrimary = function () {
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
}),
!props.isInitialized && h('.flex-row.flex-center.flex-grow', [
h('p.pointer', {
onClick: () => {
global.platform.openExtensionInBrowser()
props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
},
style: {
fontSize: '0.8em',
color: '#aeaeae',
textDecoration: 'underline',
marginTop: '32px',
},
}, 'Try Beta Version'),
]),
])
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
log.debug('rendering notice screen for lost accounts view.')
@ -586,31 +268,10 @@ App.prototype.renderPrimary = function () {
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: () => props.dispatch(actions.backToAccountDetail(props.selectedAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
return h(AccountQrScreen, {
key: 'account-qr',
selectedAddress: props.selectedAddress,
})
default:
log.debug('rendering default, account detail screen')
@ -629,41 +290,6 @@ App.prototype.toggleMetamaskActive = function () {
this.props.dispatch(actions.lockMetamask(false))
}
}
App.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
const props = this.props
if (type !== 'rpc') return null
// Concatenate long URLs
let label = rpcTarget
if (rpcTarget.length > 31) {
label = label.substr(0, 34) + '...'
}
switch (rpcTarget) {
case 'http://localhost:8545':
return null
default:
return h(
DropdownMenuItem,
{
key: rpcTarget,
onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)),
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
label,
h('.check', '✓'),
]
)
}
}
App.prototype.getNetworkName = function () {
const { provider } = this.props
const providerName = provider.type
@ -684,28 +310,3 @@ App.prototype.getNetworkName = function () {
return name
}
App.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
const rpcTarget = provider.rpcTarget
return rpcList.map((rpc) => {
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
return null
} else {
return h(
DropdownMenuItem,
{
key: `common${rpc}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
onClick: () => props.dispatch(actions.setRpcTarget(rpc)),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc ? h('.check', '✓') : null,
]
)
}
})
}

@ -0,0 +1,432 @@
const PropTypes = require('prop-types')
const {Component} = require('react')
const h = require('react-hyperscript')
const actions = require('../../../ui/app/actions')
const SandwichExpando = require('sandwich-expando')
const {Dropdown} = require('./dropdown')
const {DropdownMenuItem} = require('./dropdown')
const NetworkIndicator = require('./network')
const {AccountDropdowns} = require('./account-dropdowns')
const LOCALHOST_RPC_URL = 'http://localhost:8545'
module.exports = class AppBar extends Component {
static defaultProps = {
selectedAddress: undefined,
}
static propTypes = {
dispatch: PropTypes.func.isRequired,
frequentRpcList: PropTypes.array.isRequired,
isMascara: PropTypes.bool.isRequired,
isOnboarding: PropTypes.bool.isRequired,
identities: PropTypes.any.isRequired,
selectedAddress: PropTypes.string,
isUnlocked: PropTypes.bool.isRequired,
network: PropTypes.any.isRequired,
keyrings: PropTypes.any.isRequired,
provider: PropTypes.any.isRequired,
}
static renderSpace () {
return (
h('span', {
dangerouslySetInnerHTML: {
__html: '&nbsp;',
},
})
)
}
state = {
isNetworkMenuOpen: false,
}
renderAppBar () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('div.app-bar', [
this.renderAppBarNewUiNotice(),
this.renderAppBarAppHeader(),
])
)
}
renderAppBarNewUiNotice () {
const {dispatch} = this.props
return (
h('div.app-bar__new-ui-banner', {
style: {
height: '28px',
zIndex: 12,
},
}, [
'Try the New MetaMask',
AppBar.renderSpace(),
h('span.banner__link', {
async onClick () {
await dispatch(actions.setFeatureFlag('betaUI', true))
global.platform.openExtensionInBrowser()
},
}, [
'Now',
]),
AppBar.renderSpace(),
'or',
AppBar.renderSpace(),
h('span.banner__link', {
onClick () {
global.platform.openWindow({
url: 'https://medium.com/metamask/74dba32cc7f7',
})
},
}, [
'Learn More',
]),
])
)
}
renderAppBarAppHeader () {
const {
identities,
selectedAddress,
isUnlocked,
network,
keyrings,
provider,
} = this.props
const {
isNetworkMenuOpen,
isMainMenuOpen,
} = this.state
return (
h('.full-width', {
style: {
display: 'flex',
flexDirection: 'column',
height: '38px',
},
}, [
h('.app-header.flex-row.flex-space-between', {
style: {
alignItems: 'center',
visibility: isUnlocked ? 'visible' : 'none',
background: isUnlocked ? 'white' : 'none',
height: '38px',
position: 'relative',
zIndex: 12,
},
}, [
h('div.left-menu-section', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
// mini logo
h('img', {
height: 24,
width: 24,
src: './images/icon-128.png',
}),
h(NetworkIndicator, {
network: network,
provider: provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
isUnlocked && h('div', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
h(AccountDropdowns, {
style: {},
enableAccountsSelector: true,
identities: identities,
selected: selectedAddress,
network,
keyrings,
}, []),
h(SandwichExpando, {
className: 'sandwich-expando',
width: 16,
barHeight: 2,
padding: 0,
isOpen: isMainMenuOpen,
color: 'rgb(247,146,30)',
onClick: () => {
this.setState({
isMainMenuOpen: !isMainMenuOpen,
})
},
}),
]),
]),
])
)
}
renderNetworkDropdown () {
const {
dispatch,
frequentRpcList: rpcList,
provider,
} = this.props
const {
type: providerType,
rpcTarget: activeNetwork,
} = provider
const isOpen = this.state.isNetworkMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = [
classList.contains('menu-icon'),
classList.contains('network-name'),
classList.contains('network-indicator'),
].filter(bool => bool).length === 0
// classes from three constituent nodes of the toggle element
if (isNotToggleElement) {
this.setState({ isNetworkMenuOpen: false })
}
},
zIndex: 11,
style: {
position: 'absolute',
left: '2px',
top: '64px',
},
innerStyle: {
padding: '2px 16px 2px 0px',
},
}, [
h(DropdownMenuItem, {
key: 'main',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.setProviderType('mainnet')),
style: {
fontSize: '18px',
},
}, [
h('.menu-icon.diamond'),
'Main Ethereum Network',
providerType === 'mainnet'
? h('.check', '✓')
: null,
]),
h(DropdownMenuItem, {
key: 'ropsten',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.setProviderType('ropsten')),
style: {
fontSize: '18px',
},
}, [
h('.menu-icon.red-dot'),
'Ropsten Test Network',
providerType === 'ropsten'
? h('.check', '✓')
: null,
]),
h(DropdownMenuItem, {
key: 'kovan',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.setProviderType('kovan')),
style: {
fontSize: '18px',
},
}, [
h('.menu-icon.hollow-diamond'),
'Kovan Test Network',
providerType === 'kovan'
? h('.check', '✓')
: null,
]),
h(DropdownMenuItem, {
key: 'rinkeby',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.setProviderType('rinkeby')),
style: {
fontSize: '18px',
},
}, [
h('.menu-icon.golden-square'),
'Rinkeby Test Network',
providerType === 'rinkeby'
? h('.check', '✓')
: null,
]),
h(DropdownMenuItem, {
key: 'default',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.setProviderType('localhost')),
style: {
fontSize: '18px',
},
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Localhost 8545',
activeNetwork === LOCALHOST_RPC_URL
? h('.check', '✓')
: null,
]),
this.renderCustomOption(provider),
this.renderCommonRpc(rpcList, provider),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => dispatch(actions.showConfigPage()),
style: {
fontSize: '18px',
},
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Custom RPC',
activeNetwork === 'custom'
? h('.check', '✓')
: null,
]),
])
}
renderCustomOption ({ rpcTarget, type }) {
const {dispatch} = this.props
if (type !== 'rpc') {
return null
}
// Concatenate long URLs
let label = rpcTarget
if (rpcTarget.length > 31) {
label = label.substr(0, 34) + '...'
}
switch (rpcTarget) {
case LOCALHOST_RPC_URL:
return null
default:
return h(DropdownMenuItem, {
key: rpcTarget,
onClick: () => dispatch(actions.setRpcTarget(rpcTarget)),
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
label,
h('.check', '✓'),
])
}
}
renderCommonRpc (rpcList, {rpcTarget}) {
const {dispatch} = this.props
return rpcList.map((rpc) => {
if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) {
return null
} else {
return h(DropdownMenuItem, {
key: `common${rpc}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
onClick: () => dispatch(actions.setRpcTarget(rpc)),
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc
? h('.check', '✓')
: null,
])
}
})
}
renderDropdown () {
const {dispatch} = this.props
const isOpen = this.state.isMainMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen: isOpen,
zIndex: 11,
onClickOutside: (event) => {
const classList = event.target.classList
const parentClassList = event.target.parentElement.classList
const isToggleElement = classList.contains('sandwich-expando') ||
parentClassList.contains('sandwich-expando')
if (isOpen && !isToggleElement) {
this.setState({ isMainMenuOpen: false })
}
},
style: {
position: 'absolute',
right: '2px',
top: '66px',
},
innerStyle: {},
}, [
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { dispatch(actions.showConfigPage()) },
}, 'Settings'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { dispatch(actions.lockMetamask()) },
}, 'Log Out'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { dispatch(actions.showInfoPage()) },
}, 'Info/Help'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => {
dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
},
}, 'Try Beta!'),
])
}
render () {
return h('div.full-width', [
this.renderAppBar(),
this.renderNetworkDropdown(),
this.renderDropdown(),
])
}
}

@ -1,79 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const qrCode = require('qrcode-npm').qrcode
const inherits = require('util').inherits
const connect = require('react-redux').connect
const isHexPrefixed = require('ethereumjs-util').isHexPrefixed
const CopyButton = require('./copyButton')
module.exports = connect(mapStateToProps)(QrCodeView)
function mapStateToProps (state) {
return {
Qr: state.appState.Qr,
buyView: state.appState.buyView,
warning: state.appState.warning,
}
}
inherits(QrCodeView, Component)
function QrCodeView () {
Component.call(this)
}
QrCodeView.prototype.render = function () {
const props = this.props
const Qr = props.Qr
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
const qrImage = qrCode(4, 'M')
qrImage.addData(address)
qrImage.make()
return h('.main-container.flex-column', {
key: 'qr',
style: {
justifyContent: 'center',
paddingBottom: '45px',
paddingLeft: '45px',
paddingRight: '45px',
alignItems: 'center',
},
}, [
Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message),
this.props.warning ? this.props.warning && h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
},
this.props.warning) : null,
h('#qr-container.flex-column', {
style: {
marginTop: '25px',
marginBottom: '15px',
},
dangerouslySetInnerHTML: {
__html: qrImage.createTableTag(4),
},
}),
h('.flex-row', [
h('h3.ellip-address', {
style: {
width: '247px',
},
}, Qr.data),
h(CopyButton, {
value: Qr.data,
}),
]),
])
}
QrCodeView.prototype.renderMultiMessage = function () {
var Qr = this.props.Qr
var multiMessage = Qr.message.map((message) => h('.qr-message', message))
return multiMessage
}

@ -3,7 +3,6 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../../ui/app/actions')
const Qr = require('./qr-code')
const isValidAddress = require('../util').isValidAddress
module.exports = connect(mapStateToProps)(ShapeshiftForm)
@ -11,7 +10,6 @@ function mapStateToProps (state) {
return {
warning: state.appState.warning,
isSubLoading: state.appState.isSubLoading,
qrRequested: state.appState.qrRequested,
}
}
@ -23,7 +21,7 @@ function ShapeshiftForm () {
}
ShapeshiftForm.prototype.render = function () {
return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain()
return this.renderMain()
}
ShapeshiftForm.prototype.renderMain = function () {

@ -36,7 +36,7 @@ TransactionListItem.prototype.showRetryButton = function () {
return false
}
let currentTxIsLatest = false
let currentTxSharesEarliestNonce = false
const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
@ -45,14 +45,14 @@ TransactionListItem.prototype.showRetryButton = function () {
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transaction.id
if (currentSubmittedTxs.length > 0) {
const lastTx = currentSubmittedTxs.reduce((tx1, tx2) => {
const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
if (tx1.submittedTime < tx2.submittedTime) return tx1
return tx2
})
currentTxIsLatest = lastTx.id === transaction.id
currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
}
return currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000 && currentTxIsLatest
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000
}
TransactionListItem.prototype.render = function () {

@ -720,7 +720,131 @@ div.message-container > div:first-child {
transform: scale(1.1);
}
//Notification Modal
.new-ui-announcement {
display: flex;
flex-direction: column;
height: 100%;
background: white;
color: #4D4D4D;
font-family: Roboto, Arial, sans-serif;
padding: 1.5rem;
}
.new-ui-announcement__announcement-header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-bottom: 1rem;
}
.new-ui-announcement__announcement-header a.close {
cursor: pointer;
font-size: 32px;
line-height: 17px;
}
.new-ui-announcement__announcement-header a.close:hover {
color: inherit;
}
.new-ui-announcement__announcement-header h1 {
color: #33A4E7;
text-transform: uppercase;
font-size: 18px;
font-weight: 400;
}
.new-ui-announcement__body {
display: flex;
flex: 1;
flex-direction: column;
font-size: 10.5pt;
font-weight: 300;
}
.new-ui-announcement__body h1 {
font-size: 22px;
font-weight: 600;
padding-bottom: 1rem;
}
.new-ui-announcement__body a {
color: #33A4E7;
}
.new-ui-announcement__body .updates-list {
padding: .5rem 1rem;
}
.new-ui-announcement__body .updates-list h2 {
font-weight: 600;
}
.new-ui-announcement__body .updates-list ul {
list-style: disc inside;
}
.new-ui-announcement__footer {
display: flex;
flex-direction: column;
align-items: center;
}
.new-ui-announcement__footer h1 {
font-family: inherit;
font-weight: 600;
}
.new-ui-announcement__footer button:hover {
transform: none;
}
.new-ui-announcement__footer button.positive {
padding: 1rem;
margin: 1rem;
background: #33A4E7;
color: white;
text-transform: uppercase;
box-shadow: none;
border-radius: 5px;
font-family: inherit;
font-size: 13px;
font-weight: 400;
width: 90%;
}
.new-ui-announcement__footer button.negative {
margin: 0;
padding: 0;
background: white;
color: #33A4E7;
font-family: inherit;
font-size: 13px;
font-weight: 400;
box-shadow: none;
}
.app-bar {
width: 100%;
display: flex;
flex-direction: column;
}
.app-bar__new-ui-banner {
background: #33A4E7;
color: white;
font-size: 12px;
line-height: 12px;
padding: 8px;
font-family: Roboto, Arial, sans-serif;
font-weight: 400;
width: 100%;
}
.banner__link {
cursor: pointer;
text-decoration: underline;
}
.notification-modal-wrapper {
display: flex;

@ -0,0 +1,85 @@
const PropTypes = require('prop-types')
const {PureComponent} = require('react')
const h = require('react-hyperscript')
const actions = require('../../ui/app/actions')
module.exports = class NewUiAnnouncement extends PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
};
close = async () => {
await this.props.dispatch(actions.setFeatureFlag('skipAnnounceBetaUI', true))
}
switchToNewUi = async () => {
const flag = 'betaUI'
const enabled = true
await this.props.dispatch(actions.setFeatureFlag(
flag,
enabled,
))
await this.close()
global.platform.openExtensionInBrowser()
}
render () {
return (
h('div.new-ui-announcement', [
h('section.new-ui-announcement__announcement-header', [
h('h1', 'Announcement'),
h('a.close', {
onClick: this.close,
}, '×'),
]),
h('section.new-ui-announcement__body', [
h('h1', 'A New Version of MetaMask'),
h('p', [
"We're excited to announce a brand-new version of MetaMask with enhanced features and functionality.",
]),
h('div.updates-list', [
h('h2', 'Updates include'),
h('ul', [
h('li', 'New user interface'),
h('li', 'Full-screen mode'),
h('li', 'Better token support'),
h('li', 'Better gas controls'),
h('li', 'Advanced features for developers'),
h('li', 'New confirmation screens'),
h('li', 'And more!'),
]),
]),
h('p', [
'You can still use the current version of MetaMask. The new version is still in beta, ' +
'however we encourage you to try it out as we transition into this exciting new update.',
h('span', {
dangerouslySetInnerHTML: {
__html: '&nbsp;',
},
}),
h('a', {
href: 'https://medium.com/metamask/74dba32cc7f7',
onClick ({target}) {
const url = target.href
global.platform.openWindow({
url,
})
},
}, [
'Learn more.',
]),
]),
]),
h('section.new-ui-announcement__footer', [
h('h1', 'Ready to try the new MetaMask?'),
h('button.positive', {
onClick: this.switchToNewUi,
}, 'Try it now'),
h('button.negative', {
onClick: this.close,
}, 'No thanks, maybe later'),
]),
])
)
}
}

663
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -198,7 +198,6 @@
"semaphore": "^1.0.5",
"semver": "^5.4.1",
"shallow-copy": "0.0.1",
"superstatic": "^5.0.2",
"sw-controller": "^1.0.3",
"sw-stream": "^2.0.2",
"textarea-caret": "^3.0.1",
@ -228,7 +227,7 @@
"brfs": "^1.6.1",
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "2.36.0",
"chromedriver": "^2.41.0",
"clipboardy": "^1.2.3",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
@ -308,6 +307,7 @@
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0",
"source-map": "^0.7.2",
"static-server": "^2.2.1",
"style-loader": "^0.21.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",

@ -50,15 +50,20 @@ deployButton.addEventListener('click', async function (event) {
console.log(`contract`, contract)
document.getElementById('contractStatus').innerHTML = 'Deployed'
depositButton.addEventListener('click', function (event) {
document.getElementById('contractStatus').innerHTML = 'Deposit initiated'
contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) {
console.log(result)
document.getElementById('contractStatus').innerHTML = 'Deposit completed'
})
})
withdrawButton.addEventListener('click', function (event) {
contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
console.log(result)
document.getElementById('contractStatus').innerHTML = 'Withdrawn'
})
})
}

@ -10,6 +10,9 @@
<button id="depositButton">Deposit</button>
<button id="withdrawButton">Withdraw</button>
</div>
<div id="contractStatus" style="display: flex; font-size: 1rem;">
Not yet deployed
</div>
</div>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Send eth</div>

@ -12,9 +12,11 @@ const {
} = require('../func')
const {
checkBrowserForConsoleErrors,
closeAllWindowHandlesExcept,
verboseReportOnFailure,
findElement,
findElements,
loadExtension,
} = require('./helpers')
@ -25,6 +27,7 @@ describe('Using MetaMask with an existing account', function () {
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
const tinyDelayMs = 500
const regularDelayMs = 1000
const largeDelayMs = regularDelayMs * 2
@ -74,37 +77,51 @@ describe('Using MetaMask with an existing account', function () {
describe('New UI setup', async function () {
it('switches to first tab', async function () {
await delay(tinyDelayMs)
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
await delay(regularDelayMs)
})
it('use the local network', async function () {
const networkSelector = await findElement(driver, By.css('#network_component'))
await networkSelector.click()
await delay(regularDelayMs)
const [localhost] = await findElements(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
try {
const overlay = await findElement(driver, By.css('.full-flex-height'))
await driver.wait(until.stalenessOf(overlay))
} catch (e) {}
const button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
await button.click()
await delay(regularDelayMs)
// Close all other tabs
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
const newUiOrInfoPage = newUi || infoPage
await driver.switchTo().window(oldUi)
await driver.close()
if (infoPage !== newUiOrInfoPage) {
await driver.switchTo().window(infoPage)
await driver.close()
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
await driver.switchTo().window(tab0)
await delay(tinyDelayMs)
let selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (tab0 && selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab0)
} else if (tab1) {
await driver.switchTo().window(tab1)
selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab1)
} else if (tab2) {
await driver.switchTo().window(tab2)
selectedUrl = await driver.getCurrentUrl()
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
}
} else {
throw new Error('popup.html not found')
}
await driver.switchTo().window(newUiOrInfoPage)
await delay(regularDelayMs)
const [appTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(appTab)
await delay(tinyDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
@ -208,6 +225,16 @@ describe('Using MetaMask with an existing account', function () {
})
describe('Add an account', () => {
it('switches to localhost', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(largeDelayMs)
})
it('choose Create Account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)

@ -75,30 +75,11 @@ describe('MetaMask', function () {
})
describe('New UI setup', async function () {
let networkSelector
it('switches to first tab', async function () {
await delay(tinyDelayMs)
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
await delay(regularDelayMs)
try {
networkSelector = await findElement(driver, By.css('#network_component'))
} catch (e) {
await loadExtension(driver, extensionId)
await delay(largeDelayMs * 2)
networkSelector = await findElement(driver, By.css('#network_component'))
}
await delay(regularDelayMs)
})
it('uses the local network', async function () {
await networkSelector.click()
await delay(regularDelayMs)
const networks = await findElements(driver, By.css('.dropdown-menu-item'))
const localhost = networks[4]
await driver.wait(until.elementTextMatches(localhost, /Localhost/))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
@ -107,27 +88,40 @@ describe('MetaMask', function () {
await driver.wait(until.stalenessOf(overlay))
} catch (e) {}
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
const button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
await button.click()
await delay(regularDelayMs)
// Close all other tabs
const [oldUi, tab1, tab2] = await driver.getAllWindowHandles()
await driver.switchTo().window(oldUi)
await driver.close()
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
await driver.switchTo().window(tab0)
await delay(tinyDelayMs)
await driver.switchTo().window(tab1)
const tab1Url = await driver.getCurrentUrl()
if (tab1Url.match(/metamask.io/)) {
await driver.switchTo().window(tab1)
await driver.close()
await driver.switchTo().window(tab2)
} else if (tab2) {
await driver.switchTo().window(tab2)
await driver.close()
let selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (tab0 && selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab0)
} else if (tab1) {
await driver.switchTo().window(tab1)
selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab1)
} else if (tab2) {
await driver.switchTo().window(tab2)
selectedUrl = await driver.getCurrentUrl()
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
}
} else {
throw new Error('popup.html not found')
}
await delay(regularDelayMs)
const [appTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(appTab)
await delay(tinyDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
await continueBtn.click()
@ -201,7 +195,16 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
async function retypeSeedPhrase (words, wasReloaded) {
async function clickWordAndWait (word) {
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${word}')]`
const word0 = await findElement(driver, By.xpath(xpath), 10000)
await word0.click()
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
@ -215,67 +218,26 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
}
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
await word0.click()
await delay(tinyDelayMs)
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000)
await word1.click()
await delay(tinyDelayMs)
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
await word2.click()
await delay(tinyDelayMs)
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
await word3.click()
await delay(tinyDelayMs)
await clickWordAndWait(words[0])
await clickWordAndWait(words[1])
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])
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
await word4.click()
await delay(tinyDelayMs)
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
await word5.click()
await delay(tinyDelayMs)
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
await word6.click()
await delay(tinyDelayMs)
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`), 10000)
await word7.click()
await delay(tinyDelayMs)
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`), 10000)
await word8.click()
await delay(tinyDelayMs)
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`), 10000)
await word9.click()
await delay(tinyDelayMs)
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`), 10000)
await word10.click()
await delay(tinyDelayMs)
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`), 10000)
await word11.click()
await delay(tinyDelayMs)
} catch (e) {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true)
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
@ -389,6 +351,16 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('switches to localhost', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(largeDelayMs * 2)
})
it('balance renders', async () => {
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
@ -512,7 +484,7 @@ describe('MetaMask', function () {
it('displays the contract creation data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click()
await dataTab.click()
await delay(regularDelayMs)
await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
@ -522,7 +494,7 @@ describe('MetaMask', function () {
assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click()
await detailsTab.click()
await delay(regularDelayMs)
})
@ -543,9 +515,15 @@ describe('MetaMask', function () {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
let contractStatus = await driver.findElement(By.css('#contractStatus'))
await driver.wait(until.elementTextMatches(contractStatus, /Deployed/))
const depositButton = await findElement(driver, By.css('#depositButton'))
await depositButton.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
contractStatus = await driver.findElement(By.css('#contractStatus'))
await driver.wait(until.elementTextMatches(contractStatus, /Deposit\sinitiated/))
await driver.switchTo().window(extension)
await delay(largeDelayMs)

@ -6,5 +6,5 @@ set -o pipefail
export PATH="$PATH:./node_modules/.bin"
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'

@ -59,6 +59,13 @@ describe('Metamask popup page', function () {
await driver.switchTo().window(windowHandles[0])
})
it('does not select the new UI option', async () => {
await delay(300)
const button = await driver.findElement(By.xpath("//button[contains(text(), 'No thanks, maybe later')]"))
await button.click()
await delay(1000)
})
it('sets provider type to localhost', async function () {
await delay(300)
await setProviderType('localhost')
@ -133,9 +140,9 @@ describe('Metamask popup page', function () {
})
it('adds a second account', async function () {
await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div')).click()
await driver.findElement(By.css('div.full-width > div > div:nth-child(2) > span > div')).click()
await delay(300)
await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span')).click()
await driver.findElement(By.css('div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span')).click()
})
it('shows account address', async function () {
@ -146,7 +153,7 @@ describe('Metamask popup page', function () {
it('logs out of the vault', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
const logoutButton = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
const logoutButton = await driver.findElement(By.css('.menu-droppo > li:nth-child(3)'))
assert.equal(await logoutButton.getText(), 'Log Out')
await logoutButton.click()
})
@ -178,7 +185,7 @@ describe('Metamask popup page', function () {
it('logs out', async function () {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(200)
const logOut = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
const logOut = await driver.findElement(By.css('.menu-droppo > li:nth-child(3)'))
assert.equal(await logOut.getText(), 'Log Out')
await logOut.click()
await delay(300)

@ -27,6 +27,11 @@ async function runFirstTimeUsageTest(assert, done) {
const app = $('#app-content')
// Selects new ui
const tryNewUIButton = (await findAsync(app, 'button.negative'))[0]
tryNewUIButton.click()
await timeout()
// recurse notices
while (true) {
const button = await findAsync(app, 'button')

@ -7,10 +7,11 @@ const PreferencesController = require('../../../../app/scripts/controllers/prefe
describe('DetectTokensController', () => {
const sandbox = sinon.createSandbox()
let clock
let keyringMemStore
before(async () => {
let clock, keyringMemStore, network, preferences
beforeEach(async () => {
keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController({ provider: { type: 'mainnet' }})
preferences = new PreferencesController({ network })
})
after(() => {
sandbox.restore()
@ -25,9 +26,7 @@ describe('DetectTokensController', () => {
it('should be called on every polling period', async () => {
clock = sandbox.useFakeTimers()
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -45,9 +44,7 @@ describe('DetectTokensController', () => {
})
it('should not check tokens while in test network', async () => {
const network = new NetworkController()
network.setProviderType('rinkeby')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -61,9 +58,7 @@ describe('DetectTokensController', () => {
})
it('should only check and add tokens while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -80,9 +75,7 @@ describe('DetectTokensController', () => {
})
it('should not detect same token while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
@ -100,9 +93,7 @@ describe('DetectTokensController', () => {
})
it('should trigger detect new tokens when change address', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -112,9 +103,7 @@ describe('DetectTokensController', () => {
})
it('should trigger detect new tokens when submit password', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.selectedAddress = '0x0'
@ -124,9 +113,7 @@ describe('DetectTokensController', () => {
})
it('should not trigger detect new tokens when not open or not unlocked', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = false

@ -1,11 +1,14 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () {
let preferencesController
let network
beforeEach(() => {
preferencesController = new PreferencesController()
network = {providerStore: new ObservableStore({ type: 'mainnet' })}
preferencesController = new PreferencesController({ network })
})
describe('setAddresses', function () {
@ -28,6 +31,20 @@ describe('preferences controller', function () {
})
})
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const accountTokens = preferencesController.store.getState().accountTokens
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
})
})
it('should replace its list of addresses', function () {
preferencesController.setAddresses([
'0xda22le',
@ -64,6 +81,17 @@ describe('preferences controller', function () {
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
})
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
})
it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses([
'0xda22le',
@ -158,6 +186,42 @@ describe('preferences controller', function () {
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
})
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
})
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
})
})
describe('removeToken', function () {
@ -182,6 +246,98 @@ describe('preferences controller', function () {
const [token1] = tokens
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
})
it('should remove a token from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in account')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
})
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in network')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
})
})
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
await preferencesController.setSelectedAddress('0x7e57e2')
const initialTokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
await preferencesController.setSelectedAddress('0x7e57e2')
const tokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
})
})
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
network.providerStore.updateState({ type: 'mainnet' })
const tokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
})
})

@ -1486,11 +1486,12 @@ function showAccountDetail (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
log.debug(`background.setSelectedAddress`)
background.setSelectedAddress(address, (err) => {
background.setSelectedAddress(address, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch(updateTokens(tokens))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: address,

@ -43,7 +43,7 @@ export default class ConfirmPageContainer extends Component {
// Footer
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
valid: PropTypes.bool,
disabled: PropTypes.bool,
}
render () {
@ -54,7 +54,7 @@ export default class ConfirmPageContainer extends Component {
fromAddress,
toName,
toAddress,
valid,
disabled,
errorKey,
errorMessage,
contentComponent,
@ -110,7 +110,7 @@ export default class ConfirmPageContainer extends Component {
onSubmit={() => onSubmit()}
submitText={this.context.t('confirm')}
submitButtonType="confirm"
disabled={!valid}
disabled={disabled}
/>
</div>
)

@ -87,7 +87,6 @@ class DropdownMenuItem extends Component {
padding: '8px 0px',
fontSize: '18px',
fontStyle: 'normal',
fontFamily: 'Montserrat Regular',
cursor: 'pointer',
display: 'flex',
justifyContent: 'flex-start',

@ -71,7 +71,6 @@ NetworkDropdown.prototype.render = function () {
const rpcList = props.frequentRpcList
const isOpen = this.props.networkDropdownOpen
const dropdownMenuItemStyle = {
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
@ -286,7 +285,6 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget(rpc),
style: {
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
@ -325,7 +323,6 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) {
onClick: () => props.setRpcTarget(rpcTarget),
closeMenu: () => this.props.hideNetworkDropdown(),
style: {
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',

@ -71,6 +71,10 @@ export default class ConfirmTransactionBase extends Component {
warning: PropTypes.string,
}
state = {
submitting: false,
}
componentDidUpdate () {
const {
transactionStatus,
@ -258,15 +262,25 @@ export default class ConfirmTransactionBase extends Component {
handleSubmit () {
const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
const { submitting } = this.state
if (submitting) {
return
}
this.setState({ submitting: true })
if (onSubmit) {
onSubmit(txData)
Promise.resolve(onSubmit(txData))
.then(this.setState({ submitting: false }))
} else {
sendTransaction(txData)
.then(() => {
clearConfirmTransaction()
this.setState({ submitting: false })
history.push(DEFAULT_ROUTE)
})
.catch(() => this.setState({ submitting: false }))
}
}
@ -280,7 +294,7 @@ export default class ConfirmTransactionBase extends Component {
methodData,
ethTransactionAmount,
fiatTransactionAmount,
valid: propsValid,
valid: propsValid = true,
errorMessage,
errorKey: propsErrorKey,
currentCurrency,
@ -295,6 +309,7 @@ export default class ConfirmTransactionBase extends Component {
nonce,
warning,
} = this.props
const { submitting } = this.state
const { name } = methodData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
@ -320,7 +335,7 @@ export default class ConfirmTransactionBase extends Component {
errorMessage={errorMessage}
errorKey={propsErrorKey || errorKey}
warning={warning}
valid={propsValid || valid}
disabled={!propsValid || !valid || submitting}
onEdit={() => this.handleEdit()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}

@ -213,7 +213,7 @@ TxListItem.prototype.showRetryButton = function () {
if (!txParams) {
return false
}
let currentTxIsLatest = false
let currentTxSharesEarliestNonce = false
const currentNonce = txParams.nonce
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
@ -222,14 +222,14 @@ TxListItem.prototype.showRetryButton = function () {
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId
if (currentSubmittedTxs.length > 0) {
const lastTx = currentSubmittedTxs.reduce((tx1, tx2) => {
const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
if (tx1.submittedTime < tx2.submittedTime) return tx1
return tx2
})
currentTxIsLatest = lastTx.id === transactionId
currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
}
return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000 && currentTxIsLatest
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
}
TxListItem.prototype.setSelectedToken = function (tokenAddress) {

@ -76,7 +76,6 @@
}
.network-name-item {
font-weight: 100;
flex: 1;
color: $dusty-gray;
text-overflow: ellipsis;

Loading…
Cancel
Save