diff options
Diffstat (limited to 'ui/app')
67 files changed, 2823 insertions, 1205 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index ad4270cef..81d9c333b 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -3,6 +3,7 @@ const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const { getTokenAddressFromTokenObject } = require('./util') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') +const log = require('loglevel') var actions = { _setBackgroundConnection: _setBackgroundConnection, @@ -220,8 +221,6 @@ var actions = { coinBaseSubview: coinBaseSubview, SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', shapeShiftSubview: shapeShiftSubview, - UPDATE_TOKEN_EXCHANGE_RATE: 'UPDATE_TOKEN_EXCHANGE_RATE', - updateTokenExchangeRate, PAIR_UPDATE: 'PAIR_UPDATE', pairUpdate: pairUpdate, coinShiftRquest: coinShiftRquest, @@ -346,16 +345,14 @@ function transitionBackward () { } } -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) +function clearSeedWordCache () { + log.debug(`background.clearSeedWordCache`) + return dispatch => { return new Promise((resolve, reject) => { background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.displayWarning(err.message)) - reject(err) + return reject(err) } log.info('Seed word cache cleared. ' + account) @@ -366,6 +363,22 @@ function confirmSeedWords () { } } +function confirmSeedWords () { + return async dispatch => { + dispatch(actions.showLoadingIndication()) + const account = await dispatch(clearSeedWordCache()) + return dispatch(setIsRevealingSeedWords(false)) + .then(() => { + dispatch(actions.hideLoadingIndication()) + return account + }) + .catch(() => { + dispatch(actions.hideLoadingIndication()) + return account + }) + } +} + function createNewVaultAndRestore (password, seed) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -447,11 +460,13 @@ function requestRevealSeed (password) { } dispatch(actions.showNewVaultSeed(result)) - dispatch(actions.hideLoadingIndication()) resolve() }) }) }) + .then(() => dispatch(setIsRevealingSeedWords(true))) + .then(() => dispatch(actions.hideLoadingIndication())) + .catch(() => dispatch(actions.hideLoadingIndication())) } } @@ -571,35 +586,47 @@ function signMsg (msgData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) + return new Promise((resolve, reject) => { + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } - dispatch(actions.completedTx(msgData.metamaskId)) + dispatch(actions.completedTx(msgData.metamaskId)) + return resolve(msgData) + }) }) } } function signPersonalMsg (msgData) { log.debug('action - signPersonalMsg') - return (dispatch) => { + return dispatch => { dispatch(actions.showLoadingIndication()) - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) + return new Promise((resolve, reject) => { + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } - dispatch(actions.completedTx(msgData.metamaskId)) + dispatch(actions.completedTx(msgData.metamaskId)) + return resolve(msgData) + }) }) } } @@ -609,16 +636,22 @@ function signTypedMsg (msgData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - log.debug(`actions calling background.signTypedMessage`) - background.signTypedMessage(msgData, (err, newState) => { - log.debug('signTypedMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) + return new Promise((resolve, reject) => { + log.debug(`actions calling background.signTypedMessage`) + background.signTypedMessage(msgData, (err, newState) => { + log.debug('signTypedMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } - dispatch(actions.completedTx(msgData.metamaskId)) + dispatch(actions.completedTx(msgData.metamaskId)) + return resolve(msgData) + }) }) } } @@ -798,17 +831,24 @@ function updateTransaction (txData) { function updateAndApproveTx (txData) { log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx.`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) - dispatch(actions.clearSend()) - if (err) { - dispatch(actions.txError(err)) - dispatch(actions.goHome()) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) + log.debug(`actions calling background.updateAndApproveTx`) + + return new Promise((resolve, reject) => { + background.updateAndApproveTransaction(txData, err => { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) + dispatch(actions.clearSend()) + + if (err) { + dispatch(actions.txError(err)) + dispatch(actions.goHome()) + log.error(err.message) + reject(err) + } + + dispatch(actions.completedTx(txData.id)) + resolve(txData) + }) }) } } @@ -836,29 +876,77 @@ function txError (err) { } function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id, (err, newState) => { + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) { + return reject(err) + } + + dispatch(actions.completedTx(msgData.id)) + return resolve(msgData) + }) + }) + } } function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + const id = msgData.id + background.cancelPersonalMessage(id, (err, newState) => { + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) { + return reject(err) + } + + dispatch(actions.completedTx(id)) + return resolve(msgData) + }) + }) + } } function cancelTypedMsg (msgData) { - const id = msgData.id - background.cancelTypedMessage(id) - return actions.completedTx(id) + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + const id = msgData.id + background.cancelTypedMessage(id, (err, newState) => { + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) { + return reject(err) + } + + dispatch(actions.completedTx(id)) + return resolve(msgData) + }) + }) + } } function cancelTx (txData) { - return (dispatch) => { + return dispatch => { log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id, () => { - dispatch(actions.clearSend()) - dispatch(actions.completedTx(txData.id)) + return new Promise((resolve, reject) => { + background.cancelTransaction(txData.id, () => { + dispatch(actions.clearSend()) + dispatch(actions.completedTx(txData.id)) + resolve(txData) + }) }) } } @@ -1249,12 +1337,13 @@ function markNoticeRead (notice) { dispatch(actions.displayWarning(err)) return reject(err) } + if (notice) { dispatch(actions.showNotice(notice)) - resolve() + resolve(true) } else { dispatch(actions.clearNotices()) - resolve() + resolve(false) } }) }) @@ -1677,28 +1766,6 @@ function shapeShiftRequest (query, options, cb) { } } -function updateTokenExchangeRate (token = '') { - const pair = `${token.toLowerCase()}_eth` - - return dispatch => { - if (!token) { - return - } - - shapeShiftRequest('marketinfo', { pair }, marketinfo => { - if (!marketinfo.error) { - dispatch({ - type: actions.UPDATE_TOKEN_EXCHANGE_RATE, - payload: { - pair, - marketinfo, - }, - }) - } - }) - } -} - function setFeatureFlag (feature, activated, notificationType) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -1773,7 +1840,7 @@ function forceUpdateMetamaskState (dispatch) { } dispatch(actions.updateMetamaskState(newState)) - resolve() + resolve(newState) }) }) } @@ -1856,3 +1923,11 @@ function updateNetworkEndpointType (networkEndpointType) { value: networkEndpointType, } } + +function setIsRevealingSeedWords (reveal) { + return dispatch => { + log.debug(`background.setIsRevealingSeedWords`) + background.setIsRevealingSeedWords(reveal) + return forceUpdateMetamaskState(dispatch) + } +} diff --git a/ui/app/app.js b/ui/app/app.js index 0b7a7a1e0..0b38b1326 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -1,62 +1,420 @@ -const inherits = require('util').inherits -const Component = require('react').Component +const { Component } = require('react') +const PropTypes = require('prop-types') const connect = require('react-redux').connect +const { Route, Switch, withRouter } = require('react-router-dom') +const { compose } = require('recompose') const h = require('react-hyperscript') -const PropTypes = require('prop-types') const actions = require('./actions') const classnames = require('classnames') +const log = require('loglevel') -// mascara -const MascaraFirstTime = require('../../mascara/src/app/first-time').default -const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default // init -const OldUIInitializeMenuScreen = require('./first-time/init-menu') -const InitializeMenuScreen = MascaraFirstTime -const NewKeyChainScreen = require('./new-keychain') -const WelcomeScreen = require('./welcome-screen').default - +const InitializeScreen = require('../../mascara/src/app/first-time').default // accounts -const MainContainer = require('./main-container') const SendTransactionScreen2 = require('./components/send/send-v2-container') const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // slideout menu const WalletView = require('./components/wallet-view') // other views -const Settings = require('./settings') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const NewAccount = require('./accounts/new-account') +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 RestoreVaultPage = require('./components/pages/keychains/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') +const AddTokenPage = require('./components/pages/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 BuyView = require('./components/buy-button-subview') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const NetworkDropdown = require('./components/dropdowns/network-dropdown') const AccountMenu = require('./components/account-menu') -const QrView = require('./components/qr-code') // Global Modals const Modal = require('./components/modals/index').Modal -App.contextTypes = { - t: PropTypes.func, -} +// Routes +const { + DEFAULT_ROUTE, + UNLOCK_ROUTE, + SETTINGS_ROUTE, + REVEAL_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + ADD_TOKEN_ROUTE, + NEW_ACCOUNT_ROUTE, + SEND_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + INITIALIZE_ROUTE, + NOTICE_ROUTE, +} = require('./routes') + +class App extends Component { + componentWillMount () { + const { + currentCurrency, + setCurrentCurrencyToUSD, + isRevealingSeedWords, + clearSeedWords, + } = this.props + + if (!currentCurrency) { + setCurrentCurrencyToUSD() + } + + if (isRevealingSeedWords) { + clearSeedWords() + } + } + + renderRoutes () { + const exact = true + + 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: SEND_ROUTE, exact, component: SendTransactionScreen2 }), + h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), + h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), + h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), + ]) + ) + } + + render () { + const { + isLoading, + loadingMessage, + network, + isMouseUser, + provider, + frequentRpcList, + currentView, + setMouseUserState, + } = this.props + const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + this.getConnectingLabel() : null + log.debug('Main ui render function') + + return ( + h('.flex-column.full-height', { + className: classnames({ 'mouse-user-styles': isMouseUser }), + style: { + overflowX: 'hidden', + position: 'relative', + alignItems: 'center', + }, + tabIndex: '0', + onClick: () => setMouseUserState(true), + onKeyDown: (e) => { + if (e.keyCode === 9) { + setMouseUserState(false) + } + }, + }, [ + + // global modal + h(Modal, {}, []), + + // app bar + this.renderAppBar(), + + // sidebar + this.renderSidebar(), + + // network dropdown + h(NetworkDropdown, { + provider, + frequentRpcList, + }, []), + + h(AccountMenu), + + (isLoading || isLoadingNetwork) && h(Loading, { + loadingMessage: loadMessage, + }), + + // content + this.renderRoutes(), + ]) + ) + } + + renderGlobalModal () { + return h(Modal, { + ref: 'modalRef', + }, [ + // h(BuyOptions, {}, []), + ]) + } + + renderSidebar () { + return h('div', [ + h('style', ` + .sidebar-enter { + transition: transform 300ms ease-in-out; + transform: translateX(-100%); + } + .sidebar-enter.sidebar-enter-active { + transition: transform 300ms ease-in-out; + transform: translateX(0%); + } + .sidebar-leave { + transition: transform 200ms ease-out; + transform: translateX(0%); + } + .sidebar-leave.sidebar-leave-active { + transition: transform 200ms ease-out; + transform: translateX(-100%); + } + `), + + h(ReactCSSTransitionGroup, { + transitionName: 'sidebar', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 200, + }, [ + // A second instance of Walletview is used for non-mobile viewports + this.props.sidebarOpen ? h(WalletView, { + responsiveDisplayClassname: '.sidebar', + style: {}, + }) : undefined, + + ]), + + // overlay + // TODO: add onClick for overlay to close sidebar + this.props.sidebarOpen ? h('div.sidebar-overlay', { + style: {}, + onClick: () => { + this.props.hideSidebar() + }, + }, []) : undefined, + ]) + } + + 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, + }), + ]), + ]), + ]), + ]), -module.exports = connect(mapStateToProps, mapDispatchToProps)(App) + !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'), + ]), + ]) + ) + } -inherits(App, Component) -function App () { Component.call(this) } + toggleMetamaskActive () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } + } + + getConnectingLabel = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = this.context.t('connectingToMainnet') + } else if (providerName === 'ropsten') { + name = this.context.t('connectingToRopsten') + } else if (providerName === 'kovan') { + name = this.context.t('connectingToRopsten') + } else if (providerName === 'rinkeby') { + name = this.context.t('connectingToRinkeby') + } else { + name = this.context.t('connectingToUnknown') + } + + return name + } + + getNetworkName () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = this.context.t('mainnet') + } else if (providerName === 'ropsten') { + name = this.context.t('ropsten') + } else if (providerName === 'kovan') { + name = this.context.t('kovan') + } else if (providerName === 'rinkeby') { + name = this.context.t('rinkeby') + } else { + name = this.context.t('unknownNetwork') + } + + return name + } +} + +App.propTypes = { + currentCurrency: PropTypes.string, + setCurrentCurrencyToUSD: PropTypes.func, + isLoading: PropTypes.bool, + loadingMessage: PropTypes.string, + network: PropTypes.string, + provider: PropTypes.object, + frequentRpcList: PropTypes.array, + currentView: PropTypes.object, + sidebarOpen: PropTypes.bool, + hideSidebar: PropTypes.func, + isMascara: PropTypes.bool, + isOnboarding: PropTypes.bool, + isUnlocked: PropTypes.bool, + networkDropdownOpen: PropTypes.bool, + showNetworkDropdown: PropTypes.func, + hideNetworkDropdown: PropTypes.func, + history: PropTypes.object, + location: PropTypes.object, + dispatch: PropTypes.func, + toggleAccountMenu: PropTypes.func, + selectedAddress: PropTypes.string, + noActiveNotices: PropTypes.bool, + lostAccounts: PropTypes.array, + isInitialized: PropTypes.bool, + forgottenPassword: PropTypes.bool, + activeAddress: PropTypes.string, + unapprovedTxs: PropTypes.object, + seedWords: PropTypes.string, + unapprovedMsgCount: PropTypes.number, + unapprovedPersonalMsgCount: PropTypes.number, + unapprovedTypedMessagesCount: PropTypes.number, + welcomeScreenSeen: PropTypes.bool, + isPopup: PropTypes.bool, + betaUI: PropTypes.bool, + isMouseUser: PropTypes.bool, + setMouseUserState: PropTypes.func, + t: PropTypes.func, + isRevealingSeedWords: PropTypes.bool, + clearSeedWords: PropTypes.func, +} function mapStateToProps (state) { + const { appState, metamask } = state + const { + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + } = appState + const { identities, accounts, @@ -65,17 +423,23 @@ function mapStateToProps (state) { isInitialized, noActiveNotices, seedWords, - } = state.metamask + unapprovedTxs, + lastUnreadNotice, + lostAccounts, + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, + } = metamask const selected = address || Object.keys(accounts)[0] return { // state from plugin - networkDropdownOpen: state.appState.networkDropdownOpen, - sidebarOpen: state.appState.sidebarOpen, - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + noActiveNotices, + isInitialized, isUnlocked: state.metamask.isUnlocked, selectedAddress: state.metamask.selectedAddress, currentView: state.appState.currentView, @@ -85,14 +449,17 @@ function mapStateToProps (state) { isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), isPopup: state.metamask.isPopup, seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, menuOpen: state.appState.menuOpen, network: state.metamask.network, provider: state.metamask.provider, - forgottenPassword: state.metamask.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice, + lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, @@ -117,482 +484,15 @@ function mapDispatchToProps (dispatch, ownProps) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), + clearSeedWords: () => dispatch(actions.confirmSeedWords()), } } -App.prototype.componentWillMount = function () { - if (!this.props.currentCurrency) { - this.props.setCurrentCurrencyToUSD() - } -} - -App.prototype.render = function () { - var props = this.props - const { - isLoading, - loadingMessage, - network, - isMouseUser, - setMouseUserState, - } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - this.getConnectingLabel() : null - log.debug('Main ui render function') - - return ( - h('.flex-column.full-height', { - className: classnames({ 'mouse-user-styles': isMouseUser }), - style: { - overflowX: 'hidden', - position: 'relative', - alignItems: 'center', - }, - tabIndex: '0', - onClick: () => setMouseUserState(true), - onKeyDown: (e) => { - if (e.keyCode === 9) { - setMouseUserState(false) - } - }, - }, [ - - // global modal - h(Modal, {}, []), - - // app bar - this.renderAppBar(), - - // sidebar - this.renderSidebar(), - - // network dropdown - h(NetworkDropdown, { - provider: this.props.provider, - frequentRpcList: this.props.frequentRpcList, - }, []), - - h(AccountMenu), - - (isLoading || isLoadingNetwork) && h(Loading, { - loadingMessage: loadMessage, - }), - - // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }), - - // content - this.renderPrimary(), - ]) - ) -} - -App.prototype.renderGlobalModal = function () { - return h(Modal, { - ref: 'modalRef', - }, [ - // h(BuyOptions, {}, []), - ]) -} - -App.prototype.renderSidebar = function () { - - return h('div', { - }, [ - h('style', ` - .sidebar-enter { - transition: transform 300ms ease-in-out; - transform: translateX(-100%); - } - .sidebar-enter.sidebar-enter-active { - transition: transform 300ms ease-in-out; - transform: translateX(0%); - } - .sidebar-leave { - transition: transform 200ms ease-out; - transform: translateX(0%); - } - .sidebar-leave.sidebar-leave-active { - transition: transform 200ms ease-out; - transform: translateX(-100%); - } - `), - - h(ReactCSSTransitionGroup, { - transitionName: 'sidebar', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 200, - }, [ - // A second instance of Walletview is used for non-mobile viewports - this.props.sidebarOpen ? h(WalletView, { - responsiveDisplayClassname: '.sidebar', - style: {}, - }) : undefined, - - ]), - - // overlay - // TODO: add onClick for overlay to close sidebar - this.props.sidebarOpen ? h('div.sidebar-overlay', { - style: {}, - onClick: () => { - this.props.hideSidebar() - }, - }, []) : undefined, - ]) -} - -App.prototype.renderAppBar = function () { - const { - isUnlocked, - network, - provider, - networkDropdownOpen, - showNetworkDropdown, - hideNetworkDropdown, - currentView, - isInitialized, - betaUI, - isPopup, - welcomeScreenSeen, - } = 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.dispatch(actions.backToAccountDetail(props.activeAddress)) - }, - }, [ - // 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: currentView.name === 'confTx', - 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'), - ]), - - ]) - ) -} - -App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) { - const { isMascara } = this.props - - return isMascara - ? null - : h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - const { - isMascara, - isOnboarding, - betaUI, - isRevealingSeedWords, - welcomeScreenSeen, - Qr, - isInitialized, - isUnlocked, - } = props - const isMascaraOnboarding = isMascara && isOnboarding - const isBetaUIOnboarding = betaUI && isOnboarding - - if (!welcomeScreenSeen && betaUI && !isInitialized && !isUnlocked) { - return h(WelcomeScreen) - } - - if (isMascaraOnboarding || isBetaUIOnboarding) { - return h(MascaraFirstTime) - } - - // notices - if (!props.noActiveNotices && !betaUI) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.isInitialized && props.forgottenPassword) { - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - } else if (!props.isInitialized && !props.isUnlocked && !isRevealingSeedWords) { - log.debug('rendering menu screen') - return !betaUI - ? h(OldUIInitializeMenuScreen, {key: 'menuScreenInit'}) - : h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - - // show unlock screen - if (!props.isUnlocked) { - return h(MainContainer, { - currentViewName: props.currentView.name, - isUnlocked: props.isUnlocked, - }) - } - - // show seed words screen - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show current view - switch (props.currentView.name) { - - case 'accountDetail': - log.debug('rendering main container') - return h(MainContainer, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - - // Going to leave this here until we are ready to delete SendTransactionScreen v1 - // const SendComponentToRender = checkFeatureToggle('send-v2') - // ? SendTransactionScreen2 - // : SendTransactionScreen - - return h(SendTransactionScreen2, {key: 'send-transaction'}) - - case 'sendToken': - log.debug('rendering send token screen') - - // Going to leave this here until we are ready to delete SendTransactionScreen v1 - // const SendTokenComponentToRender = checkFeatureToggle('send-v2') - // ? SendTransactionScreen2 - // : SendTokenScreen - - return h(SendTransactionScreen2, {key: 'sendToken'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(Settings, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'new-account-page': - log.debug('rendering new account screen') - return h(NewAccount, {key: 'new-account'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(Settings, {key: 'info', tab: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'onboardingBuyEth': - log.debug('rendering onboarding buy ether screen') - return h(MascaraBuyEtherScreen, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr', Qr}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(MainContainer, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.getConnectingLabel = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = this.context.t('connectingToMainnet') - } else if (providerName === 'ropsten') { - name = this.context.t('connectingToRopsten') - } else if (providerName === 'kovan') { - name = this.context.t('connectingToRopsten') - } else if (providerName === 'rinkeby') { - name = this.context.t('connectingToRinkeby') - } else { - name = this.context.t('connectingToUnknown') - } - - return name +App.contextTypes = { + t: PropTypes.func, } -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = this.context.t('mainnet') - } else if (providerName === 'ropsten') { - name = this.context.t('ropsten') - } else if (providerName === 'kovan') { - name = this.context.t('kovan') - } else if (providerName === 'rinkeby') { - name = this.context.t('rinkeby') - } else { - name = this.context.t('unknownNetwork') - } - - return name -} +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(App) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 03955e077..043008a36 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -7,8 +7,8 @@ const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem const Identicon = require('./identicon') -const ethUtil = require('ethereumjs-util') const copyToClipboard = require('copy-to-clipboard') +const { checksumAddress } = require('../util') class AccountDropdowns extends Component { constructor (props) { @@ -212,8 +212,7 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected } = this.props - const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) - copyToClipboard(checkSumAddress) + copyToClipboard(checksumAddress(selected)) }, }, this.context.t('copyAddress'), diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 21de358d6..7638995ea 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -1,20 +1,31 @@ const inherits = require('util').inherits const Component = require('react').Component -const PropTypes = require('prop-types') const connect = require('react-redux').connect +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') +const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') const { formatBalance } = require('../../util') +const { + SETTINGS_ROUTE, + INFO_ROUTE, + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + DEFAULT_ROUTE, +} = require('../../routes') + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AccountMenu) AccountMenu.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu) - - inherits(AccountMenu, Component) function AccountMenu () { Component.call(this) } @@ -25,7 +36,6 @@ function mapStateToProps (state) { keyrings: state.metamask.keyrings, identities: state.metamask.identities, accounts: state.metamask.accounts, - } } @@ -48,11 +58,6 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, - showNewAccountPage: (formToSelect) => { - dispatch(actions.showNewAccountPage(formToSelect)) - dispatch(actions.hideSidebar()) - dispatch(actions.toggleAccountMenu()) - }, showInfoPage: () => { dispatch(actions.showInfoPage()) dispatch(actions.hideSidebar()) @@ -65,10 +70,8 @@ AccountMenu.prototype.render = function () { const { isAccountMenuOpen, toggleAccountMenu, - showNewAccountPage, lockMetamask, - showConfigPage, - showInfoPage, + history, } = this.props return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ @@ -78,30 +81,45 @@ AccountMenu.prototype.render = function () { }, [ this.context.t('myAccounts'), h('button.account-menu__logout-button', { - onClick: lockMetamask, + onClick: () => { + lockMetamask() + history.push(DEFAULT_ROUTE) + }, }, this.context.t('logout')), ]), h(Divider), h('div.account-menu__accounts', this.renderAccounts()), h(Divider), h(Item, { - onClick: () => showNewAccountPage('CREATE'), + onClick: () => { + toggleAccountMenu() + history.push(NEW_ACCOUNT_ROUTE) + }, icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }), text: this.context.t('createAccount'), }), h(Item, { - onClick: () => showNewAccountPage('IMPORT'), + onClick: () => { + toggleAccountMenu() + history.push(IMPORT_ACCOUNT_ROUTE) + }, icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), text: this.context.t('importAccount'), }), h(Divider), h(Item, { - onClick: showInfoPage, + onClick: () => { + toggleAccountMenu() + history.push(INFO_ROUTE) + }, icon: h('img', { src: 'images/mm-info-icon.svg' }), text: this.context.t('infoHelp'), }), h(Item, { - onClick: showConfigPage, + onClick: () => { + toggleAccountMenu() + history.push(SETTINGS_ROUTE) + }, icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }), text: this.context.t('settings'), }), diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index d591ab455..e31552f2d 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -4,6 +4,8 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenBalance = require('./token-balance') const Identicon = require('./identicon') +const currencyFormatter = require('currency-formatter') +const currencies = require('currency-formatter/currencies') const { formatBalance, generateBalanceObject } = require('../util') @@ -97,9 +99,17 @@ BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatS const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0 if (shouldNotRenderFiat) return null + const upperCaseFiatSuffix = fiatSuffix.toUpperCase() + + const display = currencies.find(currency => currency.code === upperCaseFiatSuffix) + ? currencyFormatter.format(Number(fiatDisplayNumber), { + code: upperCaseFiatSuffix, + }) + : `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}` + return h('div.fiat-amount', { style: {}, - }, `${fiatPrefix}${fiatDisplayNumber} ${fiatSuffix}`) + }, display) } BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { @@ -117,5 +127,9 @@ BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, co const splitBalance = formattedBalance.split(' ') - return (Number(splitBalance[0]) * conversionRate).toFixed(2) + const convertedNumber = (Number(splitBalance[0]) * conversionRate) + const wholePart = Math.floor(convertedNumber) + const decimalPart = convertedNumber - wholePart + + return wholePart + Number(decimalPart.toPrecision(2)) } diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index a133f0e29..179b6617f 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -7,7 +7,7 @@ const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem const Identicon = require('../../identicon') -const ethUtil = require('ethereumjs-util') +const { checksumAddress } = require('../../../util') const copyToClipboard = require('copy-to-clipboard') const { formatBalance } = require('../../../util') @@ -311,8 +311,7 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected } = this.props - const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) - copyToClipboard(checkSumAddress) + copyToClipboard(checksumAddress(selected)) }, style: Object.assign( dropdownMenuItemStyle, diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index feb0a7037..aff4b6ef6 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -11,6 +11,7 @@ const ensRE = /.+\..+$/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const connect = require('react-redux').connect const ToAutoComplete = require('./send/to-autocomplete') +const log = require('loglevel') EnsInput.contextTypes = { t: PropTypes.func, diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index cb6fa51fb..b9afc550f 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -1,6 +1,7 @@ const { Component } = require('react') const h = require('react-hyperscript') const PropTypes = require('prop-types') +const classnames = require('classnames') class LoadingIndicator extends Component { renderMessage () { @@ -10,14 +11,16 @@ class LoadingIndicator extends Component { render () { return ( - h('.full-flex-height.loading-overlay', {}, [ - h('img', { - src: 'images/loading.svg', - }), + h('.loading-overlay', { + className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }), + }, [ + h('.flex-center.flex-column', [ + h('img', { + src: 'images/loading.svg', + }), - h('br'), - - this.renderMessage(), + this.renderMessage(), + ]), ]) ) } @@ -25,6 +28,7 @@ class LoadingIndicator extends Component { LoadingIndicator.propTypes = { loadingMessage: PropTypes.string, + fullScreen: PropTypes.bool, } module.exports = LoadingIndicator diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 1f80aed39..447e43b7a 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -3,12 +3,13 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const ethUtil = require('ethereumjs-util') +const { stripHexPrefix } = require('ethereumjs-util') const actions = require('../../actions') const AccountModalContainer = require('./account-modal-container') const { getSelectedIdentity } = require('../../selectors') const ReadOnlyInput = require('../readonly-input') const copyToClipboard = require('copy-to-clipboard') +const { checksumAddress } = require('../../util') function mapStateToProps (state) { return { @@ -60,7 +61,7 @@ ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) { } ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { - const plainKey = privateKey && ethUtil.stripHexPrefix(privateKey) + const plainKey = privateKey && stripHexPrefix(privateKey) return privateKey ? h(ReadOnlyInput, { @@ -121,7 +122,7 @@ ExportPrivateKeyModal.prototype.render = function () { h(ReadOnlyInput, { wrapperClass: 'ellip-address-wrapper', inputClass: 'qr-ellip-address ellip-address', - value: address, + value: checksumAddress(address), }), h('div.account-modal-divider'), diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 9250cc77e..43dcd20ae 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -5,7 +5,8 @@ const connect = require('react-redux').connect const FadeModal = require('boron').FadeModal const actions = require('../../actions') const isMobileView = require('../../../lib/is-mobile-view') -const isPopupOrNotification = require('../../../../app/scripts/lib/is-popup-or-notification') +const { getEnvironmentType } = require('../../../../app/scripts/lib/util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') // Modal Components const BuyOptions = require('./buy-options-modal') @@ -162,7 +163,7 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', }, laptopModalStyle: { width: '449px', @@ -179,7 +180,7 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', }, laptopModalStyle: { width: '449px', @@ -196,7 +197,7 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', }, laptopModalStyle: { width: '449px', @@ -208,7 +209,7 @@ const MODALS = { contents: h(ConfirmResetAccount), mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', }, laptopModalStyle: { width: '473px', diff --git a/ui/app/add-token.js b/ui/app/components/pages/add-token.js index 46564a5e5..566e42450 100644 --- a/ui/app/add-token.js +++ b/ui/app/components/pages/add-token.js @@ -7,8 +7,8 @@ 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 TokenBalance = require('../../components/token-balance') +const Identicon = require('../../components/identicon') const contractList = Object.entries(contractMap) .map(([ _, tokenData]) => tokenData) .filter(tokenData => Boolean(tokenData.erc20)) @@ -24,9 +24,10 @@ const fuse = new Fuse(contractList, { { name: 'symbol', weight: 0.5 }, ], }) -const actions = require('./actions') +const actions = require('../../actions') const ethUtil = require('ethereumjs-util') -const { tokenInfoGetter } = require('./token-util') +const { tokenInfoGetter } = require('../../token-util') +const { DEFAULT_ROUTE } = require('../../routes') const emptyAddr = '0x0000000000000000000000000000000000000000' @@ -47,7 +48,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - goHome: () => dispatch(actions.goHome()), addTokens: tokens => dispatch(actions.addTokens(tokens)), } } @@ -296,7 +296,7 @@ AddTokenScreen.prototype.renderConfirmation = function () { selectedTokens, } = this.state - const { addTokens, goHome } = this.props + const { addTokens, history } = this.props const customToken = { address, @@ -333,7 +333,7 @@ AddTokenScreen.prototype.renderConfirmation = function () { onClick: () => this.setState({ isShowingConfirmation: false }), }, this.context.t('back')), h('button.btn-primary--lg', { - onClick: () => addTokens(tokens).then(goHome), + onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)), }, this.context.t('addTokens')), ]), ]) @@ -382,12 +382,12 @@ AddTokenScreen.prototype.render = function () { isShowingConfirmation, displayedTab, } = this.state - const { goHome } = this.props + const { history } = this.props return h('div.add-token', [ h('div.add-token__header', [ h('div.add-token__header__cancel', { - onClick: () => goHome(), + onClick: () => history.push(DEFAULT_ROUTE), }, [ h('i.fa.fa-angle-left.fa-lg'), h('span', this.context.t('cancel')), @@ -414,14 +414,14 @@ AddTokenScreen.prototype.render = function () { ]), ]), -// + isShowingConfirmation ? this.renderConfirmation() : this.renderTabs(), !isShowingConfirmation && h('div.add-token__buttons', [ h('button.btn-secondary--lg.add-token__cancel-button', { - onClick: goHome, + onClick: () => history.push(DEFAULT_ROUTE), }, this.context.t('cancel')), h('button.btn-primary--lg.add-token__confirm-button', { onClick: this.onNext, diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js new file mode 100644 index 000000000..1f6b0be49 --- /dev/null +++ b/ui/app/components/pages/authenticated.js @@ -0,0 +1,34 @@ +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const { Redirect } = require('react-router-dom') +const h = require('react-hyperscript') +const MetamaskRoute = require('./metamask-route') +const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes') + +const Authenticated = props => { + const { isUnlocked, isInitialized } = props + + switch (true) { + case isUnlocked && isInitialized: + return h(MetamaskRoute, { ...props }) + case !isInitialized: + return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) + default: + return h(Redirect, { to: { pathname: UNLOCK_ROUTE } }) + } +} + +Authenticated.propTypes = { + isUnlocked: PropTypes.bool, + isInitialized: PropTypes.bool, +} + +const mapStateToProps = state => { + const { metamask: { isUnlocked, isInitialized } } = state + return { + isUnlocked, + isInitialized, + } +} + +module.exports = connect(mapStateToProps)(Authenticated) diff --git a/ui/app/accounts/import/index.js b/ui/app/components/pages/create-account/import-account/index.js index 52d3dcde9..52d3dcde9 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/components/pages/create-account/import-account/index.js diff --git a/ui/app/accounts/import/json.js b/ui/app/components/pages/create-account/import-account/json.js index e53c1c9ca..946907a47 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -1,11 +1,12 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../../actions') const FileInput = require('react-simple-file-input').default - - +const { DEFAULT_ROUTE } = require('../../../../routes') const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' class JsonImportSubview extends Component { @@ -51,7 +52,7 @@ class JsonImportSubview extends Component { h('div.new-account-create-form__buttons', {}, [ h('button.btn-secondary.new-account-create-form__button', { - onClick: () => this.props.goHome(), + onClick: () => this.props.history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), ]), @@ -112,6 +113,7 @@ JsonImportSubview.propTypes = { goHome: PropTypes.func, displayWarning: PropTypes.func, importNewJsonAccount: PropTypes.func, + history: PropTypes.object, t: PropTypes.func, } @@ -133,5 +135,7 @@ JsonImportSubview.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(JsonImportSubview) - +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(JsonImportSubview) diff --git a/ui/app/accounts/import/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index 0d2898cda..c77612ea4 100644 --- a/ui/app/accounts/import/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -1,15 +1,21 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const PropTypes = require('prop-types') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../../actions') +const { DEFAULT_ROUTE } = require('../../../../routes') PrivateKeyImportView.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(PrivateKeyImportView) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(PrivateKeyImportView) function mapStateToProps (state) { @@ -20,9 +26,8 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - goHome: () => dispatch(actions.goHome()), importNewAccount: (strategy, [ privateKey ]) => { - dispatch(actions.importNewAccount(strategy, [ privateKey ])) + return dispatch(actions.importNewAccount(strategy, [ privateKey ])) }, displayWarning: () => dispatch(actions.displayWarning(null)), } @@ -35,7 +40,7 @@ function PrivateKeyImportView () { } PrivateKeyImportView.prototype.render = function () { - const { error, goHome } = this.props + const { error } = this.props return ( h('div.new-account-import-form__private-key', [ @@ -55,7 +60,7 @@ PrivateKeyImportView.prototype.render = function () { h('div.new-account-import-form__buttons', {}, [ h('button.btn-secondary--lg.new-account-create-form__button', { - onClick: () => goHome(), + onClick: () => this.props.history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), ]), @@ -83,6 +88,8 @@ PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { PrivateKeyImportView.prototype.createNewKeychain = function () { const input = document.getElementById('private-key-box') const privateKey = input.value + const { importNewAccount, history } = this.props - this.props.importNewAccount('Private Key', [ privateKey ]) + importNewAccount('Private Key', [ privateKey ]) + .then(() => history.push(DEFAULT_ROUTE)) } diff --git a/ui/app/accounts/import/seed.js b/ui/app/components/pages/create-account/import-account/seed.js index d98909baa..d98909baa 100644 --- a/ui/app/accounts/import/seed.js +++ b/ui/app/components/pages/create-account/import-account/seed.js diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js new file mode 100644 index 000000000..0962477d8 --- /dev/null +++ b/ui/app/components/pages/create-account/index.js @@ -0,0 +1,81 @@ +const Component = require('react').Component +const { Switch, Route, matchPath } = require('react-router-dom') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../../actions') +const { getCurrentViewContext } = require('../../../selectors') +const classnames = require('classnames') +const NewAccountCreateForm = require('./new-account') +const NewAccountImportForm = require('./import-account') +const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes') + +class CreateAccountPage extends Component { + renderTabs () { + const { history, location } = this.props + + return h('div.new-account__tabs', [ + h('div.new-account__tabs__tab', { + className: classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(location.pathname, { + path: NEW_ACCOUNT_ROUTE, exact: true, + }), + }), + onClick: () => history.push(NEW_ACCOUNT_ROUTE), + }, 'Create'), + + h('div.new-account__tabs__tab', { + className: classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(location.pathname, { + path: IMPORT_ACCOUNT_ROUTE, exact: true, + }), + }), + onClick: () => history.push(IMPORT_ACCOUNT_ROUTE), + }, 'Import'), + ]) + } + + render () { + return h('div.new-account', {}, [ + h('div.new-account__header', [ + h('div.new-account__title', 'New Account'), + this.renderTabs(), + ]), + h('div.new-account__form', [ + h(Switch, [ + h(Route, { + exact: true, + path: NEW_ACCOUNT_ROUTE, + component: NewAccountCreateForm, + }), + h(Route, { + exact: true, + path: IMPORT_ACCOUNT_ROUTE, + component: NewAccountImportForm, + }), + ]), + ]), + ]) + } +} + +CreateAccountPage.propTypes = { + location: PropTypes.object, + history: PropTypes.object, +} + +const mapStateToProps = state => ({ + displayedForm: getCurrentViewContext(state), +}) + +const mapDispatchToProps = dispatch => ({ + displayForm: form => dispatch(actions.setNewAccountForm(form)), + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + showExportPrivateKeyModal: () => { + dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) + }, + hideModal: () => dispatch(actions.hideModal()), + saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)), +}) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage) diff --git a/ui/app/accounts/new-account/create-form.js b/ui/app/components/pages/create-account/new-account.js index 48c74192a..40fa584be 100644 --- a/ui/app/accounts/new-account/create-form.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -2,7 +2,8 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../actions') +const { DEFAULT_ROUTE } = require('../../../routes') class NewAccountCreateForm extends Component { constructor (props, context) { @@ -19,7 +20,7 @@ class NewAccountCreateForm extends Component { render () { const { newAccountName, defaultAccountName } = this.state - + const { history, createAccount } = this.props return h('div.new-account-create-form', [ @@ -38,13 +39,16 @@ class NewAccountCreateForm extends Component { h('div.new-account-create-form__buttons', {}, [ h('button.btn-secondary--lg.new-account-create-form__button', { - onClick: () => this.props.goHome(), + onClick: () => history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), ]), h('button.btn-primary--lg.new-account-create-form__button', { - onClick: () => this.props.createAccount(newAccountName || defaultAccountName), + onClick: () => { + createAccount(newAccountName || defaultAccountName) + .then(() => history.push(DEFAULT_ROUTE)) + }, }, [ this.context.t('create'), ]), @@ -59,8 +63,8 @@ NewAccountCreateForm.propTypes = { hideModal: PropTypes.func, showImportPage: PropTypes.func, createAccount: PropTypes.func, - goHome: PropTypes.func, numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, t: PropTypes.func, } @@ -77,23 +81,17 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - toCoinbase: (address) => { - dispatch(actions.buyEth({ network: '1', address, amount: 0 })) - }, - hideModal: () => { - dispatch(actions.hideModal()) - }, - createAccount: (newAccountName) => { - dispatch(actions.addNewAccount()) - .then((newAccountAddress) => { + toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })), + hideModal: () => dispatch(actions.hideModal()), + createAccount: newAccountName => { + return dispatch(actions.addNewAccount()) + .then(newAccountAddress => { if (newAccountName) { dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) } - dispatch(actions.goHome()) }) }, showImportPage: () => dispatch(actions.showImportPage()), - goHome: () => dispatch(actions.goHome()), } } diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js new file mode 100644 index 000000000..90b8e1d37 --- /dev/null +++ b/ui/app/components/pages/home.js @@ -0,0 +1,333 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const connect = require('../../metamask-connect') +const { Redirect, withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const h = require('react-hyperscript') +const actions = require('../../actions') +const log = require('loglevel') + +// init +const NewKeyChainScreen = require('../../new-keychain') +// mascara +const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default + +// accounts +const MainContainer = require('../../main-container') + +// other views +const BuyView = require('../../components/buy-button-subview') +const QrView = require('../../components/qr-code') + +// Routes +const { + REVEAL_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + NOTICE_ROUTE, +} = require('../../routes') + +class Home extends Component { + componentDidMount () { + const { + history, + unapprovedTxs = {}, + unapprovedMsgCount = 0, + unapprovedPersonalMsgCount = 0, + unapprovedTypedMessagesCount = 0, + } = this.props + + // unapprovedTxs and unapproved messages + if (Object.keys(unapprovedTxs).length || + unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) { + history.push(CONFIRM_TRANSACTION_ROUTE) + } + } + + render () { + log.debug('rendering primary') + const { + noActiveNotices, + lostAccounts, + forgottenPassword, + currentView, + activeAddress, + seedWords, + } = this.props + + // notices + if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) { + return h(Redirect, { + to: { + pathname: NOTICE_ROUTE, + }, + }) + } + + // seed words + if (seedWords) { + log.debug('rendering seed words') + return h(Redirect, { + to: { + pathname: REVEAL_SEED_ROUTE, + }, + }) + } + + if (forgottenPassword) { + log.debug('rendering restore vault screen') + return h(Redirect, { + to: { + pathname: RESTORE_VAULT_ROUTE, + }, + }) + } + + // if (!props.noActiveNotices) { + // log.debug('rendering notice screen for unread notices.') + // return h(NoticeScreen, { + // notice: props.lastUnreadNotice, + // key: 'NoticeScreen', + // onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + // }) + // } else if (props.lostAccounts && props.lostAccounts.length > 0) { + // log.debug('rendering notice screen for lost accounts view.') + // return h(NoticeScreen, { + // notice: generateLostAccountsNotice(props.lostAccounts), + // key: 'LostAccountsNotice', + // onConfirm: () => props.dispatch(actions.markAccountsFound()), + // }) + // } + + // if (props.seedWords) { + // log.debug('rendering seed words') + // return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + // } + + // show initialize screen + // if (!isInitialized || forgottenPassword) { + // // show current view + // log.debug('rendering an initialize screen') + // // switch (props.currentView.name) { + + // // case 'restoreVault': + // // log.debug('rendering restore vault screen') + // // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + // // default: + // // log.debug('rendering menu screen') + // // return h(InitializeScreen, {key: 'menuScreenInit'}) + // // } + // } + + // // show unlock screen + // if (!props.isUnlocked) { + // return h(MainContainer, { + // currentViewName: props.currentView.name, + // isUnlocked: props.isUnlocked, + // }) + // } + + // show current view + switch (currentView.name) { + + case 'accountDetail': + log.debug('rendering main container') + return h(MainContainer, {key: 'account-detail'}) + + // case 'sendTransaction': + // log.debug('rendering send tx screen') + + // // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // // const SendComponentToRender = checkFeatureToggle('send-v2') + // // ? SendTransactionScreen2 + // // : SendTransactionScreen + + // return h(SendTransactionScreen2, {key: 'send-transaction'}) + + // case 'sendToken': + // log.debug('rendering send token screen') + + // // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // // const SendTokenComponentToRender = checkFeatureToggle('send-v2') + // // ? SendTransactionScreen2 + // // : SendTokenScreen + + // return h(SendTransactionScreen2, {key: 'sendToken'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + // case 'confTx': + // log.debug('rendering confirm tx screen') + // return h(Redirect, { + // to: { + // pathname: CONFIRM_TRANSACTION_ROUTE, + // }, + // }) + // return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + // case 'add-token': + // log.debug('rendering add-token screen from unlock screen.') + // return h(AddTokenScreen, {key: 'add-token'}) + + // case 'config': + // log.debug('rendering config screen') + // return h(Settings, {key: 'config'}) + + // case 'import-menu': + // log.debug('rendering import screen') + // return h(Import, {key: 'import-menu'}) + + // case 'reveal-seed-conf': + // log.debug('rendering reveal seed confirmation screen') + // return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + // case 'info': + // log.debug('rendering info screen') + // return h(Settings, {key: 'info', tab: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'onboardingBuyEth': + log.debug('rendering onboarding buy ether screen') + return h(MascaraBuyEtherScreen, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(MainContainer, {key: 'account-detail'}) + } + } +} + +Home.propTypes = { + currentCurrency: PropTypes.string, + isLoading: PropTypes.bool, + loadingMessage: PropTypes.string, + network: PropTypes.string, + provider: PropTypes.object, + frequentRpcList: PropTypes.array, + currentView: PropTypes.object, + sidebarOpen: PropTypes.bool, + isMascara: PropTypes.bool, + isOnboarding: PropTypes.bool, + isUnlocked: PropTypes.bool, + networkDropdownOpen: PropTypes.bool, + history: PropTypes.object, + dispatch: PropTypes.func, + selectedAddress: PropTypes.string, + noActiveNotices: PropTypes.bool, + lostAccounts: PropTypes.array, + isInitialized: PropTypes.bool, + forgottenPassword: PropTypes.bool, + activeAddress: PropTypes.string, + unapprovedTxs: PropTypes.object, + seedWords: PropTypes.string, + unapprovedMsgCount: PropTypes.number, + unapprovedPersonalMsgCount: PropTypes.number, + unapprovedTypedMessagesCount: PropTypes.number, + welcomeScreenSeen: PropTypes.bool, + isPopup: PropTypes.bool, + isMouseUser: PropTypes.bool, + t: PropTypes.func, +} + +function mapStateToProps (state) { + const { appState, metamask } = state + const { + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + } = appState + + const { + accounts, + address, + isInitialized, + noActiveNotices, + seedWords, + unapprovedTxs, + lastUnreadNotice, + lostAccounts, + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, + } = metamask + const selected = address || Object.keys(accounts)[0] + + return { + // state from plugin + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + noActiveNotices, + isInitialized, + isUnlocked: state.metamask.isUnlocked, + selectedAddress: state.metamask.selectedAddress, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + isMascara: state.metamask.isMascara, + isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), + isPopup: state.metamask.isPopup, + seedWords: state.metamask.seedWords, + unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice, + lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + currentCurrency: state.metamask.currentCurrency, + isMouseUser: state.appState.isMouseUser, + isRevealingSeedWords: state.metamask.isRevealingSeedWords, + Qr: state.appState.Qr, + welcomeScreenSeen: state.metamask.welcomeScreenSeen, + + // state needed to get account dropdown temporarily rendering from app bar + selected, + } +} + +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(Home) diff --git a/ui/app/components/pages/initialized.js b/ui/app/components/pages/initialized.js new file mode 100644 index 000000000..3adf67b28 --- /dev/null +++ b/ui/app/components/pages/initialized.js @@ -0,0 +1,25 @@ +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const { Redirect } = require('react-router-dom') +const h = require('react-hyperscript') +const { INITIALIZE_ROUTE } = require('../../routes') +const MetamaskRoute = require('./metamask-route') + +const Initialized = props => { + return props.isInitialized + ? h(MetamaskRoute, { ...props }) + : h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) +} + +Initialized.propTypes = { + isInitialized: PropTypes.bool, +} + +const mapStateToProps = state => { + const { metamask: { isInitialized } } = state + return { + isInitialized, + } +} + +module.exports = connect(mapStateToProps)(Initialized) diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js new file mode 100644 index 000000000..33575bfbb --- /dev/null +++ b/ui/app/components/pages/keychains/restore-vault.js @@ -0,0 +1,178 @@ +const { withRouter } = require('react-router-dom') +const PropTypes = require('prop-types') +const { compose } = require('recompose') +const PersistentForm = require('../../../../lib/persistent-form') +const connect = require('../../../metamask-connect') +const h = require('react-hyperscript') +const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions') +const { DEFAULT_ROUTE } = require('../../../routes') +const log = require('loglevel') + +class RestoreVaultPage extends PersistentForm { + constructor (props) { + super(props) + + this.state = { + error: null, + } + } + + createOnEnter (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } + } + + cancel () { + this.props.unMarkPasswordForgotten() + .then(this.props.history.push(DEFAULT_ROUTE)) + } + + createNewVaultAndRestore () { + this.setState({ error: null }) + + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.setState({ error: 'Password not long enough' }) + return + } + + if (password !== passwordConfirm) { + this.setState({ error: 'Passwords don\'t match' }) + return + } + + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.setState({ error: 'Seed phrases are 12 words long' }) + return + } + + // submit + this.props.createNewVaultAndRestore(password, seed) + .then(() => this.props.history.push(DEFAULT_ROUTE)) + .catch(({ message }) => { + this.setState({ error: message }) + log.error(message) + }) + } + + render () { + const { error } = this.state + this.persistentFormParentId = 'restore-vault-form' + + return ( + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + this.props.t('restoreVault'), + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: this.props.t('secretPhrase'), + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: this.props.t('newPassword8Chars'), + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: this.props.t('confirmPassword'), + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + error && ( + h('span.error.in-progress-notification', error) + ), + + // submit + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: () => this.cancel(), + }, this.props.t('cancel')), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, this.props.t('ok')), + + ]), + ]) + ) + } +} + +RestoreVaultPage.propTypes = { + history: PropTypes.object, +} + +const mapStateToProps = state => { + const { appState: { warning, forgottenPassword } } = state + + return { + warning, + forgottenPassword, + } +} + +const mapDispatchToProps = dispatch => { + return { + createNewVaultAndRestore: (password, seed) => { + return dispatch(createNewVaultAndRestore(password, seed)) + }, + unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()), + } +} + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(RestoreVaultPage) diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js new file mode 100644 index 000000000..247f3c8e2 --- /dev/null +++ b/ui/app/components/pages/keychains/reveal-seed.js @@ -0,0 +1,195 @@ +const { Component } = require('react') +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const { exportAsFile } = require('../../../util') +const { requestRevealSeed, confirmSeedWords } = require('../../../actions') +const { DEFAULT_ROUTE } = require('../../../routes') + +class RevealSeedPage extends Component { + componentDidMount () { + const passwordBox = document.getElementById('password-box') + if (passwordBox) { + passwordBox.focus() + } + } + + checkConfirmation (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } + } + + revealSeedWords () { + const password = document.getElementById('password-box').value + this.props.requestRevealSeed(password) + } + + renderSeed () { + const { seedWords, confirmSeedWords, history } = this.props + + return ( + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seedWords, + }), + + h('button.primary', { + onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)), + style: { + margin: '24px', + fontSize: '0.9em', + marginBottom: '10px', + }, + }, 'I\'ve copied it somewhere safe'), + + h('button.primary', { + onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords), + style: { + margin: '10px', + fontSize: '0.9em', + }, + }, 'Save Seed Words As File'), + ]) + ) + } + + renderConfirmation () { + const { history, warning, inProgress } = this.props + + return ( + h('.initialize-screen.flex-column.flex-center.flex-grow', { + style: { maxWidth: '420px' }, + }, [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-start', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: () => history.push(DEFAULT_ROUTE), + }, 'CANCEL'), + + // submit + h('button.primary', { + style: { marginLeft: '10px' }, + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + warning && ( + h('span.error', { + style: { + margin: '20px', + }, + }, warning.split('-')) + ), + + inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) + } + + render () { + return this.props.seedWords + ? this.renderSeed() + : this.renderConfirmation() + } +} + +RevealSeedPage.propTypes = { + requestRevealSeed: PropTypes.func, + confirmSeedWords: PropTypes.func, + seedWords: PropTypes.string, + inProgress: PropTypes.bool, + history: PropTypes.object, + warning: PropTypes.string, +} + +const mapStateToProps = state => { + const { appState: { warning }, metamask: { seedWords } } = state + + return { + warning, + seedWords, + } +} + +const mapDispatchToProps = dispatch => { + return { + requestRevealSeed: password => dispatch(requestRevealSeed(password)), + confirmSeedWords: () => dispatch(confirmSeedWords()), + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage) diff --git a/ui/app/components/pages/metamask-route.js b/ui/app/components/pages/metamask-route.js new file mode 100644 index 000000000..23c5b5199 --- /dev/null +++ b/ui/app/components/pages/metamask-route.js @@ -0,0 +1,28 @@ +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const { Route } = require('react-router-dom') +const h = require('react-hyperscript') + +const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => { + return ( + h(Route, { + ...props, + component: isMascara && mascaraComponent ? mascaraComponent : component, + }) + ) +} + +MetamaskRoute.propTypes = { + component: PropTypes.func, + mascaraComponent: PropTypes.func, + isMascara: PropTypes.bool, +} + +const mapStateToProps = state => { + const { metamask: { isMascara } } = state + return { + isMascara, + } +} + +module.exports = connect(mapStateToProps)(MetamaskRoute) diff --git a/ui/app/components/pages/notice.js b/ui/app/components/pages/notice.js new file mode 100644 index 000000000..2329a9147 --- /dev/null +++ b/ui/app/components/pages/notice.js @@ -0,0 +1,203 @@ +const { Component } = require('react') +const h = require('react-hyperscript') +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const generateLostAccountsNotice = require('../../../lib/lost-accounts-notice') +const findDOMNode = require('react-dom').findDOMNode +const actions = require('../../actions') +const { DEFAULT_ROUTE } = require('../../routes') + +class Notice extends Component { + constructor (props) { + super(props) + + this.state = { + disclaimerDisabled: true, + } + } + + componentWillMount () { + if (!this.props.notice) { + this.props.history.push(DEFAULT_ROUTE) + } + } + + componentDidMount () { + // eslint-disable-next-line react/no-find-dom-node + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({ disclaimerDisabled: false }) + } + } + + componentWillReceiveProps (nextProps) { + if (!nextProps.notice) { + this.props.history.push(DEFAULT_ROUTE) + } + } + + componentWillUnmount () { + // eslint-disable-next-line react/no-find-dom-node + var node = findDOMNode(this) + linker.teardownListener(node) + } + + handleAccept () { + this.setState({ disclaimerDisabled: true }) + this.props.onConfirm() + } + + render () { + const { notice = {} } = this.props + const { title, date, body } = notice + const { disclaimerDisabled } = this.state + + return ( + h('.flex-column.flex-center.flex-grow', { + style: { + width: '100%', + }, + }, [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({ disclaimerDisabled: false }) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button.primary', { + disabled: disclaimerDisabled, + onClick: () => this.handleAccept(), + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) + } + +} + +const mapStateToProps = state => { + const { metamask } = state + const { noActiveNotices, lastUnreadNotice, lostAccounts } = metamask + + return { + noActiveNotices, + lastUnreadNotice, + lostAccounts, + } +} + +Notice.propTypes = { + notice: PropTypes.object, + onConfirm: PropTypes.func, + history: PropTypes.object, +} + +const mapDispatchToProps = dispatch => { + return { + markNoticeRead: lastUnreadNotice => dispatch(actions.markNoticeRead(lastUnreadNotice)), + markAccountsFound: () => dispatch(actions.markAccountsFound()), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { noActiveNotices, lastUnreadNotice, lostAccounts } = stateProps + const { markNoticeRead, markAccountsFound } = dispatchProps + + let notice + let onConfirm + + if (!noActiveNotices) { + notice = lastUnreadNotice + onConfirm = () => markNoticeRead(lastUnreadNotice) + } else if (lostAccounts && lostAccounts.length > 0) { + notice = generateLostAccountsNotice(lostAccounts) + onConfirm = () => markAccountsFound() + } + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + notice, + onConfirm, + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notice) diff --git a/ui/app/components/pages/settings/index.js b/ui/app/components/pages/settings/index.js new file mode 100644 index 000000000..384ae4b41 --- /dev/null +++ b/ui/app/components/pages/settings/index.js @@ -0,0 +1,59 @@ +const { Component } = require('react') +const { Switch, Route, matchPath } = require('react-router-dom') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const TabBar = require('../../tab-bar') +const Settings = require('./settings') +const Info = require('./info') +const { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } = require('../../../routes') + +class Config extends Component { + renderTabs () { + const { history, location } = this.props + + return h('div.settings__tabs', [ + h(TabBar, { + tabs: [ + { content: 'Settings', key: SETTINGS_ROUTE }, + { content: 'Info', key: INFO_ROUTE }, + ], + isActive: key => matchPath(location.pathname, { path: key, exact: true }), + onSelect: key => history.push(key), + }), + ]) + } + + render () { + const { history } = this.props + + return ( + h('.main-container.settings', {}, [ + h('.settings__header', [ + h('div.settings__close-button', { + onClick: () => history.push(DEFAULT_ROUTE), + }), + this.renderTabs(), + ]), + h(Switch, [ + h(Route, { + exact: true, + path: INFO_ROUTE, + component: Info, + }), + h(Route, { + exact: true, + path: SETTINGS_ROUTE, + component: Settings, + }), + ]), + ]) + ) + } +} + +Config.propTypes = { + location: PropTypes.object, + history: PropTypes.object, +} + +module.exports = Config diff --git a/ui/app/components/pages/settings/info.js b/ui/app/components/pages/settings/info.js new file mode 100644 index 000000000..bd9040499 --- /dev/null +++ b/ui/app/components/pages/settings/info.js @@ -0,0 +1,120 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +class Info extends Component { + constructor (props) { + super(props) + + this.state = { + version: global.platform.getVersion(), + } + } + + renderLogo () { + return ( + h('div.settings__info-logo-wrapper', [ + h('img.settings__info-logo', { src: 'images/info-logo.png' }), + ]) + ) + } + + renderInfoLinks () { + return ( + h('div.settings__content-item.settings__content-item--without-height', [ + h('div.settings__info-link-header', this.context.t('links')), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + }, [ + h('span.settings__info-link', this.context.t('privacyMsg')), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + }, [ + h('span.settings__info-link', this.context.t('terms')), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + }, [ + h('span.settings__info-link', this.context.t('attributions')), + ]), + ]), + h('hr.settings__info-separator'), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://support.metamask.io', + target: '_blank', + }, [ + h('span.settings__info-link', this.context.t('supportCenter')), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('span.settings__info-link', this.context.t('visitWebSite')), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + target: '_blank', + href: 'mailto:help@metamask.io?subject=Feedback', + }, [ + h('span.settings__info-link', this.context.t('emailUs')), + ]), + ]), + ]) + ) + } + + render () { + return ( + h('div.settings__content', [ + h('div.settings__content-row', [ + h('div.settings__content-item.settings__content-item--without-height', [ + this.renderLogo(), + h('div.settings__info-item', [ + h('div.settings__info-version-header', 'MetaMask Version'), + h('div.settings__info-version-number', this.state.version), + ]), + h('div.settings__info-item', [ + h( + 'div.settings__info-about', + this.context.t('builtInCalifornia') + ), + ]), + ]), + this.renderInfoLinks(), + ]), + ]) + ) + } +} + +Info.propTypes = { + tab: PropTypes.string, + metamask: PropTypes.object, + setCurrentCurrency: PropTypes.func, + setRpcTarget: PropTypes.func, + displayWarning: PropTypes.func, + revealSeedConfirmation: PropTypes.func, + warning: PropTypes.string, + location: PropTypes.object, + history: PropTypes.object, + t: PropTypes.func, +} + +Info.contextTypes = { + t: PropTypes.func, +} + +module.exports = Info diff --git a/ui/app/settings.js b/ui/app/components/pages/settings/settings.js index 3aa7b9c6b..05a7379fb 100644 --- a/ui/app/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -1,16 +1,18 @@ const { Component } = require('react') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('./actions') -const infuraCurrencies = require('./infura-conversion.json') +const actions = require('../../../actions') +const infuraCurrencies = require('../../../infura-conversion.json') const validUrl = require('valid-url') -const { exportAsFile } = require('./util') -const TabBar = require('./components/tab-bar') -const SimpleDropdown = require('./components/dropdowns/simple-dropdown') +const { exportAsFile } = require('../../../util') +const SimpleDropdown = require('../../dropdowns/simple-dropdown') const ToggleButton = require('react-toggle-button') -const locales = require('../../app/_locales/index.json') -const { OLD_UI_NETWORK_TYPE } = require('../../app/scripts/config').enums +const { REVEAL_SEED_ROUTE } = require('../../../routes') +const locales = require('../../../../../app/_locales/index.json') +const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/config').enums const getInfuraCurrencyOptions = () => { const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { @@ -40,30 +42,11 @@ class Settings extends Component { constructor (props) { super(props) - const { tab } = props - const activeTab = tab === 'info' ? 'info' : 'settings' - this.state = { - activeTab, newRpc: '', } } - renderTabs () { - const { activeTab } = this.state - - return h('div.settings__tabs', [ - h(TabBar, { - tabs: [ - { content: this.context.t('settings'), key: 'settings' }, - { content: this.context.t('info'), key: 'info' }, - ], - defaultTab: activeTab, - tabSelected: key => this.setState({ activeTab: key }), - }), - ]) - } - renderBlockieOptIn () { const { metamask: { useBlockie }, setUseBlockie } = this.props @@ -253,7 +236,7 @@ class Settings extends Component { } renderSeedWords () { - const { revealSeedConfirmation } = this.props + const { history } = this.props return ( h('div.settings__content-row', [ @@ -261,9 +244,9 @@ class Settings extends Component { h('div.settings__content-item', [ h('div.settings__content-item-col', [ h('button.btn-primary--lg.settings__button--red', { - onClick (event) { + onClick: event => { event.preventDefault() - revealSeedConfirmation() + history.push(REVEAL_SEED_ROUTE) }, }, this.context.t('revealSeedWords')), ]), @@ -310,7 +293,7 @@ class Settings extends Component { ]) } - renderSettingsContent () { + render () { const { warning, isMascara } = this.props return ( @@ -328,120 +311,9 @@ class Settings extends Component { ]) ) } - - renderLogo () { - return ( - h('div.settings__info-logo-wrapper', [ - h('img.settings__info-logo', { src: 'images/info-logo.png' }), - ]) - ) - } - - renderInfoLinks () { - return ( - h('div.settings__content-item.settings__content-item--without-height', [ - h('div.settings__info-link-header', this.context.t('links')), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - }, [ - h('span.settings__info-link', this.context.t('privacyMsg')), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - }, [ - h('span.settings__info-link', this.context.t('terms')), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - }, [ - h('span.settings__info-link', this.context.t('attributions')), - ]), - ]), - h('hr.settings__info-separator'), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://support.metamask.io', - target: '_blank', - }, [ - h('span.settings__info-link', this.context.t('supportCenter')), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('span.settings__info-link', this.context.t('visitWebSite')), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - target: '_blank', - href: 'mailto:help@metamask.io?subject=Feedback', - }, [ - h('span.settings__info-link', this.context.t('emailUs')), - ]), - ]), - ]) - ) - } - - renderInfoContent () { - const version = global.platform.getVersion() - - return ( - h('div.settings__content', [ - h('div.settings__content-row', [ - h('div.settings__content-item.settings__content-item--without-height', [ - this.renderLogo(), - h('div.settings__info-item', [ - h('div.settings__info-version-header', 'MetaMask Version'), - h('div.settings__info-version-number', `${version}`), - ]), - h('div.settings__info-item', [ - h( - 'div.settings__info-about', - this.context.t('builtInCalifornia') - ), - ]), - ]), - this.renderInfoLinks(), - ]), - ]) - ) - } - - render () { - const { goHome } = this.props - const { activeTab } = this.state - - return ( - h('.main-container.settings', {}, [ - h('.settings__header', [ - h('div.settings__close-button', { - onClick: goHome, - }), - this.renderTabs(), - ]), - - activeTab === 'settings' - ? this.renderSettingsContent() - : this.renderInfoContent(), - ]) - ) - } } Settings.propTypes = { - tab: PropTypes.string, metamask: PropTypes.object, setUseBlockie: PropTypes.func, setCurrentCurrency: PropTypes.func, @@ -451,7 +323,7 @@ Settings.propTypes = { setFeatureFlagToBeta: PropTypes.func, showResetAccountConfirmationModal: PropTypes.func, warning: PropTypes.string, - goHome: PropTypes.func, + history: PropTypes.object, isMascara: PropTypes.bool, updateCurrentLocale: PropTypes.func, currentLocale: PropTypes.string, @@ -469,7 +341,6 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - goHome: () => dispatch(actions.goHome()), setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)), setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)), displayWarning: warning => dispatch(actions.displayWarning(warning)), @@ -490,5 +361,7 @@ Settings.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings) - +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(Settings) diff --git a/ui/app/components/pages/unlock.js b/ui/app/components/pages/unlock.js new file mode 100644 index 000000000..567b72518 --- /dev/null +++ b/ui/app/components/pages/unlock.js @@ -0,0 +1,194 @@ +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/config').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 7bf20bced..16dbd273b 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -1,4 +1,6 @@ const Component = require('react').Component +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') @@ -21,14 +23,20 @@ const { const GasFeeDisplay = require('../send/gas-fee-display-v2') const SenderToRecipient = require('../sender-to-recipient') const NetworkDisplay = require('../network-display') +const currencyFormatter = require('currency-formatter') +const currencies = require('currency-formatter/currencies') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') +const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') ConfirmSendEther.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendEther) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmSendEther) function mapStateToProps (state) { @@ -72,7 +80,6 @@ function mapDispatchToProps (dispatch) { errors: { to: null, amount: null }, editingTransactionId: id, })) - dispatch(actions.showSendPage()) }, cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => { @@ -270,9 +277,24 @@ ConfirmSendEther.prototype.getData = function () { } } +ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, currencyCode) { + const upperCaseCurrencyCode = currencyCode.toUpperCase() + + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(value), { + code: upperCaseCurrencyCode, + }) + : value +} + +ConfirmSendEther.prototype.editTransaction = function (txMeta) { + const { editTransaction, history } = this.props + editTransaction(txMeta) + history.push(SEND_ROUTE) +} + ConfirmSendEther.prototype.render = function () { const { - editTransaction, currentCurrency, clearSend, conversionRate, @@ -309,6 +331,9 @@ ConfirmSendEther.prototype.render = function () { ? '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) + // This is from the latest master // It handles some of the errors that we are not currently handling // Leaving as comments fo reference @@ -328,7 +353,7 @@ ConfirmSendEther.prototype.render = function () { h('.page-container__header', [ h('.page-container__header-row', [ h('span.page-container__back-button', { - onClick: () => editTransaction(txMeta), + onClick: () => this.editTransaction(txMeta), style: { visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden', }, @@ -355,7 +380,7 @@ ConfirmSendEther.prototype.render = function () { // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, // ]), - h('h3.flex-center.confirm-screen-send-amount', [`${amountInFIAT}`]), + h('h3.flex-center.confirm-screen-send-amount', [`${convertedAmountInFiat}`]), h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), @@ -402,7 +427,7 @@ ConfirmSendEther.prototype.render = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${totalInFIAT} ${currentCurrency.toUpperCase()}`), + h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency.toUpperCase()}`), h('div.confirm-screen-row-detail', `${totalInETH} ETH`), ]), @@ -506,7 +531,9 @@ ConfirmSendEther.prototype.render = function () { }, this.context.t('cancel')), // Accept Button - h('button.btn-confirm.page-container__footer-button.allcaps', [this.context.t('confirm')]), + h('button.btn-confirm.page-container__footer-button.allcaps', { + onClick: event => this.onSubmit(event), + }, this.context.t('confirm')), ]), ]), ]) @@ -544,6 +571,7 @@ ConfirmSendEther.prototype.cancel = function (event, txMeta) { const { cancelTransaction } = this.props cancelTransaction(txMeta) + .then(() => this.props.history.push(DEFAULT_ROUTE)) } ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) { @@ -631,4 +659,4 @@ ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, const numBN = new BN(numerator) const denomBN = new BN(denominator) return targetBN.mul(numBN).div(denomBN) -}
\ No newline at end of file +} diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 19e591fd6..656093b3d 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -1,4 +1,6 @@ const Component = require('react').Component +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') @@ -25,6 +27,8 @@ const { calcTokenAmount, } = require('../../token-util') const classnames = require('classnames') +const currencyFormatter = require('currency-formatter') +const currencies = require('currency-formatter/currencies') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') @@ -33,16 +37,20 @@ const { getSelectedAddress, getSelectedTokenContract, } = require('../../selectors') +const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') ConfirmSendToken.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmSendToken) function mapStateToProps (state, ownProps) { - const { token: { symbol }, txData } = ownProps + const { token: { address }, txData } = ownProps const { txParams } = txData || {} const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) @@ -53,7 +61,7 @@ function mapStateToProps (state, ownProps) { } = state.metamask const accounts = state.metamask.accounts const selectedAddress = getSelectedAddress(state) - const tokenExchangeRate = getTokenExchangeRate(state, symbol) + const tokenExchangeRate = getTokenExchangeRate(state, address) const { balance } = accounts[selectedAddress] return { conversionRate, @@ -69,12 +77,9 @@ function mapStateToProps (state, ownProps) { } function mapDispatchToProps (dispatch, ownProps) { - const { token: { symbol } } = ownProps - return { backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)), editTransaction: txMeta => { const { token: { address } } = ownProps const { txParams = {}, id } = txMeta @@ -147,6 +152,12 @@ function ConfirmSendToken () { this.onSubmit = this.onSubmit.bind(this) } +ConfirmSendToken.prototype.editTransaction = function (txMeta) { + const { editTransaction, history } = this.props + editTransaction(txMeta) + history.push(SEND_ROUTE) +} + ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) { const { balance: oldBalance, @@ -191,7 +202,6 @@ ConfirmSendToken.prototype.componentWillMount = function () { .balanceOf(selectedAddress) .then(usersToken => { }) - this.props.updateTokenExchangeRate() this.updateComponentSendErrors({}) } @@ -310,10 +320,12 @@ ConfirmSendToken.prototype.renderHeroAmount = function () { const txParams = txMeta.txParams || {} const { memo = '' } = txParams + const convertedAmountInFiat = this.convertToRenderableCurrency(fiatAmount, currentCurrency) + return fiatAmount ? ( h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`), + h('h3.flex-center.confirm-screen-send-amount', `${convertedAmountInFiat}`), h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), @@ -361,6 +373,9 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatGas, token: tokenGas } = this.getGasFee() + const totalInFIAT = fiatAmount && fiatGas && addCurrencies(fiatAmount, fiatGas) + const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) + return fiatAmount && fiatGas ? ( h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ @@ -370,7 +385,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${addCurrencies(fiatAmount, fiatGas)} ${currentCurrency}`), + h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency}`), h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`), ]), ]) @@ -405,8 +420,17 @@ ConfirmSendToken.prototype.renderErrorMessage = function (message) { : null } +ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, currencyCode) { + const upperCaseCurrencyCode = currencyCode.toUpperCase() + + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(value), { + code: upperCaseCurrencyCode, + }) + : value +} + ConfirmSendToken.prototype.render = function () { - const { editTransaction } = this.props const txMeta = this.gatherTxMeta() const { from: { @@ -433,7 +457,7 @@ ConfirmSendToken.prototype.render = function () { h('div.page-container', [ h('div.page-container__header', [ !txMeta.lastGasPrice && h('button.confirm-screen-back-button', { - onClick: () => editTransaction(txMeta), + onClick: () => this.editTransaction(txMeta), }, this.context.t('edit')), h('div.page-container__title', title), h('div.page-container__subtitle', subtitle), @@ -513,7 +537,9 @@ ConfirmSendToken.prototype.render = function () { }, this.context.t('cancel')), // Accept Button - h('button.btn-confirm.page-container__footer-button.allcaps', [this.context.t('confirm')]), + h('button.btn-confirm.page-container__footer-button.allcaps', { + onClick: event => this.onSubmit(event), + }, [this.context.t('confirm')]), ]), ]), ]), @@ -566,6 +592,7 @@ ConfirmSendToken.prototype.cancel = function (event, txMeta) { const { cancelTransaction } = this.props cancelTransaction(txMeta) + .then(() => this.props.history.push(DEFAULT_ROUTE)) } ConfirmSendToken.prototype.checkValidity = function () { diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index acdd99364..6ee83ba7e 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -1,6 +1,7 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') +const PropTypes = require('prop-types') const clone = require('clone') const abi = require('human-standard-token-abi') const abiDecoder = require('abi-decoder') @@ -11,6 +12,7 @@ const util = require('../../util') const ConfirmSendEther = require('./confirm-send-ether') const ConfirmSendToken = require('./confirm-send-token') const ConfirmDeployContract = require('./confirm-deploy-contract') +const Loading = require('../loading') const TX_TYPES = { DEPLOY_CONTRACT: 'deploy_contract', @@ -53,10 +55,24 @@ function PendingTx () { } } -PendingTx.prototype.componentWillMount = async function () { +PendingTx.prototype.componentDidMount = function () { + this.setTokenData() +} + +PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) { + if (prevState.isFetching) { + this.setTokenData() + } +} + +PendingTx.prototype.setTokenData = async function () { const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} + if (txMeta.loadingDefaults) { + return + } + if (!txParams.to) { return this.setState({ transactionType: TX_TYPES.DEPLOY_CONTRACT, @@ -125,7 +141,10 @@ PendingTx.prototype.render = function () { const { sendTransaction } = this.props if (isFetching) { - return h('noscript') + return h(Loading, { + fullScreen: true, + loadingMessage: this.context.t('generatingTransaction'), + }) } switch (transactionType) { @@ -150,6 +169,12 @@ PendingTx.prototype.render = function () { sendTransaction, }) default: - return h('noscript') + return h(Loading, { + fullScreen: true, + }) } } + +PendingTx.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index 83885539c..3b2c62f49 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -3,8 +3,9 @@ const h = require('react-hyperscript') const qrCode = require('qrcode-npm').qrcode const inherits = require('util').inherits const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const { isHexPrefixed } = require('ethereumjs-util') const ReadOnlyInput = require('./readonly-input') +const { checksumAddress } = require('../util') module.exports = connect(mapStateToProps)(QrCodeView) @@ -24,16 +25,16 @@ function QrCodeView () { QrCodeView.prototype.render = function () { const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const { message, data } = props.Qr + const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${data}` const qrImage = qrCode(4, 'M') qrImage.addData(address) qrImage.make() return h('.div.flex-column.flex-center', [ - Array.isArray(Qr.message) + Array.isArray(message) ? h('.message-container', this.renderMultiMessage()) - : Qr.message && h('.qr-header', Qr.message), + : message && h('.qr-header', message), this.props.warning ? this.props.warning && h('span.error.flex-center', { style: { @@ -50,7 +51,7 @@ QrCodeView.prototype.render = function () { h(ReadOnlyInput, { wrapperClass: 'ellip-address-wrapper', inputClass: 'qr-ellip-address', - value: Qr.data, + value: checksumAddress(data), }), ]) } diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js index 1ad3f69c1..b5e604a6e 100644 --- a/ui/app/components/send/account-list-item.js +++ b/ui/app/components/send/account-list-item.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect +const { checksumAddress } = require('../../util') const Identicon = require('../identicon') const CurrencyDisplay = require('./currency-display') const { conversionRateSelector, getCurrentCurrency } = require('../../selectors') @@ -56,7 +57,7 @@ AccountListItem.prototype.render = function () { ]), - displayAddress && name && h('div.account-list-item__account-address', address), + displayAddress && name && h('div.account-list-item__account-address', checksumAddress(address)), displayBalance && h(CurrencyDisplay, { primaryCurrency: 'ETH', diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 819fee0a0..a7bd5d7ea 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -3,6 +3,8 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const CurrencyInput = require('../currency-input') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') +const currencyFormatter = require('currency-formatter') +const currencies = require('currency-formatter/currencies') module.exports = CurrencyDisplay @@ -53,12 +55,32 @@ CurrencyDisplay.prototype.getValueToRender = function () { }) } +CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { + const { primaryCurrency, convertedCurrency, conversionRate } = this.props + + let convertedValue = conversionUtil(nonFormattedValue, { + fromNumericBase: 'dec', + fromCurrency: primaryCurrency, + toCurrency: convertedCurrency, + numberOfDecimals: 2, + conversionRate, + }) + convertedValue = Number(convertedValue).toFixed(2) + + const upperCaseCurrencyCode = convertedCurrency.toUpperCase() + + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(convertedValue), { + code: upperCaseCurrencyCode, + }) + : convertedValue +} + CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', primaryBalanceClassName = 'currency-display__input', convertedBalanceClassName = 'currency-display__converted-value', - conversionRate, primaryCurrency, convertedCurrency, readOnly = false, @@ -68,14 +90,7 @@ CurrencyDisplay.prototype.render = function () { const valueToRender = this.getValueToRender() - let convertedValue = conversionUtil(valueToRender, { - fromNumericBase: 'dec', - fromCurrency: primaryCurrency, - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) - convertedValue = Number(convertedValue).toFixed(2) + const convertedValueToRender = this.getConvertedValueToRender(valueToRender) return h('div', { className, @@ -108,7 +123,7 @@ CurrencyDisplay.prototype.render = function () { h('div', { className: convertedBalanceClassName, - }, `${convertedValue} ${convertedCurrency.toUpperCase()}`), + }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`), ]) diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 08c26a91f..adfc91240 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -2,6 +2,8 @@ const connect = require('react-redux').connect const actions = require('../../actions') const abi = require('ethereumjs-abi') const SendEther = require('../../send-v2') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const { accountsWithSendEtherInfoSelector, @@ -16,7 +18,10 @@ const { getSelectedTokenContract, } = require('../../selectors') -module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SendEther) function mapStateToProps (state) { const fromAccounts = accountsWithSendEtherInfoSelector(state) @@ -61,7 +66,6 @@ function mapDispatchToProps (dispatch) { showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })), estimateGas: params => dispatch(actions.estimateGas(params)), getGasPrice: () => dispatch(actions.getGasPrice()), - updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), signTokenTx: (tokenAddress, toAddress, amount, txData) => ( dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), @@ -79,7 +83,6 @@ function mapDispatchToProps (dispatch) { updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), - goHome: () => dispatch(actions.goHome()), clearSend: () => dispatch(actions.clearSend()), setMaxModeTo: bool => dispatch(actions.setMaxModeTo(bool)), } diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index 41415411e..b958a2d2d 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -6,6 +6,8 @@ const Identicon = require('./identicon') const connect = require('react-redux').connect const ethUtil = require('ethereumjs-util') const classnames = require('classnames') +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') const AccountDropdownMini = require('./dropdowns/account-dropdown-mini') @@ -20,6 +22,8 @@ const { conversionRateSelector, } = require('../selectors.js') +const { DEFAULT_ROUTE } = require('../routes') + function mapStateToProps (state) { return { balance: getSelectedAccount(state).balance, @@ -42,7 +46,10 @@ SignatureRequest.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(SignatureRequest) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SignatureRequest) inherits(SignatureRequest, Component) @@ -229,10 +236,14 @@ SignatureRequest.prototype.renderFooter = function () { return h('div.request-signature__footer', [ h('button.btn-secondary--lg.request-signature__footer__cancel-button', { - onClick: cancel, + onClick: event => { + cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE)) + }, }, this.context.t('cancel')), h('button.btn-primary--lg', { - onClick: sign, + onClick: event => { + sign(event).then(() => this.props.history.push(DEFAULT_ROUTE)) + }, }, this.context.t('sign')), ]) } diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index a80640116..0016a09c1 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -4,31 +4,17 @@ const PropTypes = require('prop-types') const classnames = require('classnames') class TabBar extends Component { - constructor (props) { - super(props) - const { defaultTab, tabs } = props - - this.state = { - subview: defaultTab || tabs[0].key, - } - } - render () { - const { tabs = [], tabSelected } = this.props - const { subview } = this.state + const { tabs = [], onSelect, isActive } = this.props return ( h('.tab-bar', {}, [ - tabs.map((tab) => { - const { key, content } = tab + tabs.map(({ key, content }) => { return h('div', { className: classnames('tab-bar__tab pointer', { - 'tab-bar__tab--active': subview === key, + 'tab-bar__tab--active': isActive(key, content), }), - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, + onClick: () => onSelect(key), key, }, content) }), @@ -39,9 +25,9 @@ class TabBar extends Component { } TabBar.propTypes = { - defaultTab: PropTypes.string, + isActive: PropTypes.func.isRequired, tabs: PropTypes.array, - tabSelected: PropTypes.func, + onSelect: PropTypes.func, } module.exports = TabBar diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index 2f71c0687..1900ccec7 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -4,6 +4,7 @@ const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const connect = require('react-redux').connect const selectors = require('../selectors') +const log = require('loglevel') function mapStateToProps (state) { return { diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 0332fde88..c84117d84 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -16,7 +16,7 @@ function mapStateToProps (state) { currentCurrency: state.metamask.currentCurrency, selectedTokenAddress: state.metamask.selectedTokenAddress, userAddress: selectors.getSelectedAddress(state), - tokenExchangeRates: state.metamask.tokenExchangeRates, + contractExchangeRates: state.metamask.contractExchangeRates, conversionRate: state.metamask.conversionRate, sidebarOpen: state.appState.sidebarOpen, } @@ -25,7 +25,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { setSelectedToken: address => dispatch(actions.setSelectedToken(address)), - updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), hideSidebar: () => dispatch(actions.hideSidebar()), } } @@ -41,15 +40,6 @@ function TokenCell () { } } -TokenCell.prototype.componentWillMount = function () { - const { - updateTokenExchangeRate, - symbol, - } = this.props - - updateTokenExchangeRate(symbol) -} - TokenCell.prototype.render = function () { const { tokenMenuOpen } = this.state const props = this.props @@ -60,7 +50,7 @@ TokenCell.prototype.render = function () { network, setSelectedToken, selectedTokenAddress, - tokenExchangeRates, + contractExchangeRates, conversionRate, hideSidebar, sidebarOpen, @@ -68,15 +58,13 @@ TokenCell.prototype.render = function () { // userAddress, } = props - const pair = `${symbol.toLowerCase()}_eth` - let currentTokenToFiatRate let currentTokenInFiat let formattedFiat = '' - if (tokenExchangeRates[pair]) { + if (contractExchangeRates[address]) { currentTokenToFiatRate = multiplyCurrencies( - tokenExchangeRates[pair].rate, + contractExchangeRates[address], conversionRate ) currentTokenInFiat = conversionUtil(string, { diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 150a3762d..4189cf801 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -6,6 +6,7 @@ const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') const connect = require('react-redux').connect const selectors = require('../selectors') +const log = require('loglevel') function mapStateToProps (state) { return { diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 42c008798..bd4ea80a6 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -9,6 +9,7 @@ const abiDecoder = require('abi-decoder') abiDecoder.addABI(abi) const Identicon = require('./identicon') const contractMap = require('eth-contract-metadata') +const { checksumAddress } = require('../util') const actions = require('../actions') const { conversionUtil, multiplyCurrencies } = require('../conversion-util') @@ -27,7 +28,7 @@ function mapStateToProps (state) { return { tokens: state.metamask.tokens, currentCurrency: getCurrentCurrency(state), - tokenExchangeRates: state.metamask.tokenExchangeRates, + contractExchangeRates: state.metamask.contractExchangeRates, selectedAddressTxList: state.metamask.selectedAddressTxList, } } @@ -74,10 +75,12 @@ TxListItem.prototype.getAddressText = function () { const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) const { name: txDataName, params = [] } = decodedData || {} const { value } = params[0] || {} + const checksummedAddress = checksumAddress(address) + const checksummedValue = checksumAddress(value) let addressText if (txDataName === 'transfer' || address) { - const addressToRender = txDataName === 'transfer' ? value : address + const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}` } else if (isMsg) { addressText = this.context.t('sigRequest') @@ -142,31 +145,29 @@ TxListItem.prototype.getTokenInfo = async function () { ({ decimals, symbol } = await tokenInfoGetter(toAddress)) } - return { decimals, symbol } + return { decimals, symbol, address: toAddress } } TxListItem.prototype.getSendTokenTotal = async function () { const { txParams = {}, conversionRate, - tokenExchangeRates, + contractExchangeRates, currentCurrency, } = this.props const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) const { params = [] } = decodedData || {} const { value } = params[1] || {} - const { decimals, symbol } = await this.getTokenInfo() + const { decimals, symbol, address } = await this.getTokenInfo() const total = calcTokenAmount(value, decimals) - const pair = symbol && `${symbol.toLowerCase()}_eth` - let tokenToFiatRate let totalInFiat - if (tokenExchangeRates[pair]) { + if (contractExchangeRates[address]) { tokenToFiatRate = multiplyCurrencies( - tokenExchangeRates[pair].rate, + contractExchangeRates[address], conversionRate ) @@ -220,7 +221,6 @@ TxListItem.prototype.resubmit = function () { TxListItem.prototype.render = function () { const { transactionStatus, - transactionAmount, onClick, transactionId, dateString, @@ -229,7 +229,6 @@ TxListItem.prototype.render = function () { txParams, } = this.props const { total, fiatTotal, isTokenTx } = this.state - const showFiatTotal = transactionAmount !== '0x0' && fiatTotal return h(`div${className || ''}`, { key: transactionId, @@ -288,7 +287,7 @@ TxListItem.prototype.render = function () { h('span.tx-list-value', total), - showFiatTotal && h('span.tx-list-fiat-value', fiatTotal), + fiatTotal && h('span.tx-list-fiat-value', fiatTotal), ]), ]), diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 740c4a4ab..554febcff 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -11,14 +11,19 @@ const { formatDate } = require('../util') const { showConfTxPage } = require('../actions') const classnames = require('classnames') const { tokenInfoGetter } = require('../token-util') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const { CONFIRM_TRANSACTION_ROUTE } = require('../routes') + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxList) TxList.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(TxList) - - function mapStateToProps (state) { return { txsToRender: selectors.transactionsSelector(state), @@ -96,7 +101,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa transactionNetworkId, transactionSubmittedTime, } = props - const { showConfTxPage } = this.props + const { history } = this.props const opts = { key: transactionId || transactionHash, @@ -116,7 +121,10 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa const isUnapproved = transactionStatus === 'unapproved' if (isUnapproved) { - opts.onClick = () => showConfTxPage({ id: transactionId }) + opts.onClick = () => { + this.props.showConfTxPage({ id: transactionId }) + history.push(CONFIRM_TRANSACTION_ROUTE) + } opts.transactionStatus = this.context.t('notStarted') } else if (transactionHash) { opts.onClick = () => this.view(transactionHash, transactionNetworkId) diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index ca24e813f..263f992c0 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -2,22 +2,27 @@ const Component = require('react').Component const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') -const ethUtil = require('ethereumjs-util') const inherits = require('util').inherits +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const actions = require('../actions') const selectors = require('../selectors') +const { SEND_ROUTE } = require('../routes') +const { checksumAddress: toChecksumAddress } = require('../util') const BalanceComponent = require('./balance-component') const TxList = require('./tx-list') const Identicon = require('./identicon') +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxView) + TxView.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(TxView) - - function mapStateToProps (state) { const sidebarOpen = state.appState.sidebarOpen const isMascara = state.appState.isMascara @@ -27,7 +32,7 @@ function mapStateToProps (state) { const network = state.metamask.network const selectedTokenAddress = state.metamask.selectedTokenAddress const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) + const checksumAddress = toChecksumAddress(selectedAddress) const identity = identities[selectedAddress] return { @@ -69,7 +74,7 @@ TxView.prototype.renderHeroBalance = function () { } TxView.prototype.renderButtons = function () { - const {selectedToken, showModal, showSendPage, showSendTokenPage } = this.props + const {selectedToken, showModal, history } = this.props return !selectedToken ? ( @@ -84,14 +89,14 @@ TxView.prototype.renderButtons = function () { style: { marginLeft: '0.8em', }, - onClick: showSendPage, + onClick: () => history.push(SEND_ROUTE), }, this.context.t('send')), ]) ) : ( h('div.flex-row.flex-center.hero-balance-buttons', [ h('button.btn-primary.hero-balance-button', { - onClick: showSendTokenPage, + onClick: () => history.push(SEND_ROUTE), }, this.context.t('send')), ]) ) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index e6b94ad12..9e430f87b 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -2,8 +2,11 @@ const Component = require('react').Component const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const inherits = require('util').inherits const classnames = require('classnames') +const { checksumAddress } = require('../util') const Identicon = require('./identicon') // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns const Tooltip = require('./tooltip-v2.js') @@ -12,14 +15,17 @@ const actions = require('../actions') const BalanceComponent = require('./balance-component') const TokenList = require('./token-list') const selectors = require('../selectors') +const { ADD_TOKEN_ROUTE } = require('../routes') + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(WalletView) WalletView.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(WalletView) - - function mapStateToProps (state) { return { @@ -97,11 +103,13 @@ WalletView.prototype.render = function () { keyrings, showAccountDetailModal, hideSidebar, - showAddTokenPage, + history, } = this.props // temporary logs + fake extra wallets // console.log('walletview, selectedAccount:', selectedAccount) + const checksummedAddress = checksumAddress(selectedAddress) + const keyring = keyrings.find((kr) => { return kr.accounts.includes(selectedAddress) || kr.accounts.includes(selectedIdentity.address) @@ -130,7 +138,7 @@ WalletView.prototype.render = function () { }, [ h(Identicon, { diameter: 54, - address: selectedAddress, + address: checksummedAddress, }), h('span.account-name', { @@ -153,7 +161,7 @@ WalletView.prototype.render = function () { 'wallet-view__address__pressed': this.state.copyToClipboardPressed, }), onClick: () => { - copyToClipboard(selectedAddress) + copyToClipboard(checksummedAddress) this.setState({ hasCopied: true }) setTimeout(() => this.setState({ hasCopied: false }), 3000) }, @@ -164,7 +172,7 @@ WalletView.prototype.render = function () { this.setState({ copyToClipboardPressed: false }) }, }, [ - `${selectedAddress.slice(0, 4)}...${selectedAddress.slice(-4)}`, + `${checksummedAddress.slice(0, 4)}...${checksummedAddress.slice(-4)}`, h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }), ]), ]), @@ -174,10 +182,7 @@ WalletView.prototype.render = function () { h(TokenList), h('button.btn-primary.wallet-view__add-token-button', { - onClick: () => { - showAddTokenPage() - hideSidebar() - }, + onClick: () => history.push(ADD_TOKEN_ROUTE), }, this.context.t('addToken')), ]) } diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 1070436c3..b71538e31 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -2,8 +2,11 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const actions = require('./actions') const txHelper = require('../lib/tx-helper') +const log = require('loglevel') const PendingTx = require('./components/pending-tx') const SignatureRequest = require('./components/signature-request') @@ -11,19 +14,21 @@ const SignatureRequest = require('./components/signature-request') // const PendingPersonalMsg = require('./components/pending-personal-msg') // const PendingTypedMsg = require('./components/pending-typed-msg') const Loading = require('./components/loading') +const { DEFAULT_ROUTE } = require('./routes') -// const contentDivider = h('div', { -// style: { -// marginLeft: '16px', -// marginRight: '16px', -// height:'1px', -// background:'#E7E7E7', -// }, -// }) - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(ConfirmTxScreen) function mapStateToProps (state) { + const { metamask } = state + const { + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, + } = metamask + return { identities: state.metamask.identities, accounts: state.metamask.accounts, @@ -40,6 +45,10 @@ function mapStateToProps (state) { currentCurrency: state.metamask.currentCurrency, blockGasLimit: state.metamask.currentBlockGasLimit, computedBalances: state.metamask.computedBalances, + unapprovedMsgCount, + unapprovedPersonalMsgCount, + unapprovedTypedMessagesCount, + send: state.metamask.send, selectedAddressTxList: state.metamask.selectedAddressTxList, } } @@ -49,11 +58,35 @@ function ConfirmTxScreen () { Component.call(this) } +ConfirmTxScreen.prototype.getUnapprovedMessagesTotal = function () { + const { + unapprovedMsgCount = 0, + unapprovedPersonalMsgCount = 0, + unapprovedTypedMessagesCount = 0, + } = this.props + + return unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount +} + +ConfirmTxScreen.prototype.componentDidMount = function () { + const { + unapprovedTxs = {}, + network, + send, + } = this.props + const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) + + if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) { + this.props.history.push(DEFAULT_ROUTE) + } +} + ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) { const { - unapprovedTxs, + unapprovedTxs = {}, network, selectedAddressTxList, + send, } = this.props const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) @@ -61,8 +94,9 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) { const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) - if (prevTx.status === 'dropped' && unconfTxList.length === 0) { - this.goHome({}) + if (unconfTxList.length === 0 && + (prevTx.status === 'dropped' || !send.to && this.getUnapprovedMessagesTotal() === 0)) { + this.props.history.push(DEFAULT_ROUTE) } } @@ -103,7 +137,6 @@ ConfirmTxScreen.prototype.render = function () { */ log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading) return currentTxView({ // Properties @@ -152,6 +185,7 @@ function currentTxView (opts) { // return h(PendingTypedMsg, opts) // } } + return h(Loading) } @@ -163,6 +197,7 @@ ConfirmTxScreen.prototype.buyEth = function (address, event) { ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { this.stopPropagation(event) this.props.dispatch(actions.updateAndApproveTx(txData)) + .then(() => this.props.history.push(DEFAULT_ROUTE)) } ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { @@ -182,7 +217,7 @@ ConfirmTxScreen.prototype.signMessage = function (msgData, event) { var params = msgData.msgParams params.metamaskId = msgData.id this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) + return this.props.dispatch(actions.signMsg(params)) } ConfirmTxScreen.prototype.stopPropagation = function (event) { @@ -196,7 +231,7 @@ ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { var params = msgData.msgParams params.metamaskId = msgData.id this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) + return this.props.dispatch(actions.signPersonalMsg(params)) } ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) { @@ -204,25 +239,25 @@ ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) { var params = msgData.msgParams params.metamaskId = msgData.id this.stopPropagation(event) - this.props.dispatch(actions.signTypedMsg(params)) + return this.props.dispatch(actions.signTypedMsg(params)) } ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { log.info('canceling message') this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) + return this.props.dispatch(actions.cancelMsg(msgData)) } ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { log.info('canceling personal message') this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) + return this.props.dispatch(actions.cancelPersonalMsg(msgData)) } ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { log.info('canceling typed message') this.stopPropagation(event) - this.props.dispatch(actions.cancelTypedMsg(msgData)) + return this.props.dispatch(actions.cancelTypedMsg(msgData)) } ConfirmTxScreen.prototype.goHome = function (event) { diff --git a/ui/app/css/index.scss b/ui/app/css/index.scss index 445c819ff..c068028f8 100644 --- a/ui/app/css/index.scss +++ b/ui/app/css/index.scss @@ -6,9 +6,15 @@ */ @import './itcss/settings/index.scss'; + @import './itcss/tools/index.scss'; + @import './itcss/generic/index.scss'; + @import './itcss/base/index.scss'; + @import './itcss/objects/index.scss'; + @import './itcss/components/index.scss'; + @import './itcss/trumps/index.scss'; diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index ffd43ecbf..959eb9d15 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -52,6 +52,8 @@ @import './editable-label.scss'; +@import './pages/index.scss'; + @import './new-account.scss'; @import './tooltip.scss'; diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss index 15009c1e6..a92fffec5 100644 --- a/ui/app/css/itcss/components/loading-overlay.scss +++ b/ui/app/css/itcss/components/loading-overlay.scss @@ -1,13 +1,14 @@ .loading-overlay { - left: 0px; + left: 0; z-index: 50; position: absolute; flex-direction: column; display: flex; justify-content: center; align-items: center; + flex: 1 1 auto; width: 100%; - background: rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, .8); @media screen and (max-width: 575px) { margin-top: 56px; @@ -18,4 +19,11 @@ margin-top: 75px; height: calc(100% - 75px); } + + &--full-screen { + position: fixed; + height: 100vh; + width: 100vw; + margin-top: 0; + } } diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index aa7fed956..293579058 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -35,13 +35,14 @@ font-size: 18px; line-height: 24px; text-align: center; + cursor: pointer; } &__tab:first-of-type { margin-right: 20px; } - &__unselected:hover { + &__tab:hover { color: $black; border-bottom: none; } @@ -49,9 +50,9 @@ &__selected { color: $curious-blue; border-bottom: 3px solid $curious-blue; + cursor: initial; } } - } .new-account-import-disclaimer { diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss new file mode 100644 index 000000000..82446fd7a --- /dev/null +++ b/ui/app/css/itcss/components/pages/index.scss @@ -0,0 +1 @@ +@import './unlock.scss'; diff --git a/ui/app/css/itcss/components/pages/unlock.scss b/ui/app/css/itcss/components/pages/unlock.scss new file mode 100644 index 000000000..5d438377b --- /dev/null +++ b/ui/app/css/itcss/components/pages/unlock.scss @@ -0,0 +1,9 @@ +.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/sections.scss b/ui/app/css/itcss/components/sections.scss index 388aea175..ace46bd8a 100644 --- a/ui/app/css/itcss/components/sections.scss +++ b/ui/app/css/itcss/components/sections.scss @@ -17,6 +17,12 @@ textarea.twelve-word-phrase { resize: none; } +.initialize-screen { + width: 100%; + z-index: $main-container-z-index; + background: #f7f7f7; +} + .initialize-screen hr { width: 60px; margin: 12px; diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js index 4ab5f06c0..3df040922 100644 --- a/ui/app/first-time/init-menu.js +++ b/ui/app/first-time/init-menu.js @@ -1,6 +1,5 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component +const { EventEmitter } = require('events') +const { Component } = require('react') const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') @@ -8,205 +7,220 @@ const Mascot = require('../components/mascot') const actions = require('../actions') const Tooltip = require('../components/tooltip') const getCaretCoordinates = require('textarea-caret') -const environmentType = require('../../../app/scripts/lib/environment-type') +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/config').enums -let isSubmitting = false +class InitializeMenuScreen extends Component { + constructor (props) { + super(props) -InitializeMenuScreen.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, + this.animationEventEmitter = new EventEmitter() + this.state = { + warning: null, + } } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) + componentWillMount () { + const { isInitialized, isUnlocked, history } = this.props + if (isInitialized || isUnlocked) { + history.push(DEFAULT_ROUTE) + } } -} -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ + componentDidMount () { + document.getElementById('password-box').focus() + } - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), + render () { + const { warning } = this.state - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, this.context.t('appName')), + return ( + h('.initialize-screen.flex-column.flex-center', [ + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), - h('div', [ - h('h3', { + h('h1', { style: { - fontSize: '0.8em', + fontSize: '1.3em', + textTransform: 'uppercase', color: '#7F8082', - display: 'inline', + marginBottom: 10, }, - }, this.context.t('encryptNewDen')), + }, this.context.t('appName')), - h(Tooltip, { - title: this.context.t('denExplainer'), - }, [ - h('i.fa.fa-question-circle.pointer', { + h('div', [ + h('h3', { style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', }, - }), + }, this.context.t('encryptNewDen')), + + h(Tooltip, { + title: this.context.t('denExplainer'), + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: this.context.t('newPassword'), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: this.context.t('confirmPassword'), - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, this.context.t('createDen')), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), + + h('span.error.in-progress-notification', warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: this.context.t('newPassword'), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: this.context.t('confirmPassword'), + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', + width: 260, + marginTop: 16, }, - }, this.context.t('importDen')), - ]), + }), + - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showOldUI.bind(this), + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), style: { - fontSize: '0.8em', - color: '#aeaeae', - textDecoration: 'underline', - marginTop: '32px', + margin: 12, }, - }, 'Use classic interface'), - ]), + }, this.context.t('createDen')), - ]) - ) -} + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.showRestoreVault(), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, this.context.t('importDen')), + ]), -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showOldUI.bind(this), + style: { + fontSize: '0.8em', + color: '#aeaeae', + textDecoration: 'underline', + marginTop: '32px', + }, + }, 'Use classic interface'), + ]), + + ]) + ) } -} -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} + createVaultOnEnter (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } + } -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.markPasswordForgotten()) - if (environmentType() === 'popup') { - global.platform.openExtensionInBrowser() + createNewVaultAndKeychain () { + const { history } = this.props + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + this.setState({ warning: null }) + + if (password.length < 8) { + this.setState({ warning: this.context.t('passwordShort') }) + return + } + + if (password !== passwordConfirm) { + this.setState({ warning: this.context.t('passwordMismatch') }) + return + } + + this.props.createNewVaultAndKeychain(password) + .then(() => history.push(DEFAULT_ROUTE)) } -} -InitializeMenuScreen.prototype.showOldUI = function () { - this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) - .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))) -} + 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, + }) + } -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value + showRestoreVault () { + this.props.markPasswordForgotten() + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser() + } - if (password.length < 8) { - this.warning = this.context.t('passwordShort') - this.props.dispatch(actions.displayWarning(this.warning)) - return + this.props.history.push(RESTORE_VAULT_ROUTE) } - if (password !== passwordConfirm) { - this.warning = this.context.t('passwordMismatch') - this.props.dispatch(actions.displayWarning(this.warning)) - return + + showOldUI () { + this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) + .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))) } +} + +InitializeMenuScreen.propTypes = { + history: PropTypes.object, + isInitialized: PropTypes.bool, + isUnlocked: PropTypes.bool, + createNewVaultAndKeychain: PropTypes.func, + markPasswordForgotten: PropTypes.func, + dispatch: PropTypes.func, +} - if (!isSubmitting) { - isSubmitting = true - this.props.dispatch(actions.createNewVaultAndKeychain(password)) +InitializeMenuScreen.contextTypes = { + t: PropTypes.func, +} + +const mapStateToProps = state => { + const { metamask: { isInitialized, isUnlocked } } = state + + return { + isInitialized, + isUnlocked, } } -InitializeMenuScreen.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, - }) +const mapDispatchToProps = dispatch => { + return { + createNewVaultAndKeychain: password => dispatch(actions.createNewVaultAndKeychain(password)), + markPasswordForgotten: () => dispatch(actions.markPasswordForgotten()), + } } + +module.exports = connect(mapStateToProps, mapDispatchToProps)(InitializeMenuScreen) diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js index fe6d62c67..4ef618018 100644 --- a/ui/app/i18n-provider.js +++ b/ui/app/i18n-provider.js @@ -1,6 +1,8 @@ const { Component } = require('react') const connect = require('react-redux').connect const PropTypes = require('prop-types') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const t = require('../i18n-helper').getMessage class I18nProvider extends Component { @@ -32,5 +34,8 @@ const mapStateToProps = state => { } } -module.exports = connect(mapStateToProps)(I18nProvider) +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(I18nProvider) diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js index 02183f096..eb588415f 100644 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -4,12 +4,21 @@ const PropTypes = require('prop-types') const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('../../../actions') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const { + DEFAULT_ROUTE, + INITIALIZE_BACKUP_PHRASE_ROUTE, +} = require('../../../routes') RevealSeedConfirmation.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(RevealSeedConfirmation) inherits(RevealSeedConfirmation, Component) @@ -109,6 +118,8 @@ RevealSeedConfirmation.prototype.componentDidMount = function () { RevealSeedConfirmation.prototype.goHome = function () { this.props.dispatch(actions.showConfigPage(false)) + this.props.dispatch(actions.confirmSeedWords()) + .then(() => this.props.history.push(DEFAULT_ROUTE)) } // create vault @@ -123,4 +134,5 @@ RevealSeedConfirmation.prototype.checkConfirmation = function (event) { RevealSeedConfirmation.prototype.revealSeedWords = function () { var password = document.getElementById('password-box').value this.props.dispatch(actions.requestRevealSeed(password)) + .then(() => this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)) } diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js index 38ad14adb..913d20505 100644 --- a/ui/app/keychains/hd/restore-vault.js +++ b/ui/app/keychains/hd/restore-vault.js @@ -4,6 +4,7 @@ const PersistentForm = require('../../../lib/persistent-form') const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('../../actions') +const log = require('loglevel') RestoreVaultScreen.contextTypes = { t: PropTypes.func, diff --git a/ui/app/main-container.js b/ui/app/main-container.js index eed4bd164..c305687ea 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -2,8 +2,9 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const AccountAndTransactionDetails = require('./account-and-transaction-details') -const Settings = require('./settings') -const UnlockScreen = require('./unlock') +const Settings = require('./components/pages/settings') +const UnlockScreen = require('./components/pages/unlock') +const log = require('loglevel') module.exports = MainContainer diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 74a0f9299..2b39eb8db 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -1,6 +1,7 @@ const extend = require('xtend') const actions = require('../actions') const txHelper = require('../../lib/tx-helper') +const log = require('loglevel') module.exports = reduceApp diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 6d0a5bb10..5f965fbe0 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -1,7 +1,8 @@ const extend = require('xtend') const actions = require('../actions') const MetamascaraPlatform = require('../../../app/scripts/platforms/window') -const environmentType = require('../../../app/scripts/lib/environment-type') +const { getEnvironmentType } = require('../../../app/scripts/lib/util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums') const { OLD_UI_NETWORK_TYPE } = require('../../../app/scripts/config').enums module.exports = reduceMetamask @@ -15,7 +16,7 @@ function reduceMetamask (state, action) { isUnlocked: false, isAccountMenuOpen: false, isMascara: window.platform instanceof MetamascaraPlatform, - isPopup: environmentType() === 'popup', + isPopup: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP, rpcTarget: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, @@ -24,6 +25,7 @@ function reduceMetamask (state, action) { frequentRpcList: [], addressBook: [], selectedTokenAddress: null, + contractExchangeRates: {}, tokenExchangeRates: {}, tokens: [], send: { @@ -176,15 +178,6 @@ function reduceMetamask (state, action) { conversionDate: action.value.conversionDate, }) - case actions.UPDATE_TOKEN_EXCHANGE_RATE: - const { payload: { pair, marketinfo } } = action - return extend(metamaskState, { - tokenExchangeRates: { - ...metamaskState.tokenExchangeRates, - [pair]: marketinfo, - }, - }) - case actions.UPDATE_TOKENS: return extend(metamaskState, { tokens: action.newTokens, @@ -358,7 +351,7 @@ function reduceMetamask (state, action) { welcomeScreenSeen: true, }) - case action.SET_CURRENT_LOCALE: + case actions.SET_CURRENT_LOCALE: return extend(metamaskState, { currentLocale: action.value, }) diff --git a/ui/app/root.js b/ui/app/root.js index 21d6d1829..09deae1b1 100644 --- a/ui/app/root.js +++ b/ui/app/root.js @@ -1,22 +1,23 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider +const { Component } = require('react') +const PropTypes = require('prop-types') +const { Provider } = require('react-redux') const h = require('react-hyperscript') const SelectedApp = require('./select-app') -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( +class Root extends Component { + render () { + const { store } = this.props - h(Provider, { - store: this.props.store, - }, [ - h(SelectedApp), - ]) + return ( + h(Provider, { store }, [ + h(SelectedApp), + ]) + ) + } +} - ) +Root.propTypes = { + store: PropTypes.object, } + +module.exports = Root diff --git a/ui/app/routes.js b/ui/app/routes.js new file mode 100644 index 000000000..4b3f8f4d8 --- /dev/null +++ b/ui/app/routes.js @@ -0,0 +1,49 @@ +const DEFAULT_ROUTE = '/' +const UNLOCK_ROUTE = '/unlock' +const SETTINGS_ROUTE = '/settings' +const INFO_ROUTE = '/settings/info' +const REVEAL_SEED_ROUTE = '/seed' +const CONFIRM_SEED_ROUTE = '/confirm-seed' +const RESTORE_VAULT_ROUTE = '/restore-vault' +const ADD_TOKEN_ROUTE = '/add-token' +const NEW_ACCOUNT_ROUTE = '/new-account' +const IMPORT_ACCOUNT_ROUTE = '/new-account/import' +const SEND_ROUTE = '/send' +const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' +const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request' +const NOTICE_ROUTE = '/notice' +const WELCOME_ROUTE = '/welcome' +const INITIALIZE_ROUTE = '/initialize' +const INITIALIZE_CREATE_PASSWORD_ROUTE = '/initialize/create-password' +const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/import-account' +const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/import-with-seed-phrase' +const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/unique-image' +const INITIALIZE_NOTICE_ROUTE = '/initialize/notice' +const INITIALIZE_BACKUP_PHRASE_ROUTE = '/initialize/backup-phrase' +const INITIALIZE_CONFIRM_SEED_ROUTE = '/initialize/confirm-phrase' + +module.exports = { + DEFAULT_ROUTE, + UNLOCK_ROUTE, + SETTINGS_ROUTE, + INFO_ROUTE, + REVEAL_SEED_ROUTE, + CONFIRM_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + ADD_TOKEN_ROUTE, + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + SEND_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + NOTICE_ROUTE, + SIGNATURE_REQUEST_ROUTE, + WELCOME_ROUTE, + INITIALIZE_ROUTE, + INITIALIZE_CREATE_PASSWORD_ROUTE, + INITIALIZE_IMPORT_ACCOUNT_ROUTE, + INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, + INITIALIZE_UNIQUE_IMAGE_ROUTE, + INITIALIZE_NOTICE_ROUTE, + INITIALIZE_BACKUP_PHRASE_ROUTE, + INITIALIZE_CONFIRM_SEED_ROUTE, +} diff --git a/ui/app/select-app.js b/ui/app/select-app.js index 101eb1cf6..d1565e2fb 100644 --- a/ui/app/select-app.js +++ b/ui/app/select-app.js @@ -2,6 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') +const { HashRouter } = require('react-router-dom') const App = require('./app') const OldApp = require('../../old-ui/app/app') const { autoAddToBetaUI } = require('./selectors') @@ -63,7 +64,12 @@ SelectedApp.prototype.render = function () { // const Selected = betaUI || isMascara || firstTime ? App : OldApp const { betaUI, isMascara } = this.props - const Selected = betaUI || isMascara ? h(I18nProvider, [ h(App) ]) : h(OldApp) - return Selected + return betaUI || isMascara + ? h(HashRouter, { + hashType: 'noslash', + }, [ + h(I18nProvider, [ h(App) ]), + ]) + : h(OldApp) } diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 2bdc39004..60cc264da 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -62,22 +62,15 @@ function getSelectedToken (state) { } function getSelectedTokenExchangeRate (state) { - const tokenExchangeRates = state.metamask.tokenExchangeRates + const contractExchangeRates = state.metamask.contractExchangeRates const selectedToken = getSelectedToken(state) || {} - const { symbol = '' } = selectedToken - - const pair = `${symbol.toLowerCase()}_eth` - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} - - return tokenExchangeRate + const { address } = selectedToken + return contractExchangeRates[address] || 0 } -function getTokenExchangeRate (state, tokenSymbol) { - const pair = `${tokenSymbol.toLowerCase()}_eth` - const tokenExchangeRates = state.metamask.tokenExchangeRates - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} - - return tokenExchangeRate +function getTokenExchangeRate (state, address) { + const contractExchangeRates = state.metamask.contractExchangeRates + return contractExchangeRates[address] || 0 } function conversionRateSelector (state) { diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 094743ff0..30d3d3152 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -30,6 +30,7 @@ const { getGasTotal, } = require('./components/send/send-utils') const { isValidAddress } = require('./util') +const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes') SendTransactionScreen.contextTypes = { t: PropTypes.func, @@ -87,17 +88,6 @@ SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) { } SendTransactionScreen.prototype.componentWillMount = function () { - const { - updateTokenExchangeRate, - selectedToken = {}, - } = this.props - - const { symbol } = selectedToken || {} - - if (symbol) { - updateTokenExchangeRate(symbol) - } - this.updateGas() } @@ -182,7 +172,7 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) { } SendTransactionScreen.prototype.renderHeader = function () { - const { selectedToken, clearSend, goHome } = this.props + const { selectedToken, clearSend, history } = this.props return h('div.page-container__header', [ @@ -193,7 +183,7 @@ SendTransactionScreen.prototype.renderHeader = function () { h('div.page-container__header-close', { onClick: () => { clearSend() - goHome() + history.push(DEFAULT_ROUTE) }, }), @@ -495,12 +485,12 @@ SendTransactionScreen.prototype.renderForm = function () { SendTransactionScreen.prototype.renderFooter = function () { const { - goHome, clearSend, gasTotal, tokenBalance, selectedToken, errors: { amount: amountError, to: toError }, + history, } = this.props const missingTokenBalance = selectedToken && !tokenBalance @@ -510,7 +500,7 @@ SendTransactionScreen.prototype.renderFooter = function () { h('button.btn-secondary--lg.page-container__footer-button', { onClick: () => { clearSend() - goHome() + history.push(DEFAULT_ROUTE) }, }, this.context.t('cancel')), h('button.btn-primary--lg.page-container__footer-button', { @@ -621,7 +611,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (editingTransactionId) { const editedTx = this.getEditedTx() - updateTx(editedTx) } else { @@ -645,4 +634,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) { ? signTokenTx(selectedToken.address, to, amount, txParams) : signTx(txParams) } + + this.props.history.push(CONFIRM_TRANSACTION_ROUTE) } diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 84d8b7e7c..1325656af 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -7,7 +7,8 @@ const actions = require('./actions') const getCaretCoordinates = require('textarea-caret') const EventEmitter = require('events').EventEmitter const { OLD_UI_NETWORK_TYPE } = require('../../app/scripts/config').enums -const environmentType = require('../../app/scripts/lib/environment-type') +const { getEnvironmentType } = require('../../app/scripts/lib/util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums') const Mascot = require('./components/mascot') @@ -77,7 +78,7 @@ UnlockScreen.prototype.render = function () { h('p.pointer', { onClick: () => { this.props.dispatch(actions.markPasswordForgotten()) - if (environmentType() === 'popup') { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { global.platform.openExtensionInBrowser() } }, diff --git a/ui/app/util.js b/ui/app/util.js index bbe2bb09e..8e9390dfb 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -57,6 +57,7 @@ module.exports = { isInvalidChecksumAddress, allNull, getTokenAddressFromTokenObject, + checksumAddress, } function valuesFor (obj) { @@ -67,7 +68,7 @@ function valuesFor (obj) { function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) + let checked = checksumAddress(address) if (!includeHex) { checked = ethUtil.stripHexPrefix(checked) } @@ -76,7 +77,7 @@ function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includ function miniAddressSummary (address) { if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) + var checked = checksumAddress(address) return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' } @@ -287,3 +288,13 @@ function allNull (obj) { function getTokenAddressFromTokenObject (token) { return Object.values(token)[0].address.toLowerCase() } + +/** + * Safely checksumms a potentially-null address + * + * @param {String} [address] - address to checksum + * @returns {String} - checksummed address + */ +function checksumAddress (address) { + return address ? ethUtil.toChecksumAddress(address) : '' +} diff --git a/ui/app/welcome-screen.js b/ui/app/welcome-screen.js index cdbb6dba8..2fa244d9f 100644 --- a/ui/app/welcome-screen.js +++ b/ui/app/welcome-screen.js @@ -3,21 +3,35 @@ import h from 'react-hyperscript' import { Component } from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' import {closeWelcomeScreen} from './actions' import Mascot from './components/mascot' +import { INITIALIZE_CREATE_PASSWORD_ROUTE } from './routes' class WelcomeScreen extends Component { static propTypes = { closeWelcomeScreen: PropTypes.func.isRequired, + welcomeScreenSeen: PropTypes.bool, + history: PropTypes.object, } - constructor(props) { + constructor (props) { super(props) this.animationEventEmitter = new EventEmitter() } + componentWillMount () { + const { history, welcomeScreenSeen } = this.props + + if (welcomeScreenSeen) { + history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) + } + } + initiateAccountCreation = () => { this.props.closeWelcomeScreen() + this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) } render () { @@ -48,9 +62,18 @@ class WelcomeScreen extends Component { } } -export default connect( - null, - dispatch => ({ - closeWelcomeScreen: () => dispatch(closeWelcomeScreen()), - }) +const mapStateToProps = ({ metamask: { welcomeScreenSeen } }) => { + return { + welcomeScreenSeen, + } +} + +export default compose( + withRouter, + connect( + mapStateToProps, + dispatch => ({ + closeWelcomeScreen: () => dispatch(closeWelcomeScreen()), + }) + ) )(WelcomeScreen) |