diff options
Diffstat (limited to 'ui')
103 files changed, 2513 insertions, 1751 deletions
diff --git a/ui/app/accounts/new-account/index.js b/ui/app/accounts/new-account/index.js index 207cf7760..795bd7ce6 100644 --- a/ui/app/accounts/new-account/index.js +++ b/ui/app/accounts/new-account/index.js @@ -24,7 +24,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) }, hideModal: () => dispatch(actions.hideModal()), - saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)), + setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), } } diff --git a/ui/app/actions.js b/ui/app/actions.js index 62875c629..649f740e9 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1,4 +1,5 @@ const abi = require('human-standard-token-abi') +const pify = require('pify') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const { getTokenAddressFromTokenObject } = require('./util') const ethUtil = require('ethereumjs-util') @@ -123,8 +124,8 @@ var actions = { SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', showPrivateKey: showPrivateKey, exportAccountComplete, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, + SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL', + setAccountLabel, // tx conf screen COMPLETED_TX: 'COMPLETED_TX', TRANSACTION_ERROR: 'TRANSACTION_ERROR', @@ -270,11 +271,14 @@ var actions = { SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', // Network - setNetworkEndpoints, updateNetworkEndpointType, UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE', retryTransaction, + SET_PENDING_TOKENS: 'SET_PENDING_TOKENS', + CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS', + setPendingTokens, + clearPendingTokens, } module.exports = actions @@ -316,6 +320,7 @@ function tryUnlockMetamask (password) { background.verifySeedPhrase(err => { if (err) { dispatch(actions.displayWarning(err.message)) + return reject(err) } resolve() @@ -329,6 +334,7 @@ function tryUnlockMetamask (password) { .catch(err => { dispatch(actions.unlockFailed(err.message)) dispatch(actions.hideLoadingIndication()) + return Promise.reject(err) }) } } @@ -523,31 +529,26 @@ function addNewKeyring (type, opts) { } function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - return new Promise((resolve, reject) => { - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - resolve(newState) - }) - }) + return async (dispatch) => { + let newState + dispatch(actions.showLoadingIndication('This may take a while, please be patient.')) + try { + log.debug(`background.importAccountWithStrategy`) + await pify(background.importAccountWithStrategy).call(background, strategy, args) + log.debug(`background.getState`) + newState = await pify(background.getState).call(background) + } catch (err) { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(err.message)) + throw err + } + dispatch(actions.hideLoadingIndication()) + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, }) + return newState } } @@ -1396,16 +1397,24 @@ function markAccountsFound () { function retryTransaction (txId) { log.debug(`background.retryTransaction`) + let newTxId + return (dispatch) => { - background.retryTransaction(txId, (err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - const { selectedAddressTxList } = newState - const { id: newTxId } = selectedAddressTxList[selectedAddressTxList.length - 1] - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.viewPendingTx(newTxId)) + return new Promise((resolve, reject) => { + background.retryTransaction(txId, (err, newState) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + + const { selectedAddressTxList } = newState + const { id } = selectedAddressTxList[selectedAddressTxList.length - 1] + newTxId = id + resolve(newState) + }) }) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => newTxId) } } @@ -1601,13 +1610,13 @@ function showPrivateKey (key) { } } -function saveAccountLabel (account, label) { +function setAccountLabel (account, label) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) + log.debug(`background.setAccountLabel`) return new Promise((resolve, reject) => { - background.saveAccountLabel(account, label, (err) => { + background.setAccountLabel(account, label, (err) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -1616,7 +1625,7 @@ function saveAccountLabel (account, label) { } dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, + type: actions.SET_ACCOUNT_LABEL, value: { account, label }, }) @@ -1926,26 +1935,28 @@ function setLocaleMessages (localeMessages) { } } -function setNetworkEndpoints (networkEndpointType) { - return dispatch => { - log.debug('background.setNetworkEndpoints') - return new Promise((resolve, reject) => { - background.setNetworkEndpoints(networkEndpointType, err => { - if (err) { - dispatch(actions.displayWarning(err.message)) - return reject(err) - } +function updateNetworkEndpointType (networkEndpointType) { + return { + type: actions.UPDATE_NETWORK_ENDPOINT_TYPE, + value: networkEndpointType, + } +} - dispatch(actions.updateNetworkEndpointType(networkEndpointType)) - resolve(networkEndpointType) - }) - }) +function setPendingTokens (pendingTokens) { + const { customToken = {}, selectedTokens = {} } = pendingTokens + const { address, symbol, decimals } = customToken + const tokens = address && symbol && decimals + ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } } + : selectedTokens + + return { + type: actions.SET_PENDING_TOKENS, + payload: tokens, } } -function updateNetworkEndpointType (networkEndpointType) { +function clearPendingTokens () { return { - type: actions.UPDATE_NETWORK_ENDPOINT_TYPE, - value: networkEndpointType, + type: actions.CLEAR_PENDING_TOKENS, } } diff --git a/ui/app/app.js b/ui/app/app.js index 5af63dc9c..0e8b907df 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -22,16 +22,15 @@ const Home = require('./components/pages/home') const Authenticated = require('./components/pages/authenticated') const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') -const UnlockPage = require('./components/pages/unlock') +const UnlockPage = require('./components/pages/unlock-page') const RestoreVaultPage = require('./components/pages/keychains/restore-vault') const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') +const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') const CreateAccountPage = require('./components/pages/create-account') const NoticeScreen = require('./components/pages/notice') -const Loading = require('./components/loading') -const NetworkIndicator = require('./components/network') -const Identicon = require('./components/identicon') +const Loading = require('./components/loading-screen') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const NetworkDropdown = require('./components/dropdowns/network-dropdown') const AccountMenu = require('./components/account-menu') @@ -39,6 +38,8 @@ const AccountMenu = require('./components/account-menu') // Global Modals const Modal = require('./components/modals/index').Modal +const AppHeader = require('./components/app-header') + // Routes const { DEFAULT_ROUTE, @@ -47,6 +48,7 @@ const { REVEAL_SEED_ROUTE, RESTORE_VAULT_ROUTE, ADD_TOKEN_ROUTE, + CONFIRM_ADD_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, SEND_ROUTE, CONFIRM_TRANSACTION_ROUTE, @@ -69,14 +71,15 @@ class App extends Component { return ( h(Switch, [ h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }), - h(Initialized, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }), - h(Initialized, { path: SETTINGS_ROUTE, component: Settings }), h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), - h(Initialized, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), - h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }), + h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), + h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), + h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), + h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), + h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), ]) @@ -119,8 +122,7 @@ class App extends Component { // global modal h(Modal, {}, []), - // app bar - this.renderAppBar(), + h(AppHeader), // sidebar this.renderSidebar(), @@ -196,110 +198,6 @@ class App extends Component { ]) } - renderAppBar () { - const { - isUnlocked, - network, - provider, - networkDropdownOpen, - showNetworkDropdown, - hideNetworkDropdown, - isInitialized, - welcomeScreenSeen, - isPopup, - betaUI, - } = this.props - - 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('.full-width', { - style: {}, - }, [ - - (isInitialized || welcomeScreenSeen || isPopup || !betaUI) && h('.app-header.flex-row.flex-space-between', { - className: classnames({ - 'app-header--initialized': !isOnboarding, - }), - }, [ - h('div.app-header-contents', {}, [ - h('div.left-menu-wrapper', { - onClick: () => props.history.push(DEFAULT_ROUTE), - }, [ - // mini logo - h('img.metafox-icon', { - height: 42, - width: 42, - src: '/images/metamask-fox.svg', - }), - - // metamask name - h('.flex-row', [ - h('h1', this.context.t('appName')), - h('div.beta-label', this.context.t('beta')), - ]), - - ]), - - betaUI && isInitialized && h('div.header__right-actions', [ - h('div.network-component-wrapper', { - style: {}, - }, [ - // Network Indicator - h(NetworkIndicator, { - network, - provider, - disabled: this.props.location.pathname === CONFIRM_TRANSACTION_ROUTE, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - return networkDropdownOpen === false - ? showNetworkDropdown() - : hideNetworkDropdown() - }, - }), - - ]), - - isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [ - h(Identicon, { - address: this.props.selectedAddress, - diameter: 32, - }), - ]), - ]), - ]), - ]), - - !isInitialized && !isPopup && betaUI && h('.alpha-warning__container', {}, [ - h('h2', { - className: classnames({ - 'alpha-warning': welcomeScreenSeen, - 'alpha-warning-welcome-screen': !welcomeScreenSeen, - }), - }, 'Please be aware that this version is still under development'), - ]), - - ]) - ) - } - toggleMetamaskActive () { if (!this.props.isUnlocked) { // currently inactive: redirect to password box diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js new file mode 100644 index 000000000..62b04562a --- /dev/null +++ b/ui/app/components/app-header/app-header.component.js @@ -0,0 +1,140 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { matchPath } from 'react-router-dom' + +const { + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, +} = require('../../../../app/scripts/lib/enums') +const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes') +const Identicon = require('../identicon') +const NetworkIndicator = require('../network') + +class AppHeader extends Component { + static propTypes = { + history: PropTypes.object, + location: PropTypes.object, + network: PropTypes.string, + provider: PropTypes.object, + networkDropdownOpen: PropTypes.bool, + showNetworkDropdown: PropTypes.func, + hideNetworkDropdown: PropTypes.func, + toggleAccountMenu: PropTypes.func, + selectedAddress: PropTypes.string, + isUnlocked: PropTypes.bool, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleNetworkIndicatorClick (event) { + event.preventDefault() + event.stopPropagation() + + const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props + + return networkDropdownOpen === false + ? showNetworkDropdown() + : hideNetworkDropdown() + } + + isConfirming () { + const { location } = this.props + + return Boolean(matchPath(location.pathname, { + path: CONFIRM_TRANSACTION_ROUTE, exact: false, + })) + } + + renderAccountMenu () { + const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props + + return isUnlocked && ( + <div + className={classnames('account-menu__icon', { + 'account-menu__icon--disabled': this.isConfirming(), + })} + onClick={() => this.isConfirming() || toggleAccountMenu()} + > + <Identicon + address={selectedAddress} + diameter={32} + /> + </div> + ) + } + + hideAppHeader () { + const { location } = this.props + + const isInitializing = Boolean(matchPath(location.pathname, { + path: INITIALIZE_ROUTE, exact: false, + })) + + if (isInitializing) { + return true + } + + if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { + return true + } + + if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) { + return true + } + } + + render () { + const { + network, + provider, + history, + location, + isUnlocked, + } = this.props + + if (this.hideAppHeader()) { + return null + } + + return ( + <div + className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}> + <div className="app-header__contents"> + <div + className="app-header__logo-container" + onClick={() => history.push(DEFAULT_ROUTE)} + > + <img + className="app-header__metafox" + src="/images/metamask-fox.svg" + height={42} + width={42} + /> + <div className="flex-row"> + <h1>{ this.context.t('appName') }</h1> + <div className="app-header__beta-label"> + { this.context.t('beta') } + </div> + </div> + </div> + <div className="app-header__account-menu-container"> + <div className="network-component-wrapper"> + <NetworkIndicator + network={network} + provider={provider} + onClick={event => this.handleNetworkIndicatorClick(event)} + disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE} + /> + </div> + { this.renderAccountMenu() } + </div> + </div> + </div> + ) + } +} + +export default AppHeader diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js new file mode 100644 index 000000000..30d3f8cc4 --- /dev/null +++ b/ui/app/components/app-header/app-header.container.js @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' + +import AppHeader from './app-header.component' +const actions = require('../../actions') + +const mapStateToProps = state => { + const { appState, metamask } = state + const { networkDropdownOpen } = appState + const { + network, + provider, + selectedAddress, + isUnlocked, + } = metamask + + return { + networkDropdownOpen, + network, + provider, + selectedAddress, + isUnlocked, + } +} + +const mapDispatchToProps = dispatch => { + return { + showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), + hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AppHeader) diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js new file mode 100644 index 000000000..daa31f621 --- /dev/null +++ b/ui/app/components/app-header/index.js @@ -0,0 +1,2 @@ +import AppHeader from './app-header.container' +module.exports = AppHeader diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js new file mode 100644 index 000000000..fe3bf363c --- /dev/null +++ b/ui/app/components/button/button.component.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +const SECONDARY = 'secondary' +const CLASSNAME_PRIMARY = 'btn-primary' +const CLASSNAME_PRIMARY_LARGE = 'btn-primary--lg' +const CLASSNAME_SECONDARY = 'btn-secondary' +const CLASSNAME_SECONDARY_LARGE = 'btn-secondary--lg' + +const getClassName = (type, large = false) => { + let output = type === SECONDARY ? CLASSNAME_SECONDARY : CLASSNAME_PRIMARY + + if (large) { + output += ` ${type === SECONDARY ? CLASSNAME_SECONDARY_LARGE : CLASSNAME_PRIMARY_LARGE}` + } + + return output +} + +class Button extends Component { + render () { + const { type, large, className, ...buttonProps } = this.props + + return ( + <button + className={classnames(getClassName(type, large), className)} + { ...buttonProps } + > + { this.props.children } + </button> + ) + } +} + +Button.propTypes = { + type: PropTypes.string, + large: PropTypes.bool, + className: PropTypes.string, + children: PropTypes.string, +} + +export default Button + diff --git a/ui/app/components/button/button.stories.js b/ui/app/components/button/button.stories.js new file mode 100644 index 000000000..d1e14e869 --- /dev/null +++ b/ui/app/components/button/button.stories.js @@ -0,0 +1,41 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import Button from './' +import { text } from '@storybook/addon-knobs/react' + +storiesOf('Button', module) + .add('primary', () => + <Button + onClick={action('clicked')} + type="primary" + > + {text('text', 'Click me')} + </Button> + ) + .add('secondary', () => ( + <Button + onClick={action('clicked')} + type="secondary" + > + {text('text', 'Click me')} + </Button> + )) + .add('large primary', () => ( + <Button + onClick={action('clicked')} + type="primary" + large + > + {text('text', 'Click me')} + </Button> + )) + .add('large secondary', () => ( + <Button + onClick={action('clicked')} + type="secondary" + large + > + {text('text', 'Click me')} + </Button> + )) diff --git a/ui/app/components/button/index.js b/ui/app/components/button/index.js new file mode 100644 index 000000000..33ae95ae2 --- /dev/null +++ b/ui/app/components/button/index.js @@ -0,0 +1,2 @@ +import Button from './button.component' +module.exports = Button diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index fda7c3e17..c6957d2aa 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('../actions') const CoinbaseForm = require('./coinbase-form') const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') +const Loading = require('./loading-screen') const AccountPanel = require('./account-panel') const RadioList = require('./custom-radio-list') const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util') diff --git a/ui/app/components/export-text-container/export-text-container.scss b/ui/app/components/export-text-container/index.scss index a42de8233..975d62f70 100644 --- a/ui/app/components/export-text-container/export-text-container.scss +++ b/ui/app/components/export-text-container/index.scss @@ -37,7 +37,7 @@ display: flex; justify-content: center; align-items: center; - font-size: 14px; + font-size: 12px; cursor: pointer; color: $curious-blue; diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss new file mode 100644 index 000000000..e69acff63 --- /dev/null +++ b/ui/app/components/index.scss @@ -0,0 +1,7 @@ +@import './export-text-container/index'; + +@import './info-box/index'; + +@import './pages/index'; + +@import './modals/index'; diff --git a/ui/app/components/info-box/index.js b/ui/app/components/info-box/index.js new file mode 100644 index 000000000..6110422ed --- /dev/null +++ b/ui/app/components/info-box/index.js @@ -0,0 +1,2 @@ +import InfoBox from './info-box.component' +module.exports = InfoBox diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/info-box/index.scss new file mode 100644 index 000000000..8b5626d79 --- /dev/null +++ b/ui/app/components/info-box/index.scss @@ -0,0 +1,24 @@ +.info-box { + border-radius: 4px; + background-color: $alabaster; + position: relative; + padding: 16px; + display: flex; + flex-flow: column; + color: $mid-gray; + + &__close::after { + content: '\00D7'; + font-size: 29px; + font-weight: 200; + color: $dusty-gray; + position: absolute; + right: 12px; + top: 0; + cursor: pointer; + } + + &__description { + font-size: .75rem; + } +} diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/info-box/info-box.component.js new file mode 100644 index 000000000..8688b8e8f --- /dev/null +++ b/ui/app/components/info-box/info-box.component.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class InfoBox extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + onClose: PropTypes.func, + title: PropTypes.string, + description: PropTypes.string, + } + + constructor (props) { + super(props) + + this.state = { + isShowing: true, + } + } + + handleClose () { + const { onClose } = this.props + + if (onClose) { + onClose() + } else { + this.setState({ isShowing: false }) + } + } + + render () { + const { title, description } = this.props + + return !this.state.isShowing + ? null + : ( + <div className="info-box"> + <div + className="info-box__close" + onClick={() => this.handleClose()} + /> + <div className="info-box__title">{ title }</div> + <div className="info-box__description">{ description }</div> + </div> + ) + } +} diff --git a/ui/app/components/loading-screen/index.js b/ui/app/components/loading-screen/index.js new file mode 100644 index 000000000..191d953f7 --- /dev/null +++ b/ui/app/components/loading-screen/index.js @@ -0,0 +1,2 @@ +const LoadingScreen = require('./loading-screen.component') +module.exports = LoadingScreen diff --git a/ui/app/components/loading-screen/loading-screen.component.js b/ui/app/components/loading-screen/loading-screen.component.js new file mode 100644 index 000000000..6b843cfee --- /dev/null +++ b/ui/app/components/loading-screen/loading-screen.component.js @@ -0,0 +1,31 @@ +const { Component } = require('react') +const h = require('react-hyperscript') +const PropTypes = require('prop-types') +const Spinner = require('../spinner') + +class LoadingScreen extends Component { + renderMessage () { + const { loadingMessage } = this.props + return loadingMessage && h('span', loadingMessage) + } + + render () { + return ( + h('.loading-overlay', [ + h('.loading-overlay__container', [ + h(Spinner, { + color: '#F7C06C', + }), + + this.renderMessage(), + ]), + ]) + ) + } +} + +LoadingScreen.propTypes = { + loadingMessage: PropTypes.string, +} + +module.exports = LoadingScreen diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js deleted file mode 100644 index b9afc550f..000000000 --- a/ui/app/components/loading.js +++ /dev/null @@ -1,34 +0,0 @@ -const { Component } = require('react') -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const classnames = require('classnames') - -class LoadingIndicator extends Component { - renderMessage () { - const { loadingMessage } = this.props - return loadingMessage && h('span', loadingMessage) - } - - render () { - return ( - h('.loading-overlay', { - className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }), - }, [ - h('.flex-center.flex-column', [ - h('img', { - src: 'images/loading.svg', - }), - - this.renderMessage(), - ]), - ]) - ) - } -} - -LoadingIndicator.propTypes = { - loadingMessage: PropTypes.string, - fullScreen: PropTypes.bool, -} - -module.exports = LoadingIndicator diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index d9885daf5..5607cf051 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -25,7 +25,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) }, hideModal: () => dispatch(actions.hideModal()), - saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)), + setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), } } @@ -49,7 +49,7 @@ AccountDetailsModal.prototype.render = function () { selectedIdentity, network, showExportPrivateKeyModal, - saveAccountLabel, + setAccountLabel, } = this.props const { name, address } = selectedIdentity @@ -57,7 +57,7 @@ AccountDetailsModal.prototype.render = function () { h(EditableLabel, { className: 'account-modal__name', defaultValue: name, - onSubmit: label => saveAccountLabel(address, label), + onSubmit: label => setAccountLabel(address, label), }), h(QrView, { diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js index c79645dbf..5681a3cad 100644 --- a/ui/app/components/modals/edit-account-name-modal.js +++ b/ui/app/components/modals/edit-account-name-modal.js @@ -18,8 +18,8 @@ function mapDispatchToProps (dispatch) { hideModal: () => { dispatch(actions.hideModal()) }, - saveAccountLabel: (account, label) => { - dispatch(actions.saveAccountLabel(account, label)) + setAccountLabel: (account, label) => { + dispatch(actions.setAccountLabel(account, label)) }, } } @@ -41,7 +41,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameMod EditAccountNameModal.prototype.render = function () { - const { hideModal, saveAccountLabel, identity } = this.props + const { hideModal, setAccountLabel, identity } = this.props return h('div', {}, [ h('div.flex-column.edit-account-name-modal-content', { @@ -69,7 +69,7 @@ EditAccountNameModal.prototype.render = function () { h('button.btn-clear.edit-account-name-modal-save-button.allcaps', { onClick: () => { if (this.state.inputText.length !== 0) { - saveAccountLabel(identity.address, this.state.inputText) + setAccountLabel(identity.address, this.state.inputText) hideModal() } }, diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss new file mode 100644 index 000000000..ec6207f7e --- /dev/null +++ b/ui/app/components/modals/index.scss @@ -0,0 +1 @@ +@import './transaction-confirmed/index'; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 43dcd20ae..841189277 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -20,6 +20,7 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const ConfirmResetAccount = require('./notification-modals/confirm-reset-account') +const TransactionConfirmed = require('./transaction-confirmed') const accountModalStyle = { mobileModalStyle: { @@ -265,6 +266,37 @@ const MODALS = { }, }, + TRANSACTION_CONFIRMED: { + disableBackdropClick: true, + contents: [ + h(TransactionConfirmed, {}, []), + ], + mobileModalStyle: { + width: '100%', + height: '100%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', + top: '0', + display: 'flex', + }, + laptopModalStyle: { + width: '344px', + transform: 'translate3d(-50%, 0, 0px)', + top: '15%', + border: '1px solid #CCCFD1', + borderRadius: '8px', + backgroundColor: '#FFFFFF', + boxShadow: '0 2px 22px 0 rgba(0,0,0,0.2)', + }, + contentStyle: { + borderRadius: '8px', + height: '100%', + }, + }, + DEFAULT: { contents: [], mobileModalStyle: {}, @@ -306,7 +338,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal) Modal.prototype.render = function () { const modal = MODALS[this.props.modalState.name || 'DEFAULT'] - const children = modal.contents + const { contents: children, disableBackdropClick = false } = modal const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle'] const contentStyle = modal.contentStyle || {} @@ -326,6 +358,7 @@ Modal.prototype.render = function () { modalStyle, contentStyle, backdropStyle: BACKDROPSTYLE, + closeOnClick: !disableBackdropClick, }, children, ) diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js index 0635b3f72..a66a3ed4a 100644 --- a/ui/app/components/modals/new-account-modal.js +++ b/ui/app/components/modals/new-account-modal.js @@ -95,7 +95,7 @@ const mapDispatchToProps = dispatch => { dispatch(actions.addNewAccount()) .then((newAccountAddress) => { if (newAccountName) { - dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) + dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) } dispatch(actions.hideModal()) }) diff --git a/ui/app/components/modals/transaction-confirmed/index.js b/ui/app/components/modals/transaction-confirmed/index.js new file mode 100644 index 000000000..c8db91388 --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/index.js @@ -0,0 +1,2 @@ +import TransactionConfirmed from './transaction-confirmed.container' +module.exports = TransactionConfirmed diff --git a/ui/app/components/modals/transaction-confirmed/index.scss b/ui/app/components/modals/transaction-confirmed/index.scss new file mode 100644 index 000000000..f8cd1f212 --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/index.scss @@ -0,0 +1,21 @@ +.transaction-confirmed { + display: flex; + flex-direction: column; + align-items: center; + padding: 32px; + + &__title { + font-size: 2rem; + padding: 16px 0; + } + + &__description { + text-align: center; + font-size: .875rem; + line-height: 1.5rem; + } + + @media screen and (max-width: 575px) { + justify-content: center; + } +} diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js new file mode 100644 index 000000000..8d3b288ae --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' + +class TransactionConfirmed extends Component { + render () { + const { t } = this.context + + return ( + <div className="page-container page-container--full-width page-container--full-height"> + <div className="page-container__content transaction-confirmed"> + <img src="images/check-icon.svg" /> + <div className="transaction-confirmed__title"> + { `${t('confirmed')}!` } + </div> + <div className="transaction-confirmed__description"> + { t('initialTransactionConfirmed') } + </div> + </div> + <div className="page-container__footer"> + <Button + type="primary" + className="page-container__footer-button" + onClick={() => { + this.props.hideModal() + this.props.onHide() + }} + > + { t('ok') } + </Button> + </div> + </div> + ) + } +} + +TransactionConfirmed.propTypes = { + hideModal: PropTypes.func.isRequired, + onHide: PropTypes.func.isRequired, +} + +TransactionConfirmed.contextTypes = { + t: PropTypes.func, +} + +export default TransactionConfirmed diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js new file mode 100644 index 000000000..63872f7f2 --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import TransactionConfirmed from './transaction-confirmed.component' + +const { hideModal } = require('../../../actions') + +const mapStateToProps = state => { + const { appState: { modal: { modalState: { props } } } } = state + const { onHide } = props + return { + onHide, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(TransactionConfirmed) diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js deleted file mode 100644 index 8d52571d0..000000000 --- a/ui/app/components/pages/add-token.js +++ /dev/null @@ -1,431 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const classnames = require('classnames') -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const R = require('ramda') -const Fuse = require('fuse.js') -const contractMap = require('eth-contract-metadata') -const TokenBalance = require('../../components/token-balance') -const Identicon = require('../../components/identicon') -const contractList = Object.entries(contractMap) - .map(([ _, tokenData]) => tokenData) - .filter(tokenData => Boolean(tokenData.erc20)) -const fuse = new Fuse(contractList, { - shouldSort: true, - threshold: 0.45, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: [ - { name: 'name', weight: 0.5 }, - { name: 'symbol', weight: 0.5 }, - ], -}) -const actions = require('../../actions') -const ethUtil = require('ethereumjs-util') -const { tokenInfoGetter } = require('../../token-util') -const { DEFAULT_ROUTE } = require('../../routes') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -AddTokenScreen.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) - - -function mapStateToProps (state) { - const { identities, tokens } = state.metamask - return { - identities, - tokens, - } -} - -function mapDispatchToProps (dispatch) { - return { - addTokens: tokens => dispatch(actions.addTokens(tokens)), - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - isShowingConfirmation: false, - isShowingInfoBox: true, - customAddress: '', - customSymbol: '', - customDecimals: '', - searchQuery: '', - selectedTokens: {}, - errors: {}, - autoFilled: false, - displayedTab: 'SEARCH', - } - this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) - this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this) - this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this) - this.onNext = this.onNext.bind(this) - Component.call(this) -} - -AddTokenScreen.prototype.componentWillMount = function () { - this.tokenInfoGetter = tokenInfoGetter() -} - -AddTokenScreen.prototype.toggleToken = function (address, token) { - const { selectedTokens = {}, errors } = this.state - const selectedTokensCopy = { ...selectedTokens } - - if (address in selectedTokensCopy) { - delete selectedTokensCopy[address] - } else { - selectedTokensCopy[address] = token - } - - this.setState({ - selectedTokens: selectedTokensCopy, - errors: { - ...errors, - tokenSelector: null, - }, - }) -} - -AddTokenScreen.prototype.onNext = function () { - const { isValid, errors } = this.validate() - - return !isValid - ? this.setState({ errors }) - : this.setState({ isShowingConfirmation: true }) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (e) { - const customAddress = e.target.value.trim() - this.setState({ customAddress }) - if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { - this.attemptToAutoFillTokenParams(customAddress) - } else { - this.setState({ - customSymbol: '', - customDecimals: 0, - }) - } -} - -AddTokenScreen.prototype.tokenSymbolDidChange = function (e) { - const customSymbol = e.target.value.trim() - this.setState({ customSymbol }) -} - -AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) { - const customDecimals = e.target.value.trim() - this.setState({ customDecimals }) -} - -AddTokenScreen.prototype.checkExistingAddresses = function (address) { - if (!address) return false - const tokensList = this.props.tokens - const matchesAddress = existingToken => { - return existingToken.address.toLowerCase() === address.toLowerCase() - } - - return R.any(matchesAddress)(tokensList) -} - -AddTokenScreen.prototype.validate = function () { - const errors = {} - const identitiesList = Object.keys(this.props.identities) - const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state - const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() - - if (customAddress) { - const validAddress = ethUtil.isValidAddress(customAddress) - if (!validAddress) { - errors.customAddress = this.context.t('invalidAddress') - } - - const validDecimals = customDecimals !== null - && customDecimals !== '' - && customDecimals >= 0 - && customDecimals < 36 - if (!validDecimals) { - errors.customDecimals = this.context.t('decimalsMustZerotoTen') - } - - const symbolLen = customSymbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - errors.customSymbol = this.context.t('symbolBetweenZeroTen') - } - - const ownAddress = identitiesList.includes(standardAddress) - if (ownAddress) { - errors.customAddress = this.context.t('personalAddressDetected') - } - - const tokenAlreadyAdded = this.checkExistingAddresses(customAddress) - if (tokenAlreadyAdded) { - errors.customAddress = this.context.t('tokenAlreadyAdded') - } - } else if ( - Object.entries(selectedTokens) - .reduce((isEmpty, [ symbol, isSelected ]) => ( - isEmpty && !isSelected - ), true) - ) { - errors.tokenSelector = this.context.t('mustSelectOne') - } - - return { - isValid: !Object.keys(errors).length, - errors, - } -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const { symbol, decimals } = await this.tokenInfoGetter(address) - if (symbol && decimals) { - this.setState({ - customSymbol: symbol, - customDecimals: decimals, - autoFilled: true, - }) - } -} - -AddTokenScreen.prototype.renderCustomForm = function () { - const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state - - return ( - h('div.add-token__add-custom-form', [ - h('div', { - className: classnames('add-token__add-custom-field', { - 'add-token__add-custom-field--error': errors.customAddress, - }), - }, [ - h('div.add-token__add-custom-label', this.context.t('tokenAddress')), - h('input.add-token__add-custom-input', { - type: 'text', - onChange: this.tokenAddressDidChange, - value: customAddress, - }), - h('div.add-token__add-custom-error-message', errors.customAddress), - ]), - h('div', { - className: classnames('add-token__add-custom-field', { - 'add-token__add-custom-field--error': errors.customSymbol, - }), - }, [ - h('div.add-token__add-custom-label', this.context.t('tokenSymbol')), - h('input.add-token__add-custom-input', { - type: 'text', - onChange: this.tokenSymbolDidChange, - value: customSymbol, - disabled: autoFilled, - }), - h('div.add-token__add-custom-error-message', errors.customSymbol), - ]), - h('div', { - className: classnames('add-token__add-custom-field', { - 'add-token__add-custom-field--error': errors.customDecimals, - }), - }, [ - h('div.add-token__add-custom-label', this.context.t('decimal')), - h('input.add-token__add-custom-input', { - type: 'number', - onChange: this.tokenDecimalsDidChange, - value: customDecimals, - disabled: autoFilled, - }), - h('div.add-token__add-custom-error-message', errors.customDecimals), - ]), - ]) - ) -} - -AddTokenScreen.prototype.renderTokenList = function () { - const { searchQuery = '', selectedTokens } = this.state - const fuseSearchResult = fuse.search(searchQuery) - const addressSearchResult = contractList.filter(token => { - return token.address.toLowerCase() === searchQuery.toLowerCase() - }) - const results = [...addressSearchResult, ...fuseSearchResult] - - return h('div', [ - results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')), - h('div.add-token__token-icons-container', Array(6).fill(undefined) - .map((_, i) => { - const { logo, symbol, name, address } = results[i] || {} - const tokenAlreadyAdded = this.checkExistingAddresses(address) - return Boolean(logo || symbol || name) && ( - h('div.add-token__token-wrapper', { - className: classnames({ - 'add-token__token-wrapper--selected': selectedTokens[address], - 'add-token__token-wrapper--disabled': tokenAlreadyAdded, - }), - onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), - }, [ - h('div.add-token__token-icon', { - style: { - backgroundImage: logo && `url(images/contract/${logo})`, - }, - }), - h('div.add-token__token-data', [ - h('div.add-token__token-symbol', symbol), - h('div.add-token__token-name', name), - ]), - // tokenAlreadyAdded && ( - // h('div.add-token__token-message', 'Already added') - // ), - ]) - ) - })), - ]) -} - -AddTokenScreen.prototype.renderConfirmation = function () { - const { - customAddress: address, - customSymbol: symbol, - customDecimals: decimals, - selectedTokens, - } = this.state - - const { addTokens, history } = this.props - - const customToken = { - address, - symbol, - decimals, - } - - const tokens = address && symbol && decimals - ? { ...selectedTokens, [address]: customToken } - : selectedTokens - - return ( - h('div.add-token', [ - h('div.add-token__wrapper', [ - h('div.add-token__content-container.add-token__confirmation-content', [ - h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')), - h('div.add-token__confirmation-token-list', - Object.entries(tokens) - .map(([ address, token ]) => ( - h('span.add-token__confirmation-token-list-item', [ - h(Identicon, { - className: 'add-token__confirmation-token-icon', - diameter: 75, - address, - }), - h(TokenBalance, { token }), - ]) - )) - ), - ]), - ]), - h('div.add-token__buttons', [ - h('button.btn-secondary--lg.add-token__cancel-button', { - onClick: () => this.setState({ isShowingConfirmation: false }), - }, this.context.t('back')), - h('button.btn-primary--lg', { - onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)), - }, this.context.t('addTokens')), - ]), - ]) - ) -} - -AddTokenScreen.prototype.displayTab = function (selectedTab) { - this.setState({ displayedTab: selectedTab }) -} - -AddTokenScreen.prototype.renderTabs = function () { - const { isShowingInfoBox, displayedTab, errors } = this.state - - return displayedTab === 'CUSTOM_TOKEN' - ? this.renderCustomForm() - : h('div', [ - h('div.add-token__wrapper', [ - h('div.add-token__content-container', [ - isShowingInfoBox && h('div.add-token__info-box', [ - h('div.add-token__info-box__close', { - onClick: () => this.setState({ isShowingInfoBox: false }), - }), - h('div.add-token__info-box__title', this.context.t('whatsThis')), - h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')), - h('a.add-token__info-box__copy--blue', { - href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens', - target: '_blank', - }, this.context.t('learnMore')), - ]), - h('div.add-token__input-container', [ - h('input.add-token__input', { - type: 'text', - placeholder: this.context.t('searchTokens'), - onChange: e => this.setState({ searchQuery: e.target.value }), - }), - h('div.add-token__search-input-error-message', errors.tokenSelector), - ]), - this.renderTokenList(), - ]), - ]), - ]) -} - -AddTokenScreen.prototype.render = function () { - const { - isShowingConfirmation, - displayedTab, - } = this.state - const { history } = this.props - - return h('div.add-token', [ - h('div.add-token__header', [ - h('div.add-token__header__cancel', { - onClick: () => history.push(DEFAULT_ROUTE), - }, [ - h('i.fa.fa-angle-left.fa-lg'), - h('span', this.context.t('cancel')), - ]), - h('div.add-token__header__title', this.context.t('addTokens')), - isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')), - !isShowingConfirmation && h('div.add-token__header__tabs', [ - - h('div.add-token__header__tabs__tab', { - className: classnames('add-token__header__tabs__tab', { - 'add-token__header__tabs__selected': displayedTab === 'SEARCH', - 'add-token__header__tabs__unselected': displayedTab !== 'SEARCH', - }), - onClick: () => this.displayTab('SEARCH'), - }, this.context.t('search')), - - h('div.add-token__header__tabs__tab', { - className: classnames('add-token__header__tabs__tab', { - 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN', - 'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN', - }), - onClick: () => this.displayTab('CUSTOM_TOKEN'), - }, this.context.t('customToken')), - - ]), - ]), - - isShowingConfirmation - ? this.renderConfirmation() - : this.renderTabs(), - - !isShowingConfirmation && h('div.add-token__buttons', [ - h('button.btn-secondary--lg.add-token__cancel-button', { - onClick: () => history.push(DEFAULT_ROUTE), - }, this.context.t('cancel')), - h('button.btn-primary--lg.add-token__confirm-button', { - onClick: this.onNext, - }, this.context.t('next')), - ]), - ]) -} diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js new file mode 100644 index 000000000..0677b4317 --- /dev/null +++ b/ui/app/components/pages/add-token/add-token.component.js @@ -0,0 +1,351 @@ +import React, { Component } from 'react' +import classnames from 'classnames' +import PropTypes from 'prop-types' +import ethUtil from 'ethereumjs-util' +import { checkExistingAddresses } from './util' +import { tokenInfoGetter } from '../../../token-util' +import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' +import Button from '../../button' +import TextField from '../../text-field' +import TokenList from './token-list' +import TokenSearch from './token-search' + +const emptyAddr = '0x0000000000000000000000000000000000000000' +const SEARCH_TAB = 'SEARCH' +const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' + +class AddToken extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + history: PropTypes.object, + setPendingTokens: PropTypes.func, + pendingTokens: PropTypes.object, + clearPendingTokens: PropTypes.func, + tokens: PropTypes.array, + identities: PropTypes.object, + } + + constructor (props) { + super(props) + + this.state = { + customAddress: '', + customSymbol: '', + customDecimals: 0, + searchResults: [], + selectedTokens: {}, + tokenSelectorError: null, + customAddressError: null, + customSymbolError: null, + customDecimalsError: null, + autoFilled: false, + displayedTab: SEARCH_TAB, + } + } + + componentDidMount () { + this.tokenInfoGetter = tokenInfoGetter() + const { pendingTokens = {} } = this.props + const pendingTokenKeys = Object.keys(pendingTokens) + + if (pendingTokenKeys.length > 0) { + let selectedTokens = {} + let customToken = {} + + pendingTokenKeys.forEach(tokenAddress => { + const token = pendingTokens[tokenAddress] + const { isCustom } = token + + if (isCustom) { + customToken = { ...token } + } else { + selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } + } + }) + + const { + address: customAddress = '', + symbol: customSymbol = '', + decimals: customDecimals = 0, + } = customToken + + const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB + this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) + } + } + + handleToggleToken (token) { + const { address } = token + const { selectedTokens = {} } = this.state + const selectedTokensCopy = { ...selectedTokens } + + if (address in selectedTokensCopy) { + delete selectedTokensCopy[address] + } else { + selectedTokensCopy[address] = token + } + + this.setState({ + selectedTokens: selectedTokensCopy, + tokenSelectorError: null, + }) + } + + hasError () { + const { + tokenSelectorError, + customAddressError, + customSymbolError, + customDecimalsError, + } = this.state + + return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError + } + + hasSelected () { + const { customAddress = '', selectedTokens = {} } = this.state + return customAddress || Object.keys(selectedTokens).length > 0 + } + + handleNext () { + if (this.hasError()) { + return + } + + if (!this.hasSelected()) { + this.setState({ tokenSelectorError: this.context.t('mustSelectOne') }) + return + } + + const { setPendingTokens, history } = this.props + const { + customAddress: address, + customSymbol: symbol, + customDecimals: decimals, + selectedTokens, + } = this.state + + const customToken = { + address, + symbol, + decimals, + } + + setPendingTokens({ customToken, selectedTokens }) + history.push(CONFIRM_ADD_TOKEN_ROUTE) + } + + async attemptToAutoFillTokenParams (address) { + const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address) + + const autoFilled = Boolean(symbol && decimals) + this.setState({ autoFilled }) + this.handleCustomSymbolChange(symbol || '') + this.handleCustomDecimalsChange(decimals) + } + + handleCustomAddressChange (value) { + const customAddress = value.trim() + this.setState({ + customAddress, + customAddressError: null, + tokenSelectorError: null, + autoFilled: false, + }) + + const isValidAddress = ethUtil.isValidAddress(customAddress) + const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() + + switch (true) { + case !isValidAddress: + this.setState({ + customAddressError: this.context.t('invalidAddress'), + customSymbol: '', + customDecimals: 0, + customSymbolError: null, + customDecimalsError: null, + }) + + break + case Boolean(this.props.identities[standardAddress]): + this.setState({ + customAddressError: this.context.t('personalAddressDetected'), + }) + + break + case checkExistingAddresses(customAddress, this.props.tokens): + this.setState({ + customAddressError: this.context.t('tokenAlreadyAdded'), + }) + + break + default: + if (customAddress !== emptyAddr) { + this.attemptToAutoFillTokenParams(customAddress) + } + } + } + + handleCustomSymbolChange (value) { + const customSymbol = value.trim() + const symbolLength = customSymbol.length + let customSymbolError = null + + if (symbolLength <= 0 || symbolLength >= 10) { + customSymbolError = this.context.t('symbolBetweenZeroTen') + } + + this.setState({ customSymbol, customSymbolError }) + } + + handleCustomDecimalsChange (value) { + const customDecimals = value.trim() + const validDecimals = customDecimals !== null && + customDecimals !== '' && + customDecimals >= 0 && + customDecimals < 36 + let customDecimalsError = null + + if (!validDecimals) { + customDecimalsError = this.context.t('decimalsMustZerotoTen') + } + + this.setState({ customDecimals, customDecimalsError }) + } + + renderCustomTokenForm () { + const { + customAddress, + customSymbol, + customDecimals, + customAddressError, + customSymbolError, + customDecimalsError, + autoFilled, + } = this.state + + return ( + <div className="add-token__custom-token-form"> + <TextField + id="custom-address" + label="Token Address" + type="text" + value={customAddress} + onChange={e => this.handleCustomAddressChange(e.target.value)} + error={customAddressError} + fullWidth + margin="normal" + /> + <TextField + id="custom-symbol" + label="Token Symbol" + type="text" + value={customSymbol} + onChange={e => this.handleCustomSymbolChange(e.target.value)} + error={customSymbolError} + fullWidth + margin="normal" + disabled={autoFilled} + /> + <TextField + id="custom-decimals" + label="Decimals of Precision" + type="number" + value={customDecimals} + onChange={e => this.handleCustomDecimalsChange(e.target.value)} + error={customDecimalsError} + fullWidth + margin="normal" + disabled={autoFilled} + /> + </div> + ) + } + + renderSearchToken () { + const { tokenSelectorError, selectedTokens, searchResults } = this.state + + return ( + <div className="add-token__search-token"> + <TokenSearch + onSearch={({ results = [] }) => this.setState({ searchResults: results })} + error={tokenSelectorError} + /> + <div className="add-token__token-list"> + <TokenList + results={searchResults} + selectedTokens={selectedTokens} + onToggleToken={token => this.handleToggleToken(token)} + /> + </div> + </div> + ) + } + + render () { + const { displayedTab } = this.state + const { history, clearPendingTokens } = this.props + + return ( + <div className="page-container"> + <div className="page-container__header page-container__header--no-padding-bottom"> + <div className="page-container__title"> + { this.context.t('addTokens') } + </div> + <div className="page-container__tabs"> + <div + className={classnames('page-container__tab', { + 'page-container__tab--selected': displayedTab === SEARCH_TAB, + })} + onClick={() => this.setState({ displayedTab: SEARCH_TAB })} + > + { this.context.t('search') } + </div> + <div + className={classnames('page-container__tab', { + 'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB, + })} + onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })} + > + { this.context.t('customToken') } + </div> + </div> + </div> + <div className="page-container__content"> + { + displayedTab === CUSTOM_TOKEN_TAB + ? this.renderCustomTokenForm() + : this.renderSearchToken() + } + </div> + <div className="page-container__footer"> + <Button + type="secondary" + large + className="page-container__footer-button" + onClick={() => { + clearPendingTokens() + history.push(DEFAULT_ROUTE) + }} + > + { this.context.t('cancel') } + </Button> + <Button + type="primary" + large + className="page-container__footer-button" + onClick={() => this.handleNext()} + disabled={this.hasError() || !this.hasSelected()} + > + { this.context.t('next') } + </Button> + </div> + </div> + ) + } +} + +export default AddToken diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js new file mode 100644 index 000000000..87671b156 --- /dev/null +++ b/ui/app/components/pages/add-token/add-token.container.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import AddToken from './add-token.component' + +const { setPendingTokens, clearPendingTokens } = require('../../../actions') + +const mapStateToProps = ({ metamask }) => { + const { identities, tokens, pendingTokens } = metamask + return { + identities, + tokens, + pendingTokens, + } +} + +const mapDispatchToProps = dispatch => { + return { + setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), + clearPendingTokens: () => dispatch(clearPendingTokens()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddToken) diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/components/pages/add-token/index.js new file mode 100644 index 000000000..3666cae82 --- /dev/null +++ b/ui/app/components/pages/add-token/index.js @@ -0,0 +1,2 @@ +import AddToken from './add-token.container' +module.exports = AddToken diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss new file mode 100644 index 000000000..39e86b97b --- /dev/null +++ b/ui/app/components/pages/add-token/index.scss @@ -0,0 +1,25 @@ +@import './token-list/index'; + +.add-token { + &__custom-token-form { + padding: 8px 16px 16px; + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + } + + &__search-token { + padding: 16px; + } + + &__token-list { + margin-top: 16px; + } +} diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/components/pages/add-token/token-list/index.js new file mode 100644 index 000000000..21dd5ac72 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/index.js @@ -0,0 +1,2 @@ +import TokenList from './token-list.container' +module.exports = TokenList diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss new file mode 100644 index 000000000..e32739d59 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/index.scss @@ -0,0 +1,65 @@ +@import './token-list-placeholder/index'; + +.token-list { + &__title { + font-size: .75rem; + } + + &__tokens-container { + display: flex; + flex-direction: column; + } + + &__token { + transition: 200ms ease-in-out; + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 8px; + margin-top: 8px; + box-sizing: border-box; + border-radius: 10px; + cursor: pointer; + border: 2px solid transparent; + position: relative; + + &:hover { + border: 2px solid rgba($malibu-blue, .5); + } + + &--selected { + border: 2px solid $malibu-blue !important; + } + + &--disabled { + opacity: .4; + pointer-events: none; + } + } + + &__token-icon { + width: 48px; + height: 48px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + border-radius: 50%; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba($black, .24); + margin-right: 12px; + flex: 0 0 auto; + } + + &__token-data { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + } + + &__token-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js new file mode 100644 index 000000000..b82f45e93 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js @@ -0,0 +1,2 @@ +import TokenListPlaceholder from './token-list-placeholder.component' +module.exports = TokenListPlaceholder diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss new file mode 100644 index 000000000..cc495dfb0 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss @@ -0,0 +1,23 @@ +.token-list-placeholder { + display: flex; + align-items: center; + padding-top: 36px; + flex-direction: column; + line-height: 22px; + opacity: .5; + + &__text { + color: $silver-chalice; + width: 50%; + text-align: center; + margin-top: 8px; + + @media screen and (max-width: 575px) { + width: 60%; + } + } + + &__link { + color: $curious-blue; + } +} diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js new file mode 100644 index 000000000..abd599b26 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class TokenListPlaceholder extends Component { + static contextTypes = { + t: PropTypes.func, + } + + render () { + return ( + <div className="token-list-placeholder"> + <img src="images/tokensearch.svg" /> + <div className="token-list-placeholder__text"> + { this.context.t('addAcquiredTokens') } + </div> + <a + className="token-list-placeholder__link" + href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens" + target="_blank" + rel="noopener noreferrer" + > + { this.context.t('learnMore') } + </a> + </div> + ) + } +} diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/components/pages/add-token/token-list/token-list.component.js new file mode 100644 index 000000000..724a68d6e --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/token-list.component.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { checkExistingAddresses } from '../util' +import TokenListPlaceholder from './token-list-placeholder' + +export default class InfoBox extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + tokens: PropTypes.array, + results: PropTypes.array, + selectedTokens: PropTypes.object, + onToggleToken: PropTypes.func, + } + + render () { + const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props + + return results.length === 0 + ? <TokenListPlaceholder /> + : ( + <div className="token-list"> + <div className="token-list__title"> + { this.context.t('searchResults') } + </div> + <div className="token-list__tokens-container"> + { + Array(6).fill(undefined) + .map((_, i) => { + const { logo, symbol, name, address } = results[i] || {} + const tokenAlreadyAdded = checkExistingAddresses(address, tokens) + + return Boolean(logo || symbol || name) && ( + <div + className={classnames('token-list__token', { + 'token-list__token--selected': selectedTokens[address], + 'token-list__token--disabled': tokenAlreadyAdded, + })} + onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])} + key={i} + > + <div + className="token-list__token-icon" + style={{ backgroundImage: logo && `url(images/contract/${logo})` }}> + </div> + <div className="token-list__token-data"> + <span className="token-list__token-name">{ `${name} (${symbol})` }</span> + </div> + </div> + ) + }) + } + </div> + </div> + ) + } +} diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/components/pages/add-token/token-list/token-list.container.js new file mode 100644 index 000000000..cd7b07a37 --- /dev/null +++ b/ui/app/components/pages/add-token/token-list/token-list.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import TokenList from './token-list.component' + +const mapStateToProps = ({ metamask }) => { + const { tokens } = metamask + return { + tokens, + } +} + +export default connect(mapStateToProps)(TokenList) diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/components/pages/add-token/token-search/index.js new file mode 100644 index 000000000..acaa6b084 --- /dev/null +++ b/ui/app/components/pages/add-token/token-search/index.js @@ -0,0 +1,2 @@ +import TokenSearch from './token-search.component' +module.exports = TokenSearch diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js new file mode 100644 index 000000000..036b2db1e --- /dev/null +++ b/ui/app/components/pages/add-token/token-search/token-search.component.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import contractMap from 'eth-contract-metadata' +import Fuse from 'fuse.js' +import InputAdornment from '@material-ui/core/InputAdornment' +import TextField from '../../../text-field' + +const contractList = Object.entries(contractMap) + .map(([ _, tokenData]) => tokenData) + .filter(tokenData => Boolean(tokenData.erc20)) + +const fuse = new Fuse(contractList, { + shouldSort: true, + threshold: 0.45, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'symbol', weight: 0.5 }, + ], +}) + +export default class TokenSearch extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + error: null, + } + + static propTypes = { + onSearch: PropTypes.func, + error: PropTypes.string, + } + + constructor (props) { + super(props) + + this.state = { + searchQuery: '', + } + } + + handleSearch (searchQuery) { + this.setState({ searchQuery }) + const fuseSearchResult = fuse.search(searchQuery) + const addressSearchResult = contractList.filter(token => { + return token.address.toLowerCase() === searchQuery.toLowerCase() + }) + const results = [...addressSearchResult, ...fuseSearchResult] + this.props.onSearch({ searchQuery, results }) + } + + renderAdornment () { + return ( + <InputAdornment + position="start" + style={{ marginRight: '12px' }} + > + <img src="images/search.svg" /> + </InputAdornment> + ) + } + + render () { + const { error } = this.props + const { searchQuery } = this.state + + return ( + <TextField + id="search-tokens" + placeholder={this.context.t('searchTokens')} + type="text" + value={searchQuery} + onChange={e => this.handleSearch(e.target.value)} + error={error} + fullWidth + startAdornment={this.renderAdornment()} + /> + ) + } +} diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/components/pages/add-token/util.js new file mode 100644 index 000000000..579c56cc0 --- /dev/null +++ b/ui/app/components/pages/add-token/util.js @@ -0,0 +1,13 @@ +import R from 'ramda' + +export function checkExistingAddresses (address, tokenList = []) { + if (!address) { + return false + } + + const matchesAddress = existingToken => { + return existingToken.address.toLowerCase() === address.toLowerCase() + } + + return R.any(matchesAddress)(tokenList) +} diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js new file mode 100644 index 000000000..9db9efc37 --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' +import Button from '../../button' +import Identicon from '../../../components/identicon' +import TokenBalance from './token-balance' + +export default class ConfirmAddToken extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + history: PropTypes.object, + clearPendingTokens: PropTypes.func, + addTokens: PropTypes.func, + pendingTokens: PropTypes.object, + } + + componentDidMount () { + const { pendingTokens = {}, history } = this.props + + if (Object.keys(pendingTokens).length === 0) { + history.push(DEFAULT_ROUTE) + } + } + + getTokenName (name, symbol) { + return typeof name === 'undefined' + ? symbol + : `${name} (${symbol})` + } + + render () { + const { history, addTokens, clearPendingTokens, pendingTokens } = this.props + + return ( + <div className="page-container"> + <div className="page-container__header"> + <div className="page-container__title"> + { this.context.t('addTokens') } + </div> + <div className="page-container__subtitle"> + { this.context.t('likeToAddTokens') } + </div> + </div> + <div className="page-container__content"> + <div className="confirm-add-token"> + <div className="confirm-add-token__header"> + <div className="confirm-add-token__token"> + { this.context.t('token') } + </div> + <div className="confirm-add-token__balance"> + { this.context.t('balance') } + </div> + </div> + <div className="confirm-add-token__token-list"> + { + Object.entries(pendingTokens) + .map(([ address, token ]) => { + const { name, symbol } = token + + return ( + <div + className="confirm-add-token__token-list-item" + key={address} + > + <div className="confirm-add-token__token confirm-add-token__data"> + <Identicon + className="confirm-add-token__token-icon" + diameter={48} + address={address} + /> + <div className="confirm-add-token__name"> + { this.getTokenName(name, symbol) } + </div> + </div> + <div className="confirm-add-token__balance"> + <TokenBalance token={token} /> + </div> + </div> + ) + }) + } + </div> + </div> + </div> + <div className="page-container__footer"> + <Button + type="secondary" + large + className="page-container__footer-button" + onClick={() => history.push(ADD_TOKEN_ROUTE)} + > + { this.context.t('back') } + </Button> + <Button + type="primary" + large + className="page-container__footer-button" + onClick={() => { + addTokens(pendingTokens) + .then(() => { + clearPendingTokens() + history.push(DEFAULT_ROUTE) + }) + }} + > + { this.context.t('addTokens') } + </Button> + </div> + </div> + ) + } +} diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js new file mode 100644 index 000000000..0190024d9 --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import ConfirmAddToken from './confirm-add-token.component' + +const { addTokens, clearPendingTokens } = require('../../../actions') + +const mapStateToProps = ({ metamask }) => { + const { pendingTokens } = metamask + return { + pendingTokens, + } +} + +const mapDispatchToProps = dispatch => { + return { + addTokens: tokens => dispatch(addTokens(tokens)), + clearPendingTokens: () => dispatch(clearPendingTokens()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken) diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/components/pages/confirm-add-token/index.js new file mode 100644 index 000000000..b7decabec --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/index.js @@ -0,0 +1,2 @@ +import ConfirmAddToken from './confirm-add-token.container' +module.exports = ConfirmAddToken diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/components/pages/confirm-add-token/index.scss new file mode 100644 index 000000000..66146cf78 --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/index.scss @@ -0,0 +1,69 @@ +.confirm-add-token { + padding: 16px; + + &__header { + font-size: .75rem; + display: flex; + } + + &__token { + flex: 1; + min-width: 0; + } + + &__balance { + flex: 0 0 30%; + min-width: 0; + } + + &__token-list { + display: flex; + flex-flow: column nowrap; + + .token-balance { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + + &__amount { + color: $scorpion; + font-size: 43px; + line-height: 43px; + margin-right: 8px; + } + + &__symbol { + color: $scorpion; + font-size: 16px; + font-weight: 400; + line-height: 24px; + } + } + } + + &__token-list-item { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: 8px; + box-sizing: border-box; + } + + &__data { + display: flex; + align-items: center; + padding: 8px; + } + + &__name { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__token-icon { + margin-right: 12px; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js new file mode 100644 index 000000000..6fb5c8223 --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/token-balance/index.js @@ -0,0 +1,2 @@ +import TokenBalance from './token-balance.container' +module.exports = TokenBalance diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js new file mode 100644 index 000000000..976788d4c --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class TokenBalance extends Component { + static propTypes = { + string: PropTypes.string, + symbol: PropTypes.string, + error: PropTypes.string, + } + + render () { + return ( + <div className="hide-text-overflow">{ this.props.string }</div> + ) + } +} diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js new file mode 100644 index 000000000..bc1289ce1 --- /dev/null +++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withTokenTracker from '../../../../helpers/with-token-tracker' +import TokenBalance from './token-balance.component' +import selectors from '../../../../selectors' + +const mapStateToProps = state => { + return { + userAddress: selectors.getSelectedAddress(state), + } +} + +export default compose( + connect(mapStateToProps), + withTokenTracker +)(TokenBalance) diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 946907a47..0a3314b2a 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -105,6 +105,8 @@ class JsonImportSubview extends Component { } this.props.importNewJsonAccount([ fileContents, password ]) + // JS runtime requires caught rejections but failures are handled by Redux + .catch() } } diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index c77612ea4..df7ac910a 100644 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -91,5 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { const { importNewAccount, history } = this.props importNewAccount('Private Key', [ privateKey ]) + // JS runtime requires caught rejections but failures are handled by Redux + .catch() .then(() => history.push(DEFAULT_ROUTE)) } diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index 0962477d8..475261253 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -75,7 +75,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) }, hideModal: () => dispatch(actions.hideModal()), - saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)), + setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), }) module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage) diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js index 40fa584be..03a5ee72d 100644 --- a/ui/app/components/pages/create-account/new-account.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -87,7 +87,7 @@ const mapDispatchToProps = dispatch => { return dispatch(actions.addNewAccount()) .then(newAccountAddress => { if (newAccountName) { - dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) + dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) } }) }, diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss new file mode 100644 index 000000000..b15c59863 --- /dev/null +++ b/ui/app/components/pages/index.scss @@ -0,0 +1,5 @@ +@import './unlock-page/index'; + +@import './add-token/index'; + +@import './confirm-add-token/index'; diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js index bdefe56f8..f58ac7ddf 100644 --- a/ui/app/components/pages/settings/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -12,7 +12,6 @@ const SimpleDropdown = require('../../dropdowns/simple-dropdown') const ToggleButton = require('react-toggle-button') const { REVEAL_SEED_ROUTE } = require('../../../routes') const locales = require('../../../../../app/_locales/index.json') -const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/controllers/network/enums') const getInfuraCurrencyOptions = () => { const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { @@ -349,7 +348,6 @@ const mapDispatchToProps = dispatch => { updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)), setFeatureFlagToBeta: () => { return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) - .then(() => dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))) }, showResetAccountConfirmationModal: () => { return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' })) diff --git a/ui/app/components/pages/unlock-page/index.js b/ui/app/components/pages/unlock-page/index.js new file mode 100644 index 000000000..be80cde4f --- /dev/null +++ b/ui/app/components/pages/unlock-page/index.js @@ -0,0 +1,2 @@ +import UnlockPage from './unlock-page.container' +module.exports = UnlockPage diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/components/pages/unlock-page/index.scss new file mode 100644 index 000000000..3d44bd037 --- /dev/null +++ b/ui/app/components/pages/unlock-page/index.scss @@ -0,0 +1,51 @@ +.unlock-page { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + width: 357px; + padding: 30px; + font-weight: 400; + color: $silver-chalice; + + &__container { + background: $white; + display: flex; + align-self: stretch; + justify-content: center; + flex: 1 0 auto; + } + + &__mascot-container { + margin-top: 24px; + } + + &__title { + margin-top: 5px; + font-size: 2rem; + font-weight: 800; + color: $tundora; + } + + &__form { + width: 100%; + margin: 56px 0 8px; + } + + &__links { + margin-top: 25px; + width: 100%; + } + + &__link { + cursor: pointer; + + &--import { + color: $ecstasy; + } + + &--use-classic { + margin-top: 10px; + } + } +} diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js new file mode 100644 index 000000000..8bc3897da --- /dev/null +++ b/ui/app/components/pages/unlock-page/unlock-page.component.js @@ -0,0 +1,179 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '@material-ui/core/Button' +import TextField from '../../text-field' + +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter +const Mascot = require('../../mascot') +const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes') + +class UnlockPage extends Component { + static contextTypes = { + t: PropTypes.func, + } + + constructor (props) { + super(props) + + this.state = { + password: '', + error: null, + } + + this.animationEventEmitter = new EventEmitter() + } + + componentWillMount () { + const { isUnlocked, history } = this.props + + if (isUnlocked) { + history.push(DEFAULT_ROUTE) + } + } + + async handleSubmit (event) { + event.preventDefault() + event.stopPropagation() + + const { password } = this.state + const { tryUnlockMetamask, history } = this.props + + if (password === '') { + return + } + + this.setState({ error: null }) + + try { + await tryUnlockMetamask(password) + } catch ({ message }) { + this.setState({ error: message }) + return + } + + history.push(DEFAULT_ROUTE) + } + + handleInputChange ({ target }) { + this.setState({ password: target.value, error: null }) + + // tell mascot to look at page action + const element = target + const boundingRect = element.getBoundingClientRect() + const coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) + } + + renderSubmitButton () { + const style = { + backgroundColor: '#f7861c', + color: 'white', + marginTop: '20px', + height: '60px', + fontWeight: '400', + boxShadow: 'none', + borderRadius: '4px', + } + + return ( + <Button + type="submit" + style={style} + disabled={!this.state.password} + fullWidth + variant="raised" + size="large" + onClick={event => this.handleSubmit(event)} + disableRipple + > + { this.context.t('login') } + </Button> + ) + } + + render () { + const { error } = this.state + + return ( + <div className="unlock-page__container"> + <div className="unlock-page"> + <div className="unlock-page__mascot-container"> + <Mascot + animationEventEmitter={this.animationEventEmitter} + width="120" + height="120" + /> + </div> + <h1 className="unlock-page__title"> + { this.context.t('welcomeBack') } + </h1> + <div>{ this.context.t('unlockMessage') }</div> + <form + className="unlock-page__form" + onSubmit={event => this.handleSubmit(event)} + > + <TextField + id="password" + label="Password" + type="password" + value={this.state.password} + onChange={event => this.handleInputChange(event)} + error={error} + autoFocus + autoComplete="current-password" + material + fullWidth + /> + </form> + { this.renderSubmitButton() } + <div className="unlock-page__links"> + <div + className="unlock-page__link" + onClick={() => { + this.props.markPasswordForgotten() + this.props.history.push(RESTORE_VAULT_ROUTE) + + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser() + } + }} + > + { this.context.t('restoreFromSeed') } + </div> + <div + className="unlock-page__link unlock-page__link--import" + onClick={() => { + this.props.markPasswordForgotten() + this.props.history.push(RESTORE_VAULT_ROUTE) + + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser() + } + }} + > + { this.context.t('importUsingSeed') } + </div> + </div> + </div> + </div> + ) + } +} + +UnlockPage.propTypes = { + forgotPassword: PropTypes.func, + tryUnlockMetamask: PropTypes.func, + markPasswordForgotten: PropTypes.func, + history: PropTypes.object, + isUnlocked: PropTypes.bool, + t: PropTypes.func, + useOldInterface: PropTypes.func, +} + +export default UnlockPage diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js new file mode 100644 index 000000000..18fed9b2e --- /dev/null +++ b/ui/app/components/pages/unlock-page/unlock-page.container.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' + +const { + tryUnlockMetamask, + forgotPassword, + markPasswordForgotten, +} = require('../../../actions') + +import UnlockPage from './unlock-page.component' + +const mapStateToProps = state => { + const { metamask: { isUnlocked } } = state + return { + isUnlocked, + } +} + +const mapDispatchToProps = dispatch => { + return { + forgotPassword: () => dispatch(forgotPassword()), + tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)), + markPasswordForgotten: () => dispatch(markPasswordForgotten()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(UnlockPage) diff --git a/ui/app/components/pages/unlock.js b/ui/app/components/pages/unlock.js deleted file mode 100644 index 30144b978..000000000 --- a/ui/app/components/pages/unlock.js +++ /dev/null @@ -1,194 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const connect = require('../../metamask-connect') -const h = require('react-hyperscript') -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const { - tryUnlockMetamask, - forgotPassword, - markPasswordForgotten, - setNetworkEndpoints, - setFeatureFlag, -} = require('../../actions') -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') -const { getEnvironmentType } = require('../../../../app/scripts/lib/util') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter -const Mascot = require('../mascot') -const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/controllers/network/enums') -const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../routes') - -class UnlockScreen extends Component { - constructor (props) { - super(props) - - this.state = { - error: null, - } - - this.animationEventEmitter = new EventEmitter() - } - - componentWillMount () { - const { isUnlocked, history } = this.props - - if (isUnlocked) { - history.push(DEFAULT_ROUTE) - } - } - - componentDidMount () { - const passwordBox = document.getElementById('password-box') - - if (passwordBox) { - passwordBox.focus() - } - } - - tryUnlockMetamask (password) { - const { tryUnlockMetamask, history } = this.props - tryUnlockMetamask(password) - .then(() => history.push(DEFAULT_ROUTE)) - .catch(({ message }) => this.setState({ error: message })) - } - - onSubmit (event) { - const input = document.getElementById('password-box') - const password = input.value - this.tryUnlockMetamask(password) - } - - onKeyPress (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } - } - - submitPassword (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.tryUnlockMetamask(password) - } - - inputChanged (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) - } - - render () { - const { error } = this.state - return ( - h('.unlock-screen', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, this.props.t('appName')), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - background: 'white', - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: error ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, error), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, this.props.t('login')), - - h('p.pointer', { - onClick: () => { - this.props.markPasswordForgotten() - this.props.history.push(RESTORE_VAULT_ROUTE) - - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }, - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, this.props.t('restoreFromSeed')), - - h('p.pointer', { - onClick: () => { - this.props.useOldInterface() - .then(() => this.props.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)) - }, - style: { - fontSize: '0.8em', - color: '#aeaeae', - textDecoration: 'underline', - marginTop: '32px', - }, - }, this.props.t('classicInterface')), - ]) - ) - } -} - -UnlockScreen.propTypes = { - forgotPassword: PropTypes.func, - tryUnlockMetamask: PropTypes.func, - markPasswordForgotten: PropTypes.func, - history: PropTypes.object, - isUnlocked: PropTypes.bool, - t: PropTypes.func, - useOldInterface: PropTypes.func, - setNetworkEndpoints: PropTypes.func, -} - -const mapStateToProps = state => { - const { metamask: { isUnlocked } } = state - return { - isUnlocked, - } -} - -const mapDispatchToProps = dispatch => { - return { - forgotPassword: () => dispatch(forgotPassword()), - tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)), - markPasswordForgotten: () => dispatch(markPasswordForgotten()), - useOldInterface: () => dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')), - setNetworkEndpoints: type => dispatch(setNetworkEndpoints(type)), - } -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(UnlockScreen) diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 16dbd273b..5ad35c269 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -28,6 +28,10 @@ const currencies = require('currency-formatter/currencies') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') +const { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, +} = require('../../../../app/scripts/lib/enums') ConfirmSendEther.contextTypes = { t: PropTypes.func, @@ -287,12 +291,50 @@ ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, curren : value } -ConfirmSendEther.prototype.editTransaction = function (txMeta) { +ConfirmSendEther.prototype.editTransaction = function () { const { editTransaction, history } = this.props + const txMeta = this.gatherTxMeta() editTransaction(txMeta) history.push(SEND_ROUTE) } +ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) { + const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP + + if (isTxReprice && isFullScreen) { + return null + } + + return ( + h('.page-container__header-row', [ + h('span.page-container__back-button', { + onClick: () => this.editTransaction(), + style: { + visibility: isTxReprice ? 'hidden' : 'initial', + }, + }, 'Edit'), + !isFullScreen && h(NetworkDisplay), + ]) + ) +} + +ConfirmSendEther.prototype.renderHeader = function (isTxReprice) { + const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') + const subtitle = isTxReprice + ? this.context.t('speedUpSubtitle') + : this.context.t('pleaseReviewTransaction') + + return ( + h('.page-container__header', [ + this.renderHeaderRow(isTxReprice), + h('.page-container__title', title), + h('.page-container__subtitle', subtitle), + ]) + ) +} + ConfirmSendEther.prototype.render = function () { const { currentCurrency, @@ -308,6 +350,7 @@ ConfirmSendEther.prototype.render = function () { }, } = this.props const txMeta = this.gatherTxMeta() + const isTxReprice = Boolean(txMeta.lastGasPrice) const txParams = txMeta.txParams || {} const { @@ -326,11 +369,6 @@ ConfirmSendEther.prototype.render = function () { totalInETH, } = this.getData() - const title = txMeta.lastGasPrice ? 'Reprice Transaction' : 'Confirm' - const subtitle = txMeta.lastGasPrice - ? 'Increase your gas fee to attempt to overwrite and speed up your transaction' - : 'Please review your transaction.' - const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency) const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) @@ -350,19 +388,7 @@ ConfirmSendEther.prototype.render = function () { return ( // Main Send token Card h('.page-container', [ - h('.page-container__header', [ - h('.page-container__header-row', [ - h('span.page-container__back-button', { - onClick: () => this.editTransaction(txMeta), - style: { - visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden', - }, - }, 'Edit'), - window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay), - ]), - h('.page-container__title', title), - h('.page-container__subtitle', subtitle), - ]), + this.renderHeader(isTxReprice), h('.page-container__content', [ h(SenderToRecipient, { senderName: fromName, diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 656093b3d..ddaa13d22 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -12,6 +12,7 @@ const actions = require('../../actions') const clone = require('clone') const Identicon = require('../identicon') const GasFeeDisplay = require('../send/gas-fee-display-v2.js') +const NetworkDisplay = require('../network-display') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const { @@ -39,6 +40,11 @@ const { } = require('../../selectors') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') +const { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, +} = require('../../../../app/scripts/lib/enums') + ConfirmSendToken.contextTypes = { t: PropTypes.func, } @@ -430,6 +436,43 @@ ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, curren : value } +ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) { + const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP + + if (isTxReprice && isFullScreen) { + return null + } + + return ( + h('.page-container__header-row', [ + h('span.page-container__back-button', { + onClick: () => this.editTransaction(), + style: { + visibility: isTxReprice ? 'hidden' : 'initial', + }, + }, 'Edit'), + !isFullScreen && h(NetworkDisplay), + ]) + ) +} + +ConfirmSendToken.prototype.renderHeader = function (isTxReprice) { + const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') + const subtitle = isTxReprice + ? this.context.t('speedUpSubtitle') + : this.context.t('pleaseReviewTransaction') + + return ( + h('.page-container__header', [ + this.renderHeaderRow(isTxReprice), + h('.page-container__title', title), + h('.page-container__subtitle', subtitle), + ]) + ) +} + ConfirmSendToken.prototype.render = function () { const txMeta = this.gatherTxMeta() const { @@ -443,25 +486,13 @@ ConfirmSendToken.prototype.render = function () { }, } = this.getData() - this.inputs = [] - const isTxReprice = Boolean(txMeta.lastGasPrice) - const title = isTxReprice ? this.context.t('reprice_title') : this.context.t('confirm') - const subtitle = isTxReprice - ? this.context.t('reprice_subtitle') - : this.context.t('pleaseReviewTransaction') return ( h('div.confirm-screen-container.confirm-send-token', [ // Main Send token Card h('div.page-container', [ - h('div.page-container__header', [ - !txMeta.lastGasPrice && h('button.confirm-screen-back-button', { - onClick: () => this.editTransaction(txMeta), - }, this.context.t('edit')), - h('div.page-container__title', title), - h('div.page-container__subtitle', subtitle), - ]), + this.renderHeader(isTxReprice), h('.page-container__content', [ h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.confirm-screen-account-wrapper', [ diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index fb409cb92..3f8cd8823 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -12,7 +12,7 @@ const { getSymbolAndDecimals } = require('../../token-util') const ConfirmSendEther = require('./confirm-send-ether') const ConfirmSendToken = require('./confirm-send-token') const ConfirmDeployContract = require('./confirm-deploy-contract') -const Loading = require('../loading') +const Loading = require('../loading-screen') const TX_TYPES = { DEPLOY_CONTRACT: 'deploy_contract', @@ -130,7 +130,6 @@ PendingTx.prototype.render = function () { if (isFetching) { return h(Loading, { - fullScreen: true, loadingMessage: this.context.t('generatingTransaction'), }) } @@ -157,9 +156,7 @@ PendingTx.prototype.render = function () { sendTransaction, }) default: - return h(Loading, { - fullScreen: true, - }) + return h(Loading) } } diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index b958a2d2d..474fcf439 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () { return h('div.request-signature__balance', [ - h('div.request-signature__balance-text', [this.context.t('balance')]), + h('div.request-signature__balance-text', `${this.context.t('balance')}:`), h('div.request-signature__balance-value', `${balanceInEther} ETH`), diff --git a/ui/app/components/spinner/index.js b/ui/app/components/spinner/index.js new file mode 100644 index 000000000..9589efcf0 --- /dev/null +++ b/ui/app/components/spinner/index.js @@ -0,0 +1,2 @@ +const Spinner = require('./spinner.component') +module.exports = Spinner diff --git a/ui/app/components/spinner/spinner.component.js b/ui/app/components/spinner/spinner.component.js new file mode 100644 index 000000000..b9a2eb52a --- /dev/null +++ b/ui/app/components/spinner/spinner.component.js @@ -0,0 +1,78 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const Spinner = ({ className = '', color = '#000000' }) => { + return ( + <div className={`spinner ${className}`}> + <svg className="lds-spinner" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style={{background: 'none'}}> + <g transform="rotate(0 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(30 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(60 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(90 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(120 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(150 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(180 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(210 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(240 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(270 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(300 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite" /> + </rect> + </g> + <g transform="rotate(330 50 50)"> + <rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}> + <animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" /> + </rect> + </g> + </svg> + </div> + ) +} + +Spinner.propTypes = { + className: PropTypes.string, + color: PropTypes.string, +} + +module.exports = Spinner diff --git a/ui/app/components/text-field/index.js b/ui/app/components/text-field/index.js new file mode 100644 index 000000000..171caf7a4 --- /dev/null +++ b/ui/app/components/text-field/index.js @@ -0,0 +1,2 @@ +import TextField from './text-field.component' +module.exports = TextField diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js new file mode 100644 index 000000000..2c72d8124 --- /dev/null +++ b/ui/app/components/text-field/text-field.component.js @@ -0,0 +1,109 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/core/styles' +import { default as MaterialTextField } from '@material-ui/core/TextField' + +const inputLabelBase = { + transform: 'none', + transition: 'none', + position: 'initial', + color: '#5b5b5b', +} + +const styles = { + materialLabel: { + '&$materialFocused': { + color: '#aeaeae', + }, + '&$materialError': { + color: '#aeaeae', + }, + fontWeight: '400', + color: '#aeaeae', + }, + materialFocused: {}, + materialUnderline: { + '&:after': { + borderBottom: '2px solid #f7861c', + }, + }, + materialError: {}, + // Non-material styles + formLabel: { + '&$formLabelFocused': { + color: '#5b5b5b', + }, + '&$materialError': { + color: '#5b5b5b', + }, + }, + formLabelFocused: {}, + inputFocused: {}, + inputRoot: { + 'label + &': { + marginTop: '8px', + }, + border: '1px solid #d2d8dd', + height: '48px', + borderRadius: '4px', + padding: '0 16px', + display: 'flex', + alignItems: 'center', + '&$inputFocused': { + border: '1px solid #2f9ae0', + }, + }, + largeInputLabel: { + ...inputLabelBase, + fontSize: '1rem', + }, + inputLabel: { + ...inputLabelBase, + fontSize: '.75rem', + }, +} + +const TextField = props => { + const { error, classes, material, startAdornment, largeLabel, ...textFieldProps } = props + + return ( + <MaterialTextField + error={Boolean(error)} + helperText={error} + InputLabelProps={{ + shrink: material ? undefined : true, + className: material ? '' : (largeLabel ? classes.largeInputLabel : classes.inputLabel), + FormLabelClasses: { + root: material ? classes.materialLabel : classes.formLabel, + focused: material ? classes.materialFocused : classes.formLabelFocused, + error: classes.materialError, + }, + }} + InputProps={{ + startAdornment: startAdornment || undefined, + disableUnderline: !material, + classes: { + root: material ? '' : classes.inputRoot, + input: material ? '' : classes.input, + underline: material ? classes.materialUnderline : '', + focused: material ? '' : classes.inputFocused, + }, + }} + {...textFieldProps} + /> + ) +} + +TextField.defaultProps = { + error: null, +} + +TextField.propTypes = { + error: PropTypes.string, + classes: PropTypes.object, + material: PropTypes.bool, + startAdornment: PropTypes.element, + largeLabel: PropTypes.bool, +} + +export default withStyles(styles)(TextField) diff --git a/ui/app/components/text-field/text-field.stories.js b/ui/app/components/text-field/text-field.stories.js new file mode 100644 index 000000000..c00873b8a --- /dev/null +++ b/ui/app/components/text-field/text-field.stories.js @@ -0,0 +1,53 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import TextField from './' + +storiesOf('TextField', module) + .add('text', () => + <TextField + label="Text" + type="text" + /> + ) + .add('password', () => + <TextField + label="Password" + type="password" + /> + ) + .add('error', () => + <TextField + type="text" + label="Name" + error="Invalid value" + /> + ) + .add('Mascara text', () => + <TextField + label="Text" + type="text" + largeLabel + /> + ) + .add('Material text', () => + <TextField + label="Text" + type="text" + material + /> + ) + .add('Material password', () => + <TextField + label="Password" + type="password" + material + /> + ) + .add('Material error', () => + <TextField + type="text" + label="Name" + error="Invalid value" + material + /> + ) diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index bd4ea80a6..ef441ff73 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -1,5 +1,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') const h = require('react-hyperscript') const connect = require('react-redux').connect const inherits = require('util').inherits @@ -16,13 +18,16 @@ const { conversionUtil, multiplyCurrencies } = require('../conversion-util') const { calcTokenAmount } = require('../token-util') const { getCurrentCurrency } = require('../selectors') +const { CONFIRM_TRANSACTION_ROUTE } = require('../routes') TxListItem.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(TxListItem) - +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxListItem) function mapStateToProps (state) { return { @@ -216,6 +221,7 @@ TxListItem.prototype.setSelectedToken = function (tokenAddress) { TxListItem.prototype.resubmit = function () { const { transactionId } = this.props this.props.retryTransaction(transactionId) + .then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) } TxListItem.prototype.render = function () { diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 9e430f87b..3b29dacac 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -102,6 +102,7 @@ WalletView.prototype.render = function () { selectedIdentity, keyrings, showAccountDetailModal, + sidebarOpen, hideSidebar, history, } = this.props @@ -182,7 +183,10 @@ WalletView.prototype.render = function () { h(TokenList), h('button.btn-primary.wallet-view__add-token-button', { - onClick: () => history.push(ADD_TOKEN_ROUTE), + onClick: () => { + history.push(ADD_TOKEN_ROUTE) + sidebarOpen && hideSidebar() + }, }, this.context.t('addToken')), ]) } diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index b71538e31..461587cb1 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -7,13 +7,14 @@ const { compose } = require('recompose') const actions = require('./actions') const txHelper = require('../lib/tx-helper') const log = require('loglevel') +const R = require('ramda') const PendingTx = require('./components/pending-tx') const SignatureRequest = require('./components/signature-request') // const PendingMsg = require('./components/pending-msg') // const PendingPersonalMsg = require('./components/pending-personal-msg') // const PendingTypedMsg = require('./components/pending-typed-msg') -const Loading = require('./components/loading') +const Loading = require('./components/loading-screen') const { DEFAULT_ROUTE } = require('./routes') module.exports = compose( @@ -87,37 +88,74 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) { network, selectedAddressTxList, send, + history, + match: { params: { id: transactionId } = {} }, } = this.props - const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps - const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) - const prevTxData = prevUnconfTxList[prevIndex] || {} - const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} + + let prevTx + + if (transactionId) { + prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList) + } else { + const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps + const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) + const prevTxData = prevUnconfTxList[prevIndex] || {} + prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} + } + const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) - if (unconfTxList.length === 0 && - (prevTx.status === 'dropped' || !send.to && this.getUnapprovedMessagesTotal() === 0)) { + if (prevTx.status === 'dropped') { + this.props.dispatch(actions.showModal({ + name: 'TRANSACTION_CONFIRMED', + onHide: () => history.push(DEFAULT_ROUTE), + })) + + return + } + + if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) { this.props.history.push(DEFAULT_ROUTE) } } -ConfirmTxScreen.prototype.render = function () { - const props = this.props +ConfirmTxScreen.prototype.getTxData = function () { const { network, + index, + unapprovedTxs, + unapprovedMsgs, + unapprovedPersonalMsgs, + unapprovedTypedMessages, + match: { params: { id: transactionId } = {} }, + } = this.props + + const unconfTxList = txHelper( unapprovedTxs, - currentCurrency, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, + network + ) + + log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`) + + return transactionId + ? R.find(({ id }) => id + '' === transactionId)(unconfTxList) + : unconfTxList[index] +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { + currentCurrency, conversionRate, blockGasLimit, // provider, // computedBalances, } = props - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) - - var txData = unconfTxList[props.index] || {} + var txData = this.getTxData() || {} var txParams = txData.params || {} // var isNotification = isPopupOrNotification() === 'notification' @@ -136,7 +174,6 @@ ConfirmTxScreen.prototype.render = function () { ]), */ - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) return currentTxView({ // Properties diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss index 725da9d39..b29afdc8c 100644 --- a/ui/app/css/itcss/components/account-dropdown.scss +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -33,7 +33,7 @@ margin-top: 4px; position: relative; } - + &__account-name { font-size: 16px; margin-left: 8px; @@ -50,7 +50,6 @@ font-family: Roboto; line-height: 16px; font-size: 12px; - font-weight: 300; } &__account-primary-balance { diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index c4037d862..657760ab5 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -23,6 +23,10 @@ &__icon { margin-left: 20px; cursor: pointer; + + &--disabled { + cursor: initial; + } } &__header { @@ -40,7 +44,6 @@ font-size: 12px; line-height: 23px; padding: 0 24px; - font-weight: 300; } &__item-icon { @@ -113,7 +116,6 @@ &__name { color: $white; font-size: 18px; - font-weight: 300; } &__balance { @@ -124,7 +126,6 @@ &__action { font-size: 16px; line-height: 18px; - font-weight: 300; cursor: pointer; } } diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss deleted file mode 100644 index 01579c27c..000000000 --- a/ui/app/css/itcss/components/add-token.scss +++ /dev/null @@ -1,462 +0,0 @@ -.add-token { - width: 498px; - max-height: 805px; - display: flex; - flex-flow: column nowrap; - position: relative; - z-index: 12; - font-family: 'Roboto'; - background: white; - border-radius: 8px; - box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08); - - &__wrapper { - background-color: $white; - display: flex; - flex-flow: column nowrap; - align-items: center; - flex: 0 0 auto; - } - - &__header { - display: flex; - flex-flow: column nowrap; - padding: 20px 20px 0px; - border-bottom: 1px solid $geyser; - flex: 0 0 auto; - - &__cancel { - color: $dodger-blue; - display: flex; - align-items: center; - - span { - font-family: Roboto; - font-size: 16px; - font-weight: 400; - line-height: 21px; - margin-left: 8px; - cursor:pointer; - } - } - - &__title { - color: $tundora; - font-size: 32px; - font-weight: 500; - margin-top: 4px; - } - - &__subtitle { - font-weight: 400; - margin-top: 15px; - margin-bottom: 21px; - } - - &__tabs { - display: flex; - - &__tab { - height: 54px; - padding: 15px 10px; - color: $dusty-gray; - font-family: Roboto; - font-size: 18px; - font-weight: 400; - line-height: 24px; - text-align: center; - } - - &__tab:first-of-type { - margin-right: 20px; - } - - &__unselected:hover { - color: $black; - border-bottom: none; - cursor: pointer; - } - - &__selected { - color: $curious-blue; - border-bottom: 3px solid $curious-blue; - } - } - } - - &__info-box { - height: 96px; - margin: 20px 20px 0px; - border-radius: 4px; - background-color: $alabaster; - position: relative; - padding-left: 18px; - display: flex; - flex-flow: column; - - &__close::after { - content: '\00D7'; - font-size: 29px; - font-weight: 200; - color: $dusty-gray; - position: absolute; - right: 17px; - cursor: pointer; - } - - &__title { - color: $mid-gray; - font-family: Roboto; - font-size: 14px; - font-weight: 400; - margin-top: 15px; - margin-bottom: 9px; - } - - &__copy, - &__copy--blue { - color: $mid-gray; - font-family: Roboto; - font-size: 12px; - font-weight: 400; - line-height: 18px; - } - - &__copy--blue { - color: $curious-blue; - } - } - - &__description { - text-align: center; - } - - &__description + &__description { - margin-top: 24px; - } - - &__confirmation-description { - font-weight: 400; - margin: 20px 0 40px 0; - } - - &__content-container { - width: 100%; - } - - &__input-container { - display: flex; - position: relative; - } - - &__search-input-error-message { - position: absolute; - bottom: -10px; - left: 22px; - font-size: 12px; - width: 100%; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - color: $red; - } - - &__input, - &__add-custom-input { - height: 54px; - padding: 0px 20px; - border: 1px solid $geyser; - border-radius: 4px; - margin: 22px 24px; - position: relative; - flex: 1 0 auto; - color: $scorpion; - font-family: Roboto; - font-size: 16px; - - &::placeholder { - color: $scorpion; - font-family: Roboto; - font-size: 16px; - line-height: 21px; - } - } - - &__footers { - width: 100%; - } - - &__add-custom { - color: $scorpion; - font-size: 18px; - line-height: 24px; - text-align: center; - padding: 12px 0; - font-weight: 600; - cursor: pointer; - position: relative; - - &:hover { - background-color: rgba(0, 0, 0, .05); - } - - &:active { - background-color: rgba(0, 0, 0, .1); - } - - .fa { - position: absolute; - right: 24px; - font-size: 24px; - line-height: 24px; - } - } - - &__add-custom-form { - display: flex; - flex-flow: column nowrap; - margin: 40px 0 30px; - } - - &__add-custom-field { - position: relative; - display: flex; - flex-flow: column; - flex: 1 0 auto; - - &--error { - .add-token__add-custom-input { - border-color: $red; - } - } - } - - &__add-custom-error-message { - position: absolute; - bottom: 1px; - left: 22px; - font-size: 12px; - width: 100%; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - color: $red; - } - - &__add-custom-label { - font-size: 16px; - font-weight: 400; - line-height: 21px; - margin-left: 22px; - color: $scorpion; - } - - &__add-custom-input { - margin-top: 6px; - font-size: 16px; - - &::placeholder { - color: $silver; - font-size: 16px; - } - } - - &__add-custom-field + &__add-custom-field { - margin-top: 6px; - } - - &__buttons { - display: flex; - flex-flow: row nowrap; - flex: 0 0 auto; - align-items: center; - justify-content: center; - padding-bottom: 30px; - padding-top: 20px; - } - - &__confirm-button, - &__cancel-button { - margin: 0 12px; - padding: 10px 13px; - height: 54px; - width: 133px; - margin-right: 1.2rem; - } - - &__token-icons-title { - color: #5B5D67; - font-family: Roboto; - font-size: 18px; - font-weight: 400; - line-height: 24px; - margin-left: 24px; - margin-top: 8px; - margin-bottom: 20px; - } - - &__token-icons-container { - display: flex; - flex-flow: row wrap; - } - - &__token-wrapper { - transition: 200ms ease-in-out; - display: flex; - flex-flow: row nowrap; - flex: 0 0 42.5%; - align-items: center; - padding: 12px; - margin: 0% 2.5% 1.5%; - box-sizing: border-box; - border-radius: 10px; - cursor: pointer; - border: 2px solid transparent; - position: relative; - - &:hover { - border: 2px solid rgba($malibu-blue, .5); - } - - &--selected { - border: 2px solid $malibu-blue !important; - } - - &--disabled { - opacity: .4; - pointer-events: none; - } - } - - &__token-data { - align-self: flex-start; - } - - &__token-name { - font-weight: 400; - font-size: 14px; - line-height: 19px; - } - - &__token-symbol { - font-size: 22px; - line-height: 29px; - font-weight: 600; - } - - &__token-icon { - width: 60px; - height: 60px; - background-repeat: no-repeat; - background-size: contain; - background-position: center; - border-radius: 50%; - background-color: $white; - box-shadow: 0 2px 4px 0 rgba($black, .24); - margin-right: 12px; - flex: 0 0 auto; - } - - &__token-message { - position: absolute; - color: $caribbean-green; - font-size: 11px; - bottom: 0; - left: 85px; - } - - &__confirmation-token-list { - display: flex; - flex-flow: column nowrap; - - .token-balance { - display: flex; - flex-flow: row nowrap; - align-items: flex-start; - - &__amount { - color: $scorpion; - font-size: 43px; - font-weight: 300; - line-height: 43px; - margin-right: 8px; - } - - &__symbol { - color: $scorpion; - font-size: 16px; - font-weight: 400; - line-height: 24px; - } - } - } - - &__confirmation-title { - padding: 30px 120px 12px; - - @media screen and (max-width: $break-small) { - padding: 20px 0; - width: 100%; - } - } - - &__confirmation-content { - padding-bottom: 60px; - } - - &__confirmation-token-list-item { - display: flex; - flex-flow: row nowrap; - margin: 0 auto; - align-items: center; - } - - &__confirmation-token-list-item + &__confirmation-token-list-item { - margin-top: 30px; - } - - &__confirmation-token-icon { - margin-right: 18px; - } - - @media screen and (max-width: $break-small) { - top: 0; - width: 100%; - overflow: hidden; - flex: 1 0 auto; - - &__wrapper { - box-shadow: none !important; - flex: 1 1 auto; - width: 100%; - overflow-y: scroll; - height: 400px; - } - - &__footers { - border-bottom: 1px solid $gallery; - } - - &__token-icon { - width: 50px; - height: 50px; - } - - &__token-symbol { - font-size: 18px; - line-height: 24px; - } - - &__token-name { - font-size: 12px; - line-height: 16px; - } - - &__buttons { - padding: 1rem; - margin: 0; - border-top: 1px solid $gallery; - width: 100%; - } - } -} diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 04e1ed96e..4cbed6093 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -15,9 +15,11 @@ font-size: 14px; font-weight: 500; transition: border-color .3s ease; - padding: 0 20px; + padding: 0 16px; min-width: 140px; + width: 100%; text-transform: uppercase; + outline: none; } .btn-primary, @@ -109,6 +111,7 @@ font-size: .85rem; font-weight: 400; transition: border-color .3s ease; + width: 100%; &:hover { border-color: $scorpion; @@ -125,6 +128,7 @@ font-size: .85rem; font-weight: 400; transition: border-color .3s ease; + width: 100%; } // No longer used in flat design, remove when modal buttons done diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 47762e8de..44cfcf870 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -175,7 +175,6 @@ margin-top: 12px; text-align: center; font-size: 40px; - font-weight: 300; line-height: 53px; flex: 0 0 auto; } @@ -235,7 +234,6 @@ section .confirm-screen-account-number, padding-left: 35px; font-size: 16px; line-height: 22px; - font-weight: 300; &:not(:last-of-type) { border-bottom: 1px solid $alto; @@ -336,7 +334,6 @@ section .confirm-screen-account-number, border-width: 0; box-shadow: none; flex: 1 0 auto; - font-weight: 300; margin: 0 5px; } @@ -353,6 +350,5 @@ section .confirm-screen-account-number, box-shadow: none; cursor: pointer; flex: 1 0 auto; - font-weight: 300; margin: 0 5px; } diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss index e043c1966..36d843c79 100644 --- a/ui/app/css/itcss/components/currency-display.scss +++ b/ui/app/css/itcss/components/currency-display.scss @@ -7,7 +7,6 @@ color: $scorpion; font-family: Roboto; font-size: 16px; - font-weight: 300; padding: 8px 10px; position: relative; diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index eeed9ee06..3ccfd5c15 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -1,15 +1,15 @@ .app-header { align-items: center; - visibility: visible; background: $gallery; position: relative; z-index: $header-z-index; display: flex; flex-flow: column nowrap; + width: 100%; + flex: 0 0 auto; @media screen and (max-width: 575px) { padding: 12px; - width: 100%; box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); z-index: $mobile-header-z-index; } @@ -17,48 +17,71 @@ @media screen and (min-width: 576px) { height: 75px; justify-content: center; + + &--back-drop { + &::after { + content: ''; + position: absolute; + width: 100%; + height: 32px; + background: $gallery; + bottom: -32px; + } + } } - .metafox-icon { + &__metafox { cursor: pointer; } -} - -.app-header--initialized { - @media screen and (min-width: 576px) { - &::after { - content: ''; - position: absolute; - width: 100%; - height: 32px; - background: $gallery; - bottom: -32px; + &__beta-label { + font-family: Roboto; + text-transform: uppercase; + font-weight: 500; + font-size: .8rem; + color: $buttercup; + margin-left: 5px; + line-height: initial; + + @media screen and (max-width: 575px) { + display: none; } } -} -.app-header-contents { - display: flex; - justify-content: space-between; - flex-flow: row nowrap; - width: 100%; - height: 6.9vh; + &__contents { + display: flex; + justify-content: space-between; + flex-flow: row nowrap; + width: 100%; - @media screen and (max-width: 575px) { - height: 100%; - } + @media screen and (max-width: 575px) { + height: 100%; + } - @media screen and (min-width: 576px) { - width: 85vw; + @media screen and (min-width: 576px) { + width: 85vw; + } + + @media screen and (min-width: 769px) { + width: 80vw; + } + + @media screen and (min-width: 1281px) { + width: 62vw; + } } - @media screen and (min-width: 769px) { - width: 80vw; + &__logo-container { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; } - @media screen and (min-width: 1281px) { - width: 62vw; + &__account-menu-container { + display: flex; + flex-flow: row nowrap; + align-items: center; } } @@ -76,20 +99,6 @@ } } -.beta-label { - font-family: Roboto; - text-transform: uppercase; - font-weight: 500; - font-size: .8rem; - color: $buttercup; - margin-left: 5px; - line-height: initial; - - @media screen and (max-width: 575px) { - display: none; - } -} - h2.page-subtitle { text-transform: uppercase; color: #aeaeae; @@ -102,20 +111,3 @@ h2.page-subtitle { flex-direction: row; align-items: center; } - -.left-menu-wrapper { - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; -} - -.header__right-actions { - display: flex; - flex-flow: row nowrap; - align-items: center; - - .identicon { - cursor: pointer; - } -} diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 1c544e162..1d87b8004 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -30,8 +30,6 @@ @import './token-list.scss'; -@import './add-token.scss'; - @import './currency-display.scss'; @import './account-menu.scss'; @@ -62,4 +60,4 @@ @import './sender-to-recipient.scss'; -@import '../../../components/export-text-container/export-text-container.scss'; +@import '../../../components/index'; diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss index a92fffec5..b07d6af17 100644 --- a/ui/app/css/itcss/components/loading-overlay.scss +++ b/ui/app/css/itcss/components/loading-overlay.scss @@ -11,8 +11,8 @@ background: rgba(255, 255, 255, .8); @media screen and (max-width: 575px) { - margin-top: 56px; - height: calc(100% - 56px); + margin-top: 66px; + height: calc(100% - 66px); } @media screen and (min-width: 576px) { @@ -26,4 +26,25 @@ width: 100vw; margin-top: 0; } + + &__container { + position: absolute; + top: 33%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + &__message { + margin-top: 32px; + font-weight: 400; + font-size: 20px; + color: $manatee; + } +} + +.spinner { + height: 58px; + width: 58px; } diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss index eb92a1b70..6409ad545 100644 --- a/ui/app/css/itcss/components/menu.scss +++ b/ui/app/css/itcss/components/menu.scss @@ -11,7 +11,6 @@ flex-flow: row nowrap; align-items: center; position: relative; - font-weight: 300; z-index: 201; @media screen and (max-width: 575px) { diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 9ae3ea7fa..74658f656 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -368,7 +368,6 @@ resize: none; padding: 9px 13px 8px; text-transform: uppercase; - font-weight: 300; } @@ -567,7 +566,6 @@ padding: 30px; font-size: 22px; color: $nile-blue; - height: 79px; } &__message { @@ -796,7 +794,6 @@ .simple-dropdown { color: #5B5D67; font-size: 16px; - font-weight: 300; line-height: 21px; border: 1px solid #D8D8D8; background-color: #FFFFFF; @@ -834,7 +831,6 @@ padding: 30px; font-size: 22px; color: $nile-blue; - height: 79px; } .notification-modal-message { diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 777a82318..bbe0ee661 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -117,7 +117,6 @@ $wallet-view-bg: $alabaster; font-size: 14px; line-height: 12px; padding: 4px 12px; - font-weight: 300; cursor: pointer; flex: 0 0 auto; @@ -145,8 +144,8 @@ $wallet-view-bg: $alabaster; flex: 0 0 auto; margin: 36px auto; background: none; - padding: .7rem 2rem; transition: border-color .3s ease; + width: 150px; &:hover { border-color: $curious-blue; @@ -264,7 +263,6 @@ $wallet-view-bg: $alabaster; // wallet view .account-name { font-size: 24px; - font-weight: 300; color: $black; margin-top: 8px; margin-bottom: .9rem; diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss index d0b59da53..709f8baf6 100644 --- a/ui/app/css/itcss/components/pages/index.scss +++ b/ui/app/css/itcss/components/pages/index.scss @@ -1,3 +1 @@ -@import './unlock.scss'; - @import './reveal-seed.scss'; diff --git a/ui/app/css/itcss/components/pages/unlock.scss b/ui/app/css/itcss/components/pages/unlock.scss deleted file mode 100644 index 5d438377b..000000000 --- a/ui/app/css/itcss/components/pages/unlock.scss +++ /dev/null @@ -1,9 +0,0 @@ -.unlock-page { - box-shadow: none; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgb(247, 247, 247); - width: 100%; -} diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss index 083481b8f..e6916c418 100644 --- a/ui/app/css/itcss/components/request-signature.scss +++ b/ui/app/css/itcss/components/request-signature.scss @@ -43,12 +43,9 @@ } &__header__text { - height: 29px; - width: 179px; color: #5B5D67; font-family: Roboto; font-size: 22px; - font-weight: 300; line-height: 29px; z-index: 3; } @@ -125,7 +122,6 @@ color: $tundora; font-family: Roboto; font-size: 18px; - font-weight: 300; line-height: 24px; text-align: center; margin-top: 20px; diff --git a/ui/app/css/itcss/components/sections.scss b/ui/app/css/itcss/components/sections.scss index ace46bd8a..feec71c89 100644 --- a/ui/app/css/itcss/components/sections.scss +++ b/ui/app/css/itcss/components/sections.scss @@ -95,19 +95,6 @@ textarea.twelve-word-phrase { margin: -2px 8px 0px -8px; } -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; -} - .sizing-input { font-size: 14px; height: 30px; @@ -118,34 +105,6 @@ textarea.twelve-word-phrase { display: flex; } -/* Webkit */ - -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -/* Firefox 18- */ - -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} - -/* Firefox 19+ */ - -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} - -/* IE */ - -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - /* accounts */ .accounts-section { diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 362feeec8..c168242cf 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -507,7 +507,6 @@ &__copy { color: $gray; font-size: 14px; - font-weight: 300; line-height: 19px; text-align: center; margin-top: 10px; @@ -641,7 +640,6 @@ font-family: Roboto; font-size: 16px; line-height: 21px; - font-weight: 300; } } @@ -832,7 +830,6 @@ color: $tundora; font-family: Roboto; font-size: 20px; - font-weight: 300; line-height: 26px; margin-top: 17px; } diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss index dcc9b98d5..0dd61ac5e 100644 --- a/ui/app/css/itcss/components/settings.scss +++ b/ui/app/css/itcss/components/settings.scss @@ -3,8 +3,6 @@ background: $white; display: flex; flex-flow: column nowrap; - height: auto; - overflow: auto; } .settings__header { @@ -29,6 +27,8 @@ .settings__content { padding: 0 25px; + height: auto; + overflow: auto; } .settings__content-row { diff --git a/ui/app/css/itcss/components/welcome-screen.scss b/ui/app/css/itcss/components/welcome-screen.scss index bfd174ad9..af1d67398 100644 --- a/ui/app/css/itcss/components/welcome-screen.scss +++ b/ui/app/css/itcss/components/welcome-screen.scss @@ -1,59 +1,60 @@ .welcome-screen { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + font-family: Roboto; + font-weight: 400; + width: 100%; + flex: 1 0 auto; + padding: 70px 0; + background: $white; + + @media screen and (max-width: 575px) { + padding: 0; + } + + &__info { display: flex; flex-flow: column; - justify-content: center; - align-items: center; - font-family: Roboto; - font-weight: 400; width: 100%; - flex: 1 0 auto; - padding: 70px 0; - background: $white; - - @media screen and (max-width: 575px) { - padding: 0; - } - - &__info { - display: flex; - flex-flow: column; - width: 100%; - height: 100%; - align-items: center; - - &__header { - font-size: 1.65em; - margin-bottom: 14px; - - @media screen and (max-width: 575px) { - font-size: 1.5em; - } - } + height: 100%; + align-items: center; + justify-content: center; - &__copy { - font-size: 1em; - width: 400px; - max-width: 90vw; - text-align: center; + &__header { + font-size: 1.65em; + margin-bottom: 14px; - @media screen and (max-width: 575px) { - font-size: 0.9em; - } - } + @media screen and (max-width: 575px) { + font-size: 1.5em; + } } - &__button { - height: 54px; - width: 198px; - box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); - color: #FFFFFF; - font-size: 20px; - font-weight: 500; - line-height: 26px; + &__copy { + font-size: 1em; + width: 400px; + max-width: 90vw; text-align: center; - text-transform: uppercase; - margin: 35px 0 14px; - transition: 200ms ease-in-out; - background-color: rgba(247, 134, 28, 0.9); + + @media screen and (max-width: 575px) { + font-size: .9em; + } } + } + + &__button { + height: 54px; + width: 198px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); + color: #fff; + font-size: 20px; + font-weight: 500; + line-height: 26px; + text-align: center; + text-transform: uppercase; + margin: 35px 0 14px; + transition: 200ms ease-in-out; + background-color: rgba(247, 134, 28, .9); + } } diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss index 7a64810c4..3525d2003 100644 --- a/ui/app/css/itcss/generic/index.scss +++ b/ui/app/css/itcss/generic/index.scss @@ -12,7 +12,7 @@ html, body { font-family: Roboto, Arial; color: #4d4d4d; - font-weight: 300; + font-weight: 400; background: #f7f7f7; width: 100%; height: 100%; @@ -74,28 +74,32 @@ input.large-input { } .page-container { - width: 400px; + width: 408px; background-color: $white; box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); z-index: 25; display: flex; flex-flow: column; - border-radius: 7px; + border-radius: 8px; &__header { display: flex; flex-flow: column; border-bottom: 1px solid $geyser; - padding: 20px; + padding: 16px; flex: 0 0 auto; position: relative; + + &--no-padding-bottom { + padding-bottom: 0; + } } &__header-close { color: $tundora; position: absolute; - top: 20px; - right: 20px; + top: 16px; + right: 16px; cursor: pointer; overflow: hidden; @@ -117,7 +121,7 @@ input.large-input { flex-flow: row; justify-content: center; border-top: 1px solid $geyser; - padding: 1.6rem; + padding: 16px; flex: 0 0 auto; .btn-clear, @@ -128,11 +132,10 @@ input.large-input { } &__footer-button { - width: 165px; height: 55px; font-size: 1rem; text-transform: uppercase; - margin-right: 1.2rem; + margin-right: 16px; border-radius: 2px; &:last-of-type { @@ -162,25 +165,20 @@ input.large-input { } &__tabs { - padding: 0 1.3rem; display: flex; + margin-top: 16px; } &__tab { min-width: 5rem; - padding: .2rem .8rem .9rem; + padding: 8px; color: $dusty-gray; font-family: Roboto; - font-size: 1.1rem; - line-height: initial; + font-size: 1rem; text-align: center; cursor: pointer; border-bottom: none; - margin-right: 1rem; - - &:hover { - color: $black; - } + margin-right: 16px; &:last-of-type { margin-right: 0; @@ -189,26 +187,22 @@ input.large-input { &--selected { color: $curious-blue; border-bottom: 3px solid $curious-blue; - - &:hover { - color: $curious-blue; - } } } &--full-width { - width: initial; + width: 100% !important; } &--full-height { - height: 100%; + height: 100% !important; + max-height: initial !important; + min-height: initial !important; } &__content { - height: 100%; overflow-y: auto; - min-height: 250px; - max-height: 400px; + flex: 1; } &__warning-container { @@ -256,12 +250,14 @@ input.large-input { overflow-y: auto; background-color: $white; border-radius: 0; + flex: 1; } } @media screen and (min-width: 576px) { .page-container { - height: 600px; + max-height: 82vh; + min-height: 570px; flex: 0 0 auto; } } @@ -304,3 +300,9 @@ input.form-control { border: 1px solid $monzo; } } + +.hide-text-overflow { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js index 6cb548bb9..c20ba2d77 100644 --- a/ui/app/first-time/init-menu.js +++ b/ui/app/first-time/init-menu.js @@ -10,7 +10,6 @@ const getCaretCoordinates = require('textarea-caret') const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes') const { getEnvironmentType } = require('../../../app/scripts/lib/util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums') -const { OLD_UI_NETWORK_TYPE } = require('../../../app/scripts/controllers/network/enums') class InitializeMenuScreen extends Component { constructor (props) { @@ -190,7 +189,6 @@ class InitializeMenuScreen extends Component { showOldUI () { this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) - .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))) } } diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js new file mode 100644 index 000000000..e24517c18 --- /dev/null +++ b/ui/app/helpers/with-token-tracker.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import TokenTracker from 'eth-token-tracker' + +const withTokenTracker = WrappedComponent => { + return class TokenTrackerWrappedComponent extends Component { + static propTypes = { + userAddress: PropTypes.string.isRequired, + token: PropTypes.object.isRequired, + } + + constructor (props) { + super(props) + + this.state = { + string: '', + symbol: '', + error: null, + } + + this.tracker = null + this.updateBalance = this.updateBalance.bind(this) + this.setError = this.setError.bind(this) + } + + componentDidMount () { + this.createFreshTokenTracker() + } + + componentDidUpdate (prevProps) { + const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props + const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps + + if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) { + return + } + + if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) { + return + } + + this.createFreshTokenTracker() + } + + componentWillUnmount () { + this.removeListeners() + } + + createFreshTokenTracker () { + this.removeListeners() + + if (!global.ethereumProvider) { + return + } + + const { userAddress, token } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: [token], + pollingInterval: 8000, + }) + + this.tracker.on('update', this.updateBalance) + this.tracker.on('error', this.setError) + + this.tracker.updateBalances() + .then(() => this.updateBalance(this.tracker.serialize())) + .catch(error => this.setState({ error: error.message })) + } + + setError (error) { + this.setState({ error }) + } + + updateBalance (tokens = []) { + const [{ string, symbol }] = tokens + this.setState({ string, symbol, error: null }) + } + + removeListeners () { + if (this.tracker) { + this.tracker.stop() + this.tracker.removeListener('update', this.updateBalance) + this.tracker.removeListener('error', this.setError) + } + } + + render () { + const { string, symbol, error } = this.state + + return ( + <WrappedComponent + { ...this.props } + string={string} + symbol={symbol} + error={error} + /> + ) + } + } +} + +module.exports = withTokenTracker diff --git a/ui/app/main-container.js b/ui/app/main-container.js index c305687ea..c9b05db3b 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -3,7 +3,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const AccountAndTransactionDetails = require('./account-and-transaction-details') const Settings = require('./components/pages/settings') -const UnlockScreen = require('./components/pages/unlock') +const UnlockScreen = require('./components/pages/unlock-page') const log = require('loglevel') module.exports = MainContainer diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 2b39eb8db..4e9d0848c 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -42,6 +42,7 @@ function reduceApp (state, action) { open: false, modalState: { name: null, + props: {}, }, previousModalState: { name: null, @@ -88,13 +89,17 @@ function reduceApp (state, action) { // modal methods: case actions.MODAL_OPEN: + const { name, ...modalProps } = action.payload + return extend(appState, { - modal: Object.assign( - state.appState.modal, - { open: true }, - { modalState: action.payload }, - { previousModalState: appState.modal.modalState}, - ), + modal: { + open: true, + modalState: { + name: name, + props: { ...modalProps }, + }, + previousModalState: { ...appState.modal.modalState }, + }, }) case actions.MODAL_CLOSE: @@ -102,7 +107,7 @@ function reduceApp (state, action) { modal: Object.assign( state.appState.modal, { open: false }, - { modalState: { name: null } }, + { modalState: { name: null, props: {} } }, { previousModalState: appState.modal.modalState}, ), }) diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index bb35cf990..732fa6dea 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -28,6 +28,7 @@ function reduceMetamask (state, action) { contractExchangeRates: {}, tokenExchangeRates: {}, tokens: [], + pendingTokens: {}, send: { gasLimit: null, gasPrice: null, @@ -163,7 +164,7 @@ function reduceMetamask (state, action) { selectedTokenAddress: action.value, }) - case actions.SAVE_ACCOUNT_LABEL: + case actions.SET_ACCOUNT_LABEL: const account = action.value.account const name = action.value.label const id = {} @@ -356,6 +357,17 @@ function reduceMetamask (state, action) { currentLocale: action.value, }) + case actions.SET_PENDING_TOKENS: + return extend(metamaskState, { + pendingTokens: { ...action.payload }, + }) + + case actions.CLEAR_PENDING_TOKENS: { + return extend(metamaskState, { + pendingTokens: {}, + }) + } + default: return metamaskState diff --git a/ui/app/routes.js b/ui/app/routes.js index 4b3f8f4d8..0ff3f644d 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed' const CONFIRM_SEED_ROUTE = '/confirm-seed' const RESTORE_VAULT_ROUTE = '/restore-vault' const ADD_TOKEN_ROUTE = '/add-token' +const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token' const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' const SEND_ROUTE = '/send' @@ -31,6 +32,7 @@ module.exports = { CONFIRM_SEED_ROUTE, RESTORE_VAULT_ROUTE, ADD_TOKEN_ROUTE, + CONFIRM_ADD_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, SEND_ROUTE, diff --git a/ui/app/select-app.js b/ui/app/select-app.js index 808f9ba56..f2e8e8d10 100644 --- a/ui/app/select-app.js +++ b/ui/app/select-app.js @@ -6,8 +6,7 @@ const { HashRouter } = require('react-router-dom') const App = require('./app') const OldApp = require('../../old-ui/app/app') const { autoAddToBetaUI } = require('./selectors') -const { setFeatureFlag, setNetworkEndpoints } = require('./actions') -const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums') +const { setFeatureFlag } = require('./actions') const I18nProvider = require('./i18n-provider') function mapStateToProps (state) { @@ -24,11 +23,9 @@ function mapDispatchToProps (dispatch) { return { setFeatureFlagWithModal: () => { return dispatch(setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL')) - .then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE))) }, setFeatureFlagWithoutModal: () => { return dispatch(setFeatureFlag('betaUI', true)) - .then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE))) }, } } diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index bd00b186e..6736b5571 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -31,6 +31,7 @@ const { } = require('./components/send/send-utils') const { isValidAddress } = require('./util') const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes') +const Button = require('./components/button') SendTransactionScreen.contextTypes = { t: PropTypes.func, @@ -497,13 +498,19 @@ SendTransactionScreen.prototype.renderFooter = function () { const noErrors = !amountError && toError === null return h('div.page-container__footer', [ - h('button.btn-secondary--lg.page-container__footer-button', { + h(Button, { + type: 'secondary', + large: true, + className: 'page-container__footer-button', onClick: () => { clearSend() history.push(DEFAULT_ROUTE) }, }, this.context.t('cancel')), - h('button.btn-primary--lg.page-container__footer-button', { + h(Button, { + type: 'primary', + large: true, + className: 'page-container__footer-button', disabled: !noErrors || !gasTotal || missingTokenBalance, onClick: event => this.onSubmit(event), }, this.context.t('next')), diff --git a/ui/app/token-util.js b/ui/app/token-util.js index 920442bfc..8c5b37d7b 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -1,3 +1,4 @@ +const log = require('loglevel') const util = require('./util') function tokenInfoGetter () { @@ -29,7 +30,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { token.decimals(), ]) } catch (err) { - console.log(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err) + log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err) } const [ symbol = [], decimals = [] ] = result diff --git a/ui/app/unlock.js b/ui/app/unlock.js deleted file mode 100644 index 099e5f9c6..000000000 --- a/ui/app/unlock.js +++ /dev/null @@ -1,141 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter -const { OLD_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums') -const { getEnvironmentType } = require('../../app/scripts/lib/util') -const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums') - -const Mascot = require('./components/mascot') - -UnlockScreen.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(UnlockScreen) - - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.unlock-screen', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, this.context.t('appName')), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - background: 'white', - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, this.context.t('login')), - - h('p.pointer', { - onClick: () => { - this.props.dispatch(actions.markPasswordForgotten()) - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }, - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, this.context.t('restoreFromSeed')), - - h('p.pointer', { - onClick: () => { - this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) - .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))) - }, - style: { - fontSize: '0.8em', - color: '#aeaeae', - textDecoration: 'underline', - marginTop: '32px', - }, - }, this.context.t('classicInterface')), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/index.js b/ui/index.js index 075faf66d..bd9ecc28b 100644 --- a/ui/index.js +++ b/ui/index.js @@ -5,11 +5,6 @@ const actions = require('./app/actions') const configureStore = require('./app/store') const txHelper = require('./lib/tx-helper') const { fetchLocale } = require('./i18n-helper') -const { - OLD_UI_NETWORK_TYPE, - BETA_UI_NETWORK_TYPE, -} = require('../app/scripts/controllers/network/enums') - const log = require('loglevel') module.exports = launchMetamaskUi @@ -55,10 +50,6 @@ async function startApp (metamaskState, accountManager, opts) { networkVersion: opts.networkVersion, }) - const useBetaUi = metamaskState.featureFlags.betaUI - const networkEndpointType = useBetaUi ? BETA_UI_NETWORK_TYPE : OLD_UI_NETWORK_TYPE - store.dispatch(actions.setNetworkEndpoints(networkEndpointType)) - // if unconfirmed txs, start on txConf page const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const numberOfUnapprivedTx = unapprovedTxsAll.length |