diff options
38 files changed, 1388 insertions, 1284 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1e1ff4b..ea5d90500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6f315bb7a..c4c19762d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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." }, diff --git a/app/phishing.html b/app/phishing.html index 86f2985cc..e20c9ac9c 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -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,11 +53,11 @@ 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> -</html>
\ No newline at end of file +</html> diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 195ec918a..62e639795 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -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 diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 3bbd48f06..a42bb77b3 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -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 } } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 801363cb0..1464b16a6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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. diff --git a/app/scripts/migrations/028.js b/app/scripts/migrations/028.js new file mode 100644 index 000000000..6553a1052 --- /dev/null +++ b/app/scripts/migrations/028.js @@ -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 +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index bd0005221..3b512715e 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -38,4 +38,5 @@ module.exports = [ require('./025'), require('./026'), require('./027'), + require('./028'), ] diff --git a/mascara/src/app/first-time/confirm-seed-screen.js b/mascara/src/app/first-time/confirm-seed-screen.js index 7c0431431..dfbaffe33 100644 --- a/mascara/src/app/first-time/confirm-seed-screen.js +++ b/mascara/src/app/first-time/confirm-seed-screen.js @@ -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) { diff --git a/mascara/src/app/first-time/import-seed-phrase-screen.js b/mascara/src/app/first-time/import-seed-phrase-screen.js index fd2516ad4..883893e88 100644 --- a/mascara/src/app/first-time/import-seed-phrase-screen.js +++ b/mascara/src/app/first-time/import-seed-phrase-screen.js @@ -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 }) diff --git a/mascara/src/app/first-time/index.js b/mascara/src/app/first-time/index.js index dc254bb19..6e4dc74bb 100644 --- a/mascara/src/app/first-time/index.js +++ b/mascara/src/app/first-time/index.js @@ -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} /> diff --git a/old-ui/app/account-qr.js b/old-ui/app/account-qr.js new file mode 100644 index 000000000..b41cc5112 --- /dev/null +++ b/old-ui/app/account-qr.js @@ -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) diff --git a/old-ui/app/app.js b/old-ui/app/app.js index fb15d414d..f60ee5beb 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -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, - ] - ) - } - }) -} diff --git a/old-ui/app/components/app-bar.js b/old-ui/app/components/app-bar.js new file mode 100644 index 000000000..8ab647efd --- /dev/null +++ b/old-ui/app/components/app-bar.js @@ -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: ' ', + }, + }) + ) + } + + 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(), + ]) + } +} diff --git a/old-ui/app/components/qr-code.js b/old-ui/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/old-ui/app/components/qr-code.js +++ /dev/null @@ -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 -} diff --git a/old-ui/app/components/shapeshift-form.js b/old-ui/app/components/shapeshift-form.js index 97068db0a..14de309ab 100644 --- a/old-ui/app/components/shapeshift-form.js +++ b/old-ui/app/components/shapeshift-form.js @@ -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 () { diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js index f479ce666..015ef6ba6 100644 --- a/old-ui/app/components/transaction-list-item.js +++ b/old-ui/app/components/transaction-list-item.js @@ -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 () { diff --git a/old-ui/app/css/index.css b/old-ui/app/css/index.css index 7af713336..d209b4754 100644 --- a/old-ui/app/css/index.css +++ b/old-ui/app/css/index.css @@ -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; @@ -812,4 +936,4 @@ div.message-container > div:first-child { .notification-modal__link { color: #2f9ae0; -}
\ No newline at end of file +} diff --git a/old-ui/app/new-ui-annoucement.js b/old-ui/app/new-ui-annoucement.js new file mode 100644 index 000000000..59b126279 --- /dev/null +++ b/old-ui/app/new-ui-annoucement.js @@ -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: ' ', + }, + }), + 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'), + ]), + ]) + ) + } +} diff --git a/package-lock.json b/package-lock.json index 074e90cce..0efe9b481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1986,14 +1986,6 @@ } } }, - "ansi-align": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-1.1.0.tgz", - "integrity": "sha1-LwwWWIKXOa3V67FeawxuNCPwFro=", - "requires": { - "string-width": "^1.0.1" - } - }, "ansi-colors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.0.1.tgz", @@ -2318,11 +2310,6 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, - "as-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", - "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=" - }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3970,19 +3957,6 @@ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", "dev": true }, - "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "basic-auth-connect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", - "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" - }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", @@ -4251,29 +4225,6 @@ "integrity": "sha512-/gp96UlcFw5DbV2KQPCqTqi0Mb9gZRyDAHiDsGEH+4B/KOQjeoE5lM1PxlVX8DQDvfEfitmC1rW2Oy8fk/XBDg==", "dev": true }, - "boxen": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-0.6.0.tgz", - "integrity": "sha1-g2TUJIrDT/DvGy8r9JpsYM4NgbY=", - "requires": { - "ansi-align": "^1.1.0", - "camelcase": "^2.1.0", - "chalk": "^1.1.1", - "cli-boxes": "^1.0.0", - "filled-array": "^1.0.0", - "object-assign": "^4.0.1", - "repeating": "^2.0.0", - "string-width": "^1.0.1", - "widest-line": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - } - } - }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -5010,7 +4961,8 @@ "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true }, "case-sensitive-paths-webpack-plugin": { "version": "2.1.2", @@ -5084,11 +5036,6 @@ "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" }, - "char-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/char-spinner/-/char-spinner-1.0.1.tgz", - "integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=" - }, "character-entities": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.1.tgz", @@ -5169,16 +5116,16 @@ "dev": true }, "chromedriver": { - "version": "2.36.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.36.0.tgz", - "integrity": "sha512-Lq2HrigCJ4RVdIdCmchenv1rVrejNSJ7EUCQojycQo12ww3FedQx4nb+GgTdqMhjbOMTqq5+ziaiZlrEN2z1gQ==", + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.41.0.tgz", + "integrity": "sha512-6O9HxvrSuHqmRlIgMzi0/05GsDNHqs8kaF5gNTIyaZNwRzb/RBUWH1xNNXKNxyhXSnGSalH8hWsKP5mc/npSQQ==", "dev": true, "requires": { "del": "^3.0.0", - "extract-zip": "^1.6.5", + "extract-zip": "^1.6.7", "kew": "^0.7.0", "mkdirp": "^0.5.1", - "request": "^2.83.0" + "request": "^2.87.0" } }, "cipher-base": { @@ -5312,11 +5259,6 @@ "glob": "^7.1.1" } }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" - }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -5810,14 +5752,6 @@ } } }, - "compare-semver": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", - "integrity": "sha1-fAp5onu4C2xplERfgpWCWdPQIVM=", - "requires": { - "semver": "^5.0.1" - } - }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -5839,6 +5773,7 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz", "integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=", + "dev": true, "requires": { "mime-db": ">= 1.30.0 < 2" } @@ -5847,6 +5782,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "dev": true, "requires": { "accepts": "~1.3.4", "bytes": "3.0.0", @@ -5882,26 +5818,11 @@ "proto-list": "~1.2.1" } }, - "configstore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", - "integrity": "sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=", - "requires": { - "dot-prop": "^3.0.0", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" - } - }, "connect": { "version": "3.6.6", "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, "requires": { "debug": "2.6.9", "finalhandler": "1.1.0", @@ -5909,21 +5830,6 @@ "utils-merge": "1.0.1" } }, - "connect-query": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/connect-query/-/connect-query-1.0.0.tgz", - "integrity": "sha1-3kT1dyCdokBNH8BGktGkEY5YIRk=", - "requires": { - "qs": "~6.4.0" - }, - "dependencies": { - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - } - } - }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -6085,6 +5991,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, "requires": { "capture-stack-trace": "^1.0.0" } @@ -7476,14 +7383,6 @@ "integrity": "sha1-9k0h7b5v8xFJlfEGGmGpNcMAIEs=", "dev": true }, - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "requires": { - "is-obj": "^1.0.0" - } - }, "dotenv": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", @@ -8681,12 +8580,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -9939,30 +9839,33 @@ } }, "extract-zip": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", - "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", "dev": true, "requires": { - "concat-stream": "1.6.0", + "concat-stream": "1.6.2", "debug": "2.6.9", - "mkdirp": "0.5.0", + "mkdirp": "0.5.1", "yauzl": "2.4.1" }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { - "minimist": "0.0.8" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } } } @@ -10403,14 +10306,6 @@ "integrity": "sha512-h2avnhux4p3tXTA9xR7ntnQSFQdY4hAkyNj8wDXlVT2Die38JxVCInnrieuktdxzRevRWa3dBjN+SbQe1os0GQ==", "dev": true }, - "fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", - "requires": { - "punycode": "^1.3.2" - } - }, "fastparse": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", @@ -10501,6 +10396,12 @@ } } }, + "file-size": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/file-size/-/file-size-0.0.5.tgz", + "integrity": "sha1-BX1Dw6Ptc12j+Q1gUqs4Dx5tXjs=", + "dev": true + }, "file-tree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-tree/-/file-tree-1.0.0.tgz", @@ -10584,11 +10485,6 @@ "repeat-string": "^1.5.2" } }, - "filled-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz", - "integrity": "sha1-w8T2xmO5I0WamqKZEtLQMfFQf4Q=" - }, "finalhandler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", @@ -11003,49 +10899,6 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-1.0.0.tgz", "integrity": "sha1-Ad/dW8vBScZrNe1AHh11PxqtjVk=" }, - "flat-arguments": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flat-arguments/-/flat-arguments-1.0.2.tgz", - "integrity": "sha1-m6p4Ct8FAfKC1ybJxqA426ROp28=", - "requires": { - "array-flatten": "^1.0.0", - "as-array": "^1.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isobject": "^3.0.0" - }, - "dependencies": { - "as-array": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/as-array/-/as-array-1.0.0.tgz", - "integrity": "sha1-KKbu6qVynx9OyiBH316d4avaDtE=", - "requires": { - "lodash.isarguments": "2.4.x", - "lodash.isobject": "^2.4.1", - "lodash.values": "^2.4.1" - }, - "dependencies": { - "lodash.isarguments": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-2.4.1.tgz", - "integrity": "sha1-STGpwIJTrfCRrnyhkiWKlzh27Mo=" - }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - } - } - }, - "lodash.isobject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", - "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" - } - } - }, "flat-cache": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", @@ -12756,21 +12609,6 @@ "is-glob": "^2.0.0" } }, - "glob-slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", - "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=" - }, - "glob-slasher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", - "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", - "requires": { - "glob-slash": "^1.0.0", - "lodash.isobject": "^2.4.1", - "toxic": "^1.0.0" - } - }, "glob-stream": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", @@ -12918,56 +12756,6 @@ "sparkles": "^1.0.0" } }, - "got": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", - "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", - "requires": { - "create-error-class": "^3.0.1", - "duplexer2": "^0.1.4", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "node-status-codes": "^1.0.0", - "object-assign": "^4.0.1", - "parse-json": "^2.1.0", - "pinkie-promise": "^2.0.0", - "read-all-stream": "^3.0.0", - "readable-stream": "^2.0.5", - "timed-out": "^3.0.0", - "unzip-response": "^1.0.2", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "timed-out": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", - "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=" - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - } - } - }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -14629,11 +14417,6 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" }, - "home-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/home-dir/-/home-dir-1.0.0.tgz", - "integrity": "sha1-KRfrRL3JByztqUJXlUOEfjAX/k4=" - }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -15892,11 +15675,6 @@ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" - }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -16003,7 +15781,8 @@ "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true }, "is-regex": { "version": "1.0.4", @@ -16034,7 +15813,8 @@ "is-retry-allowed": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true }, "is-root": { "version": "1.0.0", @@ -16109,11 +15889,6 @@ "unc-path-regex": "^0.1.2" } }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" - }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -16333,16 +16108,6 @@ "raphael": "^2.2.0" } }, - "join-path": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", - "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", - "requires": { - "as-array": "^2.0.0", - "url-join": "0.0.1", - "valid-url": "^1" - } - }, "js-base64": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", @@ -17720,19 +17485,6 @@ "es6-weak-map": "^2.0.1" } }, - "latest-version": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz", - "integrity": "sha1-VvjWE5YghHuAF/jx9NeOIRMkFos=", - "requires": { - "package-json": "^2.0.0" - } - }, - "lazy-req": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/lazy-req/-/lazy-req-1.1.0.tgz", - "integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=" - }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -18398,16 +18150,6 @@ "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" }, - "lodash._isnative": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", - "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=" - }, - "lodash._objecttypes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", - "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=" - }, "lodash._reescape": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", @@ -18428,14 +18170,6 @@ "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" }, - "lodash._shimkeys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", - "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -18525,14 +18259,6 @@ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -18634,26 +18360,6 @@ "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=" }, - "lodash.values": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", - "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", - "requires": { - "lodash.keys": "~2.4.1" - }, - "dependencies": { - "lodash.keys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", - "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", - "requires": { - "lodash._isnative": "~2.4.1", - "lodash._shimkeys": "~2.4.1", - "lodash.isobject": "~2.4.1" - } - } - } - }, "log-driver": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", @@ -18952,7 +18658,8 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true }, "lru-cache": { "version": "4.1.1", @@ -20167,18 +19874,6 @@ "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==", "dev": true }, - "morgan": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", - "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", - "requires": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.1", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - } - }, "mout": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", @@ -20381,29 +20076,6 @@ } } }, - "nash": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nash/-/nash-2.0.4.tgz", - "integrity": "sha1-y5ZHkc79N21Zz6zYAQknRhaqFdI=", - "requires": { - "async": "^1.3.0", - "flat-arguments": "^1.0.0", - "lodash": "^3.10.0", - "minimist": "^1.1.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -20717,7 +20389,8 @@ "node-status-codes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", - "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" + "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", + "dev": true }, "node-uuid": { "version": "1.4.8", @@ -22595,7 +22268,8 @@ "on-headers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", + "dev": true }, "once": { "version": "1.4.0", @@ -22905,17 +22579,6 @@ "thunkify": "^2.1.2" } }, - "package-json": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", - "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", - "requires": { - "got": "^5.0.0", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -25669,6 +25332,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -25679,7 +25343,8 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true } } }, @@ -26252,6 +25917,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dev": true, "requires": { "pinkie-promise": "^2.0.0", "readable-stream": "^2.0.0" @@ -26584,23 +26250,6 @@ "regjsparser": "^0.1.4" } }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { - "rc": "^1.0.1" - } - }, "regjsgen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", @@ -27129,27 +26778,6 @@ "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.0.0.tgz", "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=" }, - "router": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/router/-/router-1.3.2.tgz", - "integrity": "sha1-v6oWiIpSg9XuQNmZ2nqfoVKWpgw=", - "requires": { - "array-flatten": "2.1.1", - "debug": "2.6.9", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "setprototypeof": "1.1.0", - "utils-merge": "1.0.1" - }, - "dependencies": { - "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" - } - } - }, "rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -27160,11 +26788,6 @@ "nearley": "^2.7.10" } }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" - }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -27557,14 +27180,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { - "semver": "^5.0.3" - } - }, "semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -28014,7 +27629,8 @@ "slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true }, "smart-buffer": { "version": "1.1.15", @@ -28661,6 +28277,70 @@ } } }, + "static-server": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/static-server/-/static-server-2.2.1.tgz", + "integrity": "sha512-j5eeW6higxYNmXMIT8iHjsdiViTpQDthg7o+SHsRtqdbxscdHqBHXwrXjHC8hL3F0Tsu34ApUpDkwzMBPBsrLw==", + "dev": true, + "requires": { + "chalk": "^0.5.1", + "commander": "^2.3.0", + "file-size": "0.0.5", + "mime": "^1.2.11", + "opn": "^5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -28861,6 +28541,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", + "dev": true, "requires": { "strip-ansi": "^3.0.0" } @@ -29566,63 +29247,6 @@ } } }, - "superstatic": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-5.0.2.tgz", - "integrity": "sha1-186TKe4w2qp2KwHUO1SNC3MGulQ=", - "requires": { - "as-array": "^2.0.0", - "async": "^1.5.2", - "basic-auth-connect": "^1.0.0", - "chalk": "^1.1.3", - "char-spinner": "^1.0.1", - "compare-semver": "^1.0.0", - "compression": "^1.7.0", - "connect": "^3.6.2", - "connect-query": "^1.0.0", - "destroy": "^1.0.4", - "fast-url-parser": "^1.1.3", - "fs-extra": "^0.30.0", - "glob": "^7.1.2", - "glob-slasher": "^1.0.1", - "home-dir": "^1.0.0", - "is-url": "^1.2.2", - "join-path": "^1.1.1", - "lodash": "^4.17.4", - "mime-types": "^2.1.16", - "minimatch": "^3.0.4", - "morgan": "^1.8.2", - "nash": "^2.0.4", - "on-finished": "^2.2.0", - "on-headers": "^1.0.0", - "path-to-regexp": "^1.7.0", - "router": "^1.3.1", - "rsvp": "^3.6.2", - "string-length": "^1.0.0", - "try-require": "^1.0.0", - "update-notifier": "^1.0.3" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "requires": { - "isarray": "0.0.1" - } - } - } - }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -30485,14 +30109,6 @@ "punycode": "^1.4.1" } }, - "toxic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", - "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", - "requires": { - "lodash": "^4.17.10" - } - }, "tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -30626,11 +30242,6 @@ } } }, - "try-require": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/try-require/-/try-require-1.2.1.tgz", - "integrity": "sha1-NEiaLKwMCcHMEO2RugEVlNQzO+I=" - }, "tslib": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", @@ -31149,7 +30760,8 @@ "unzip-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", - "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=" + "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=", + "dev": true }, "upath": { "version": "1.0.4", @@ -31157,21 +30769,6 @@ "integrity": "sha512-d4SJySNBXDaQp+DPrziv3xGS6w3d2Xt69FijJr86zMPBy23JEloMCEOUBBzuN7xCtjLCnmB9tI/z7SBCahHBOw==", "dev": true }, - "update-notifier": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-1.0.3.tgz", - "integrity": "sha1-j5LFFUgr1oMbfJMBPnD4dVLHz1o=", - "requires": { - "boxen": "^0.6.0", - "chalk": "^1.0.0", - "configstore": "^2.0.0", - "is-npm": "^1.0.0", - "latest-version": "^2.0.0", - "lazy-req": "^1.1.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^2.0.0" - } - }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", @@ -31214,11 +30811,6 @@ } } }, - "url-join": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", - "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" - }, "url-loader": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz", @@ -33317,14 +32909,6 @@ "string-width": "^1.0.2" } }, - "widest-line": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", - "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", - "requires": { - "string-width": "^1.0.1" - } - }, "window-size": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", @@ -33450,6 +33034,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -33483,14 +33068,6 @@ "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" }, - "xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "requires": { - "os-homedir": "^1.0.0" - } - }, "xhr": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.4.1.tgz", diff --git a/package.json b/package.json index 96b0829db..4536546f4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/e2e/beta/contract-test/contract.js b/test/e2e/beta/contract-test/contract.js index 8af008dce..51891ea21 100644 --- a/test/e2e/beta/contract-test/contract.js +++ b/test/e2e/beta/contract-test/contract.js @@ -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' }) }) } diff --git a/test/e2e/beta/contract-test/index.html b/test/e2e/beta/contract-test/index.html index 0d63fd940..f6e6f44c7 100644 --- a/test/e2e/beta/contract-test/index.html +++ b/test/e2e/beta/contract-test/index.html @@ -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> diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index 32f57b157..5582d4697 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -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) diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index ca1977c5a..3ad5c2d61 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -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) diff --git a/test/e2e/beta/run-all.sh b/test/e2e/beta/run-all.sh index 493e1360a..7da61e504 100755 --- a/test/e2e/beta/run-all.sh +++ b/test/e2e/beta/run-all.sh @@ -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' diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js index b6efae5b3..ac7600f09 100644 --- a/test/e2e/metamask.spec.js +++ b/test/e2e/metamask.spec.js @@ -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) diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index 052d89518..8cacd7f14 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -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') diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js index 426ffe23a..d6c3fad8a 100644 --- a/test/unit/app/controllers/detect-tokens-test.js +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -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 diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index e055500b1..9b2c846bd 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -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') + }) }) }) diff --git a/ui/app/actions.js b/ui/app/actions.js index 04ad344cf..5cc7dc2fa 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -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, diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js index 93e4ae7bf..24ff05353 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -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> ) diff --git a/ui/app/components/dropdowns/components/dropdown.js b/ui/app/components/dropdowns/components/dropdown.js index 0336dbb8b..149f063a7 100644 --- a/ui/app/components/dropdowns/components/dropdown.js +++ b/ui/app/components/dropdowns/components/dropdown.js @@ -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', diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index dcd6b4370..e5363ff56 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -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', diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index e1bf2210f..b170880b4 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -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()} diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 1a639d0b9..7513ba267 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -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) { diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index 545a2a940..b23876d01 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -76,7 +76,6 @@ } .network-name-item { - font-weight: 100; flex: 1; color: $dusty-gray; text-overflow: ellipsis; |