diff options
Diffstat (limited to 'ui/app')
45 files changed, 2589 insertions, 1044 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index aae037a2d..4ac553812 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -349,7 +349,7 @@ function transitionBackward () { } function confirmSeedWords () { - return (dispatch) => { + return dispatch => { dispatch(actions.showLoadingIndication()) log.debug(`background.clearSeedWordCache`) return new Promise((resolve, reject) => { @@ -357,7 +357,7 @@ function confirmSeedWords () { dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.displayWarning(err.message)) - reject(err) + return reject(err) } log.info('Seed word cache cleared. ' + account) @@ -573,35 +573,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) + }) }) } } @@ -611,16 +623,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) + }) }) } } @@ -800,17 +818,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) + }) }) } } @@ -838,29 +863,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) + }) }) } } @@ -1251,12 +1324,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) } }) }) @@ -1775,7 +1849,7 @@ function forceUpdateMetamaskState (dispatch) { } dispatch(actions.updateMetamaskState(newState)) - resolve() + resolve(newState) }) }) } diff --git a/ui/app/app.js b/ui/app/app.js index 0b7a7a1e0..75c3febb0 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -1,62 +1,421 @@ -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') -// 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 } = this.props + + if (!currentCurrency) { + setCurrentCurrencyToUSD() + } + } + + 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(), -module.exports = connect(mapStateToProps, mapDispatchToProps)(App) + // network dropdown + h(NetworkDropdown, { + provider, + frequentRpcList, + }, []), + h(AccountMenu), -inherits(App, Component) -function App () { Component.call(this) } + (isLoading || isLoadingNetwork) && h(Loading, { + loadingMessage: loadMessage, + }), + + // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, 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, + }), + ]), + ]), + ]), + ]), + + !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'), + ]), + + ]) + ) + } + + renderLoadingIndicator ({ isLoading, isLoadingNetwork, loadMessage }) { + const { isMascara } = this.props + + return isMascara + ? null + : h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }) + } + + 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, +} function mapStateToProps (state) { + const { appState, metamask } = state + const { + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + } = appState + const { identities, accounts, @@ -65,17 +424,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 +450,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, @@ -120,479 +488,11 @@ function mapDispatchToProps (dispatch, ownProps) { } } -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-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/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..7857a2a99 --- /dev/null +++ b/ui/app/components/pages/home.js @@ -0,0 +1,332 @@ +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') + +// 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..d57894e00 --- /dev/null +++ b/ui/app/components/pages/keychains/restore-vault.js @@ -0,0 +1,177 @@ +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') + +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..eb9be66e6 --- /dev/null +++ b/ui/app/components/pages/settings/info.js @@ -0,0 +1,112 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +class Info 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')), + ]), + ]), + ]) + ) + } + + 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', '4.0.0'), + ]), + 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..b3320da21 --- /dev/null +++ b/ui/app/components/pages/unlock.js @@ -0,0 +1,193 @@ +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 environmentType = require('../../../../app/scripts/lib/environment-type') +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 (environmentType() === '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..3ee614a2a 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') @@ -23,12 +25,16 @@ const SenderToRecipient = require('../sender-to-recipient') const NetworkDisplay = require('../network-display') 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 +78,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 +275,14 @@ ConfirmSendEther.prototype.getData = function () { } } +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, @@ -328,7 +338,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', }, @@ -506,7 +516,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 +556,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 +644,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..6942f9b51 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') @@ -33,12 +35,16 @@ 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) { @@ -147,6 +153,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, @@ -406,7 +418,6 @@ ConfirmSendToken.prototype.renderErrorMessage = function (message) { } ConfirmSendToken.prototype.render = function () { - const { editTransaction } = this.props const txMeta = this.gatherTxMeta() const { from: { @@ -433,7 +444,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 +524,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 +579,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/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 08c26a91f..aca1a5a0a 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) @@ -79,7 +84,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/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..80aac35c4 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -4,20 +4,25 @@ 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 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 @@ -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..e3e1b8903 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -2,6 +2,8 @@ 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 Identicon = require('./identicon') @@ -12,14 +14,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,7 +102,7 @@ WalletView.prototype.render = function () { keyrings, showAccountDetailModal, hideSidebar, - showAddTokenPage, + history, } = this.props // temporary logs + fake extra wallets // console.log('walletview, selectedAccount:', selectedAccount) @@ -174,10 +179,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..fee7cd36f 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -2,6 +2,8 @@ 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') @@ -11,19 +13,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 +44,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 +57,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 +93,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 +136,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 +184,7 @@ function currentTxView (opts) { // return h(PendingTypedMsg, opts) // } } + return h(Loading) } @@ -163,6 +196,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 +216,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 +230,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 +238,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/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..692b01c8c 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,219 @@ const Mascot = require('../components/mascot') const actions = require('../actions') const Tooltip = require('../components/tooltip') const getCaretCoordinates = require('textarea-caret') +const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes') const environmentType = require('../../../app/scripts/lib/environment-type') 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 (environmentType() === '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/main-container.js b/ui/app/main-container.js index eed4bd164..6dd4ff151 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -2,8 +2,8 @@ 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') module.exports = MainContainer 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/send-v2.js b/ui/app/send-v2.js index d608957c8..9c540671b 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') import PageContainer from './components/page-container/page-container.component' import SendHeader from './components/send_/send-header/send-header.container' @@ -186,6 +187,25 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) { } } +SendTransactionScreen.prototype.renderHeader = function () { + const { selectedToken, clearSend, history } = this.props + + return h('div.page-container__header', [ + + h('div.page-container__title', selectedToken ? this.context.t('sendTokens') : this.context.t('sendETH')), + + h('div.page-container__subtitle', this.context.t('onlySendToEtherAddress')), + + h('div.page-container__header-close', { + onClick: () => { + clearSend() + history.push(DEFAULT_ROUTE) + }, + }), + + ]) +} + SendTransactionScreen.prototype.renderErrorMessage = function (errorType) { const { errors } = this.props const errorMessage = errors[errorType] @@ -478,12 +498,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 @@ -492,7 +512,7 @@ SendTransactionScreen.prototype.renderFooter = function () { return h(PageContainerFooter, { onCancel: () => { clearSend() - goHome() + history.push(DEFAULT_ROUTE) }, onSubmit: e => this.onSubmit(e), disabled: !noErrors || !gasTotal || missingTokenBalance, @@ -600,7 +620,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (editingTransactionId) { const editedTx = this.getEditedTx() - updateTx(editedTx) } else { @@ -624,4 +643,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/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) |