diff options
Diffstat (limited to 'ui')
40 files changed, 1976 insertions, 1464 deletions
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 0da435298..85951f512 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -71,8 +71,8 @@ AccountDetailScreen.prototype.tabSections = function () { { content: 'Sent', key: 'history' }, { content: 'Tokens', key: 'tokens' }, ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { + selectedTab: currentAccountTab || 'history', + onSelect: key => { this.props.dispatch(actions.setCurrentAccountTab(key)) }, }), diff --git a/ui/app/actions.js b/ui/app/actions.js index ed0518184..51cc4dfbf 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -266,14 +266,18 @@ function tryUnlockMetamask (password) { dispatch(actions.showLoadingIndication()) dispatch(actions.unlockInProgress()) log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } + + return new Promise((resolve, reject) => { + background.submitPassword(password, err => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + reject(err) + } else { + dispatch(actions.transitionForward()) + return forceUpdateMetamaskState(dispatch).then(resolve) + } + }) }) } } @@ -291,7 +295,7 @@ function transitionBackward () { } function confirmSeedWords () { - return (dispatch) => { + return dispatch => { dispatch(actions.showLoadingIndication()) log.debug(`background.clearSeedWordCache`) return new Promise((resolve, reject) => { @@ -299,7 +303,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) @@ -344,14 +348,14 @@ function createNewVaultAndKeychain (password) { return reject(err) } log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { if (err) { dispatch(actions.displayWarning(err.message)) return reject(err) } dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - resolve() + forceUpdateMetamaskState(dispatch).then(resolve) }) }) }) @@ -696,16 +700,23 @@ 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)) + + 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) + }) }) } } @@ -751,10 +762,13 @@ function cancelTypedMsg (msgData) { } function cancelTx (txData) { - return (dispatch) => { + return dispatch => { log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id, () => { - dispatch(actions.completedTx(txData.id)) + return new Promise((resolve, reject) => { + background.cancelTransaction(txData.id, () => { + dispatch(actions.completedTx(txData.id)) + resolve(txData) + }) }) } } @@ -1585,11 +1599,17 @@ function callBackgroundThenUpdate (method, ...args) { function forceUpdateMetamaskState (dispatch) { log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) + + return new Promise((resolve, reject) => { + background.getState((err, newState) => { + if (err) { + reject(err) + return dispatch(actions.displayWarning(err.message)) + } + + dispatch(actions.updateMetamaskState(newState)) + resolve(newState) + }) }) } diff --git a/ui/app/app.js b/ui/app/app.js index 1f40eccbe..168ec6559 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -1,6 +1,7 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect +const { Component } = require('react') +const { connect } = require('react-redux') +const { Switch, Route, Redirect, withRouter } = require('react-router-dom') +const { compose } = require('recompose') const h = require('react-hyperscript') const actions = require('./actions') // mascara @@ -14,23 +15,25 @@ 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 Authenticated = require('./components/pages/authenticated') +const Settings = require('./components/pages/settings') +const UnlockPage = require('./components/pages/unauthenticated/unlock') +const RestoreVaultPage = require('./components/pages/keychains/restore-vault') +const RevealSeedPage = require('./components/pages/keychains/reveal-seed') +const AddTokenPage = require('./components/pages/add-token') +const ImportAccountPage = require('./components/pages/import-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') @@ -39,482 +42,565 @@ const QrView = require('./components/qr-code') // Global Modals const Modal = require('./components/modals/index').Modal -module.exports = connect(mapStateToProps, mapDispatchToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - const { - identities, - accounts, - address, - keyrings, - isInitialized, - noActiveNotices, - seedWords, - } = state.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, - 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), - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - currentCurrency: state.metamask.currentCurrency, - - // state needed to get account dropdown temporarily rendering from app bar - identities, - selected, - keyrings, +// Routes +const { + DEFAULT_ROUTE, + UNLOCK_ROUTE, + SETTINGS_ROUTE, + REVEAL_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + ADD_TOKEN_ROUTE, + IMPORT_ACCOUNT_ROUTE, + SEND_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + INITIALIZE_MENU_ROUTE, + NOTICE_ROUTE, +} = require('./routes') + +class App extends Component { + constructor (props) { + super(props) + + this.renderPrimary = this.renderPrimary.bind(this) } -} -function mapDispatchToProps (dispatch, ownProps) { - return { - dispatch, - hideSidebar: () => dispatch(actions.hideSidebar()), - showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), - hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), - toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), + componentWillMount () { + const { currentCurrency, setCurrentCurrency } = this.props + + if (!currentCurrency) { + setCurrentCurrencyToUSD() + } } -} -App.prototype.componentWillMount = function () { - if (!this.props.currentCurrency) { - this.props.setCurrentCurrencyToUSD() + renderRoutes () { + const exact = true + + return ( + h(Switch, [ + h(Route, { path: INITIALIZE_MENU_ROUTE, exact, component: InitializeMenuScreen }), + h(Route, { path: UNLOCK_ROUTE, exact, component: UnlockPage }), + h(Route, { path: SETTINGS_ROUTE, component: Settings }), + h(Route, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), + h(Route, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), + h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, exact, component: ConfirmTxScreen }), + h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), + h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedPage }), + h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), + h(Authenticated, { path: IMPORT_ACCOUNT_ROUTE, exact, component: ImportAccountPage }), + h(Authenticated, { path: DEFAULT_ROUTE, exact, component: this.renderPrimary }), + ]) + ) } -} -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - log.debug('Main ui render function') - - return ( - h('.flex-column.full-height', { - style: { - overflowX: 'hidden', - position: 'relative', - alignItems: 'center', - }, - }, [ + render () { + var props = this.props + const { isLoading, loadingMessage, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + log.debug('Main ui render function') - // global modal - h(Modal, {}, []), + return ( + h('.flex-column.full-height', { + style: { + overflowX: 'hidden', + position: 'relative', + alignItems: 'center', + }, + }, [ - // app bar - this.renderAppBar(), + // global modal + h(Modal, {}, []), - // sidebar - this.renderSidebar(), + // app bar + this.renderAppBar(), - // network dropdown - h(NetworkDropdown, { - provider: this.props.provider, - frequentRpcList: this.props.frequentRpcList, - }, []), + // sidebar + this.renderSidebar(), - h(AccountMenu), + // network dropdown + h(NetworkDropdown, { + provider: this.props.provider, + frequentRpcList: this.props.frequentRpcList, + }, []), - (isLoading || isLoadingNetwork) && h(Loading, { - loadingMessage: loadMessage, - }), + h(AccountMenu), - // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }), + (isLoading || isLoadingNetwork) && h(Loading, { + loadingMessage: loadMessage, + }), - // content - this.renderPrimary(), - ]) - ) -} + // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }), -App.prototype.renderGlobalModal = function () { - return h(Modal, { - ref: 'modalRef', - }, [ - // h(BuyOptions, {}, []), - ]) -} + // content + this.renderRoutes(), + // this.renderPrimary(), + ]) + ) + } -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, + renderGlobalModal () { + return h(Modal, { + ref: 'modalRef', }, [ - // 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, - } = this.props - - if (window.METAMASK_UI_TYPE === 'notification') { - return null + // h(BuyOptions, {}, []), + ]) } - const props = this.props - const {isMascara, isOnboarding} = props + 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, - // 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 + // overlay + // TODO: add onClick for overlay to close sidebar + this.props.sidebarOpen ? h('div.sidebar-overlay', { + style: {}, + onClick: () => { + this.props.hideSidebar() + }, + }, []) : undefined, + ]) } - return ( + renderAppBar () { + const { + isUnlocked, + network, + provider, + networkDropdownOpen, + showNetworkDropdown, + hideNetworkDropdown, + currentView, + isMascara, + isOnboarding, + history, + } = this.props + + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + // Do not render header if user is in mascara onboarding + if (isMascara && isOnboarding) { + return null + } - h('.full-width', { - style: {}, - }, [ + // Do not render header if user is in mascara buy ether + if (isMascara && currentView.name === 'buyEth') { + return null + } + + return ( - h('.app-header.flex-row.flex-space-between', { + h('.full-width', { style: {}, }, [ - h('div.app-header-contents', {}, [ - h('div.left-menu-wrapper', { - onClick: () => { - props.dispatch(actions.backToAccountDetail(props.activeAddress)) - }, - }, [ - // mini logo - h('img.metafox-icon', { - height: 29, - width: 29, - src: '/images/icon-128.png', - }), - - // metamask name - h('h1', { - style: { - position: 'relative', - paddingLeft: '9px', - color: '#5B5D67', - }, - }, 'MetaMask'), - - ]), - h('div.header__right-actions', [ - h('div.network-component-wrapper', { - style: {}, + h('.app-header.flex-row.flex-space-between', { + style: {}, + }, [ + h('div.app-header-contents', {}, [ + h('div.left-menu-wrapper', { + onClick: () => history.push(DEFAULT_ROUTE), }, [ - // Network Indicator - h(NetworkIndicator, { - network, - provider, - disabled: currentView.name === 'confTx', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - return networkDropdownOpen === false - ? showNetworkDropdown() - : hideNetworkDropdown() - }, + // mini logo + h('img.metafox-icon', { + height: 29, + width: 29, + src: '/images/icon-128.png', }), + // metamask name + h('h1', { + style: { + position: 'relative', + paddingLeft: '9px', + color: '#5B5D67', + }, + }, 'MetaMask'), + ]), - isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [ - h(Identicon, { - address: this.props.selectedAddress, - diameter: 32, - }), + 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, + }), + ]), ]), ]), ]), - ]), - - ]) - ) -} - - -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} = props + renderLoadingIndicator ({ isLoading, isLoadingNetwork, loadMessage }) { + const { isMascara } = this.props - if (isMascara && isOnboarding) { - return h(MascaraFirstTime) + return isMascara + ? null + : h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }) } - // notices - 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()), - }) - } + renderBackButton (style, justArrow = false) { + const { dispatch } = this.props - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) } - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { + renderPrimary () { + log.debug('rendering primary') + const { + isMascara, + isOnboarding, + noActiveNotices, + lostAccounts, + isInitialized, + forgottenPassword, + currentView, + activeAddress, + unapprovedTxs = {}, + } = this.props + + if (isMascara && isOnboarding) { + return h(MascaraFirstTime) + } - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + // notices + if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) { + return h(Redirect, { + to: { + pathname: NOTICE_ROUTE, + }, + }) + } - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + // unapprovedTxs + if (Object.keys(unapprovedTxs).length) { + return h(Redirect, { + to: { + pathname: CONFIRM_TRANSACTION_ROUTE, + }, + }) } - } - // show unlock screen - if (!props.isUnlocked) { - return h(MainContainer, { - currentViewName: props.currentView.name, - isUnlocked: props.isUnlocked, - }) - } + // 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(InitializeMenuScreen, {key: 'menuScreenInit'}) + // } + } - // show current view - switch (props.currentView.name) { + // // show unlock screen + // if (!props.isUnlocked) { + // return h(MainContainer, { + // currentViewName: props.currentView.name, + // isUnlocked: props.isUnlocked, + // }) + // } - case 'accountDetail': - log.debug('rendering main container') - return h(MainContainer, {key: 'account-detail'}) + // show current view + switch (currentView.name) { - case 'sendTransaction': - log.debug('rendering send tx screen') + case 'accountDetail': + log.debug('rendering main container') + return h(MainContainer, {key: 'account-detail'}) - // Going to leave this here until we are ready to delete SendTransactionScreen v1 - // const SendComponentToRender = checkFeatureToggle('send-v2') - // ? SendTransactionScreen2 - // : SendTransactionScreen + // case 'sendTransaction': + // log.debug('rendering send tx screen') - return h(SendTransactionScreen2, {key: 'send-transaction'}) + // // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // // const SendComponentToRender = checkFeatureToggle('send-v2') + // // ? SendTransactionScreen2 + // // : SendTransactionScreen - case 'sendToken': - log.debug('rendering send token screen') + // return h(SendTransactionScreen2, {key: 'send-transaction'}) - // Going to leave this here until we are ready to delete SendTransactionScreen v1 - // const SendTokenComponentToRender = checkFeatureToggle('send-v2') - // ? SendTransactionScreen2 - // : SendTokenScreen + // case 'sendToken': + // log.debug('rendering send token screen') - return h(SendTransactionScreen2, {key: 'sendToken'}) + // // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // // const SendTokenComponentToRender = checkFeatureToggle('send-v2') + // // ? SendTransactionScreen2 + // // : SendTokenScreen - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) + // return h(SendTransactionScreen2, {key: 'sendToken'}) - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) + // case 'confTx': + // log.debug('rendering confirm tx screen') + // return h(Redirect, { + // to: { + // pathname: CONFIRM_TRANSACTION_ROUTE, + // }, + // }) + // return h(ConfirmTxScreen, {key: 'confirm-tx'}) - case 'config': - log.debug('rendering config screen') - return h(Settings, {key: 'config'}) + // case 'add-token': + // log.debug('rendering add-token screen from unlock screen.') + // return h(AddTokenScreen, {key: 'add-token'}) - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) + // case 'config': + // log.debug('rendering config screen') + // return h(Settings, {key: 'config'}) - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + // case 'import-menu': + // log.debug('rendering import screen') + // return h(Import, {key: 'import-menu'}) - case 'info': - log.debug('rendering info screen') - return h(Settings, {key: 'info', tab: 'info'}) + // case 'reveal-seed-conf': + // log.debug('rendering reveal seed confirmation screen') + // return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) + // case 'info': + // log.debug('rendering info screen') + // return h(Settings, {key: 'info', tab: 'info'}) - case 'onboardingBuyEth': - log.debug('rendering onboarding buy ether screen') - return h(MascaraBuyEtherScreen, {key: 'buyEthView'}) + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {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', { + 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', - left: '44px', - width: '285px', + height: '100%', + top: '0px', + left: '0px', }, }, [ - h(QrView, {key: 'qr'}), - ]), - ]) + 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'}) + 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)) + 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)) + } + } + + getNetworkName () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name } } -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' +function mapStateToProps (state) { + const { appState, metamask } = state + const { + networkDropdownOpen, + sidebarOpen, + isLoading, + loadingMessage, + } = appState + + const { + identities, + accounts, + address, + keyrings, + isInitialized, + noActiveNotices, + seedWords, + unapprovedTxs, + lastUnreadNotice, + lostAccounts, + } = 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), + seedWords: state.metamask.seedWords, + unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + 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, + + // state needed to get account dropdown temporarily rendering from app bar + identities, + selected, + keyrings, } +} - return name +function mapDispatchToProps (dispatch, ownProps) { + return { + dispatch, + hideSidebar: () => dispatch(actions.hideSidebar()), + showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), + hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), + toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), + } } + +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 a9f075ec7..0965cba38 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -1,13 +1,19 @@ const inherits = require('util').inherits const Component = require('react').Component const connect = require('react-redux').connect +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') 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, IMPORT_ACCOUNT_ROUTE } = require('../../routes') -module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AccountMenu) inherits(AccountMenu, Component) function AccountMenu () { Component.call(this) } @@ -19,7 +25,6 @@ function mapStateToProps (state) { keyrings: state.metamask.keyrings, identities: state.metamask.identities, accounts: state.metamask.accounts, - } } @@ -63,6 +68,7 @@ AccountMenu.prototype.render = function () { lockMetamask, showConfigPage, showInfoPage, + history, } = this.props return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ @@ -84,18 +90,27 @@ AccountMenu.prototype.render = function () { text: 'Create Account', }), h(Item, { - onClick: showImportPage, + onClick: () => { + toggleAccountMenu() + history.push(IMPORT_ACCOUNT_ROUTE) + }, icon: h('img', { src: 'images/import-account.svg' }), text: 'Import Account', }), h(Divider), h(Item, { - onClick: showInfoPage, + onClick: () => { + toggleAccountMenu() + history.push(INFO_ROUTE) + }, icon: h('img', { src: 'images/mm-info-icon.svg' }), text: 'Info & Help', }), h(Item, { - onClick: showConfigPage, + onClick: () => { + toggleAccountMenu() + history.push(SETTINGS_ROUTE) + }, icon: h('img', { src: 'images/settings.svg' }), text: 'Settings', }), diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js deleted file mode 100644 index 941ac33e6..000000000 --- a/ui/app/components/notice.js +++ /dev/null @@ -1,132 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - 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, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - // 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}) - } -} - -Notice.prototype.componentWillUnmount = function () { - // eslint-disable-next-line react/no-find-dom-node - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/ui/app/add-token.js b/ui/app/components/pages/add-token.js index 10aaae103..7f0c37475 100644 --- a/ui/app/add-token.js +++ b/ui/app/components/pages/add-token.js @@ -5,8 +5,8 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect 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)) @@ -19,10 +19,12 @@ const fuse = new Fuse(contractList, { minMatchCharLength: 1, keys: ['address', 'name', 'symbol'], }) -const actions = require('./actions') +// const actions = require('./actions') +const actions = require('../../actions') const ethUtil = require('ethereumjs-util') -const { tokenInfoGetter } = require('./token-util') +const { tokenInfoGetter } = require('../../token-util') const R = require('ramda') +const { DEFAULT_ROUTE } = require('../../routes') const emptyAddr = '0x0000000000000000000000000000000000000000' @@ -258,7 +260,7 @@ AddTokenScreen.prototype.renderConfirmation = function () { selectedTokens, } = this.state - const { addTokens, goHome } = this.props + const { addTokens, history } = this.props const customToken = { address, @@ -296,7 +298,7 @@ AddTokenScreen.prototype.renderConfirmation = function () { ]), h('div.add-token__buttons', [ h('button.btn-secondary', { - onClick: () => addTokens(tokens).then(goHome), + onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)), }, 'Add Tokens'), h('button.btn-tertiary', { onClick: () => this.setState({ isShowingConfirmation: false }), @@ -308,7 +310,7 @@ AddTokenScreen.prototype.renderConfirmation = function () { AddTokenScreen.prototype.render = function () { const { isCollapsed, errors, isShowingConfirmation } = this.state - const { goHome } = this.props + const { history } = this.props return isShowingConfirmation ? this.renderConfirmation() @@ -349,7 +351,7 @@ AddTokenScreen.prototype.render = function () { onClick: this.onNext, }, 'Next'), h('button.btn-tertiary', { - onClick: goHome, + onClick: () => history.goBack(), }, 'Cancel'), ]), ]) diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js new file mode 100644 index 000000000..78f3a4225 --- /dev/null +++ b/ui/app/components/pages/authenticated.js @@ -0,0 +1,42 @@ +const { connect } = require('react-redux') +const PropTypes = require('prop-types') +const { Redirect, Route } = require('react-router-dom') +const h = require('react-hyperscript') +const { UNLOCK_ROUTE, INITIALIZE_MENU_ROUTE } = require('../../routes') + +const Authenticated = ({ component: Component, isUnlocked, isInitialized, ...props }) => { + + const render = props => { + switch (true) { + case isUnlocked: + return h(Component, { ...props }) + case !isInitialized: + return h(Redirect, { to: { pathname: INITIALIZE_MENU_ROUTE } }) + default: + return h(Redirect, { to: { pathname: UNLOCK_ROUTE } }) + } + } + + return ( + h(Route, { + ...props, + render, + }) + ) +} + +Authenticated.propTypes = { + component: PropTypes.func, + 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/components/pages/import-account/index.js b/ui/app/components/pages/import-account/index.js new file mode 100644 index 000000000..481ed6a4b --- /dev/null +++ b/ui/app/components/pages/import-account/index.js @@ -0,0 +1,95 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const PropTypes = require('prop-types') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const PRIVATE_KEY_MENU_ITEM = 'Private Key' +const JSON_FILE_MENU_ITEM = 'JSON File' + +class ImportAccount extends Component { + constructor (props) { + super(props) + + this.state = { + current: PRIVATE_KEY_MENU_ITEM, + menuItems: [ PRIVATE_KEY_MENU_ITEM, JSON_FILE_MENU_ITEM ], + } + } + + renderImportView () { + const { current } = this.state + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } + } + + render () { + const { history } = this.props + const { current, menuItems } = this.state + + return ( + h('div.flex-center', { + style: { + flexDirection: 'column', + marginTop: '32px', + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: history.goBack, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px 0', + width: '260px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: current, + options: menuItems.map(type => { + return { + value: type, + label: type, + } + }), + onChange: opt => { + this.setState({ current: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) + } +} + +ImportAccount.propTypes = { + history: PropTypes.object, +} + +module.exports = ImportAccount diff --git a/ui/app/accounts/import/json.js b/ui/app/components/pages/import-account/json.js index 486ed8886..63a44b4b7 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/app/components/pages/import-account/json.js @@ -2,7 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../actions') const FileInput = require('react-simple-file-input').default const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' diff --git a/ui/app/accounts/import/private-key.js b/ui/app/components/pages/import-account/private-key.js index e214bcbbe..217e6d9f9 100644 --- a/ui/app/accounts/import/private-key.js +++ b/ui/app/components/pages/import-account/private-key.js @@ -2,7 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../actions') module.exports = connect(mapStateToProps)(PrivateKeyImportView) diff --git a/ui/app/accounts/import/seed.js b/ui/app/components/pages/import-account/seed.js index b4a7c0afa..b4a7c0afa 100644 --- a/ui/app/accounts/import/seed.js +++ b/ui/app/components/pages/import-account/seed.js 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..5573f2dd2 --- /dev/null +++ b/ui/app/components/pages/keychains/restore-vault.js @@ -0,0 +1,167 @@ +const { withRouter } = require('react-router-dom') +const PropTypes = require('prop-types') +const { compose } = require('recompose') +const PersistentForm = require('../../../../lib/persistent-form') +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const { createNewVaultAndRestore } = 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() + } + } + + 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(() => history.push(DEFAULT_ROUTE)) + .catch(({ message }) => this.setState({ error: message })) + } + + render () { + const { error } = this.state + const { history } = this.props + 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, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + 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: () => history.goBack() }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, '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)) + }, + } +} + +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..dc2cfc23e --- /dev/null +++ b/ui/app/components/pages/keychains/reveal-seed.js @@ -0,0 +1,192 @@ +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 () { + document.getElementById('password-box').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.goBack(), + }, '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/notice.js b/ui/app/components/pages/notice.js new file mode 100644 index 000000000..8e68cd52b --- /dev/null +++ b/ui/app/components/pages/notice.js @@ -0,0 +1,219 @@ +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 state = this.state || { disclaimerDisabled: true } + 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 + // 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()), + // }) + // } + + 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..39e9b26ed --- /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 { 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('/'), + }), + 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..d8155eb9b --- /dev/null +++ b/ui/app/components/pages/settings/info.js @@ -0,0 +1,108 @@ +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', 'Links'), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + }, [ + h('span.settings__info-link', 'Privacy Policy'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + }, [ + h('span.settings__info-link', 'Terms of Use'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + }, [ + h('span.settings__info-link', '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', 'Visit our Support Center'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('span.settings__info-link', 'Visit our web site'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + target: '_blank', + href: 'mailto:help@metamask.io?subject=Feedback', + }, [ + h('span.settings__info-link', 'Email us!'), + ]), + ]), + ]) + ) + } + + 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', + 'MetaMask is designed and built in California.' + ), + ]), + ]), + 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, + goHome: PropTypes.func, + location: PropTypes.object, + history: PropTypes.object, +} + +module.exports = Info diff --git a/ui/app/settings.js b/ui/app/components/pages/settings/settings.js index ca7535d26..537270dee 100644 --- a/ui/app/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -1,14 +1,16 @@ 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') -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 { REVEAL_SEED_ROUTE } = require('../../../routes') const getInfuraCurrencyOptions = () => { const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { @@ -28,30 +30,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: 'Settings', key: 'settings' }, - { content: 'Info', key: 'info' }, - ], - defaultTab: activeTab, - tabSelected: key => this.setState({ activeTab: key }), - }), - ]) - } - renderBlockieOptIn () { const { metamask: { useBlockie }, setUseBlockie } = this.props @@ -210,7 +193,7 @@ class Settings extends Component { } renderSeedWords () { - const { revealSeedConfirmation } = this.props + const { history } = this.props return ( h('div.settings__content-row', [ @@ -218,17 +201,14 @@ class Settings extends Component { h('div.settings__content-item', [ h('div.settings__content-item-col', [ h('button.settings__clear-button.settings__clear-button--red', { - onClick (event) { - event.preventDefault() - revealSeedConfirmation() - }, + onClick: () => history.push(REVEAL_SEED_ROUTE), }, 'Reveal Seed Words'), ]), ]), ]) ) } - + renderOldUI () { const { setFeatureFlagToBeta } = this.props @@ -249,7 +229,7 @@ class Settings extends Component { ) } - renderSettingsContent () { + render () { const { warning } = this.props return ( @@ -265,118 +245,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', 'Links'), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - }, [ - h('span.settings__info-link', 'Privacy Policy'), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - }, [ - h('span.settings__info-link', 'Terms of Use'), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - }, [ - h('span.settings__info-link', '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', 'Visit our Support Center'), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('span.settings__info-link', 'Visit our web site'), - ]), - ]), - h('div.settings__info-link-item', [ - h('a', { - target: '_blank', - href: 'mailto:help@metamask.io?subject=Feedback', - }, [ - h('span.settings__info-link', 'Email us!'), - ]), - ]), - ]) - ) - } - - renderInfoContent () { - 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', - 'MetaMask is designed and built in California.' - ), - ]), - ]), - 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, @@ -385,7 +256,7 @@ Settings.propTypes = { revealSeedConfirmation: PropTypes.func, setFeatureFlagToBeta: PropTypes.func, warning: PropTypes.string, - goHome: PropTypes.func, + history: PropTypes.object, } const mapStateToProps = state => { @@ -397,7 +268,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)), @@ -407,4 +277,7 @@ const mapDispatchToProps = dispatch => { } } -module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(Settings) diff --git a/ui/app/components/pages/unauthenticated/unlock.js b/ui/app/components/pages/unauthenticated/unlock.js new file mode 100644 index 000000000..72f27c11d --- /dev/null +++ b/ui/app/components/pages/unauthenticated/unlock.js @@ -0,0 +1,172 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const { Redirect, withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const { tryUnlockMetamask, forgotPassword } = require('../../../actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter +const Mascot = require('../../mascot') +const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes') + +class UnlockScreen extends Component { + constructor (props) { + super(props) + + this.state = { + error: null, + } + + this.animationEventEmitter = new EventEmitter() + } + + 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 + const { isUnlocked, history } = this.props + + if (isUnlocked) { + return ( + h(Redirect, { + to: { + pathname: DEFAULT_ROUTE, + }, + }) + ) + } + + return ( + h('.unlock-page.main-container', [ + h('.flex-column', { + style: { + width: 'inherit', + }, + }, [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + 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, + }, + }, 'Unlock'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => history.push(RESTORE_VAULT_ROUTE), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Restore from seed phrase'), + ]), + ]), + ]), + ]) + ) + } +} + +UnlockScreen.propTypes = { + forgotPassword: PropTypes.func, + tryUnlockMetamask: PropTypes.func, + history: PropTypes.object, + isUnlocked: PropTypes.bool, +} + +const mapStateToProps = state => { + const { metamask: { isUnlocked } } = state + return { + isUnlocked, + } +} + +const mapDispatchToProps = dispatch => { + return { + forgotPassword: () => dispatch(forgotPassword()), + tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)), + } +} + +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 1264da153..8a167f7cd 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -1,5 +1,7 @@ const Component = require('react').Component const { connect } = require('react-redux') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const h = require('react-hyperscript') const inherits = require('util').inherits const actions = require('../../actions') @@ -11,8 +13,12 @@ const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') const { conversionUtil, addCurrencies } = require('../../conversion-util') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') +const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') -module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendEther) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmSendEther) function mapStateToProps (state) { const { @@ -43,6 +49,7 @@ function mapDispatchToProps (dispatch) { to, value: amount, } = txParams + dispatch(actions.updateSend({ gasLimit, gasPrice, @@ -52,7 +59,6 @@ function mapDispatchToProps (dispatch) { errors: { to: null, amount: null }, editingTransactionId: id, })) - dispatch(actions.showSendPage()) }, cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), } @@ -177,8 +183,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 } = this.props + const { currentCurrency, clearSend } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -220,7 +232,7 @@ ConfirmSendEther.prototype.render = function () { h('div.confirm-screen-wrapper.flex-column.flex-grow', [ h('h3.flex-center.confirm-screen-header', [ h('button.confirm-screen-back-button', { - onClick: () => editTransaction(txMeta), + onClick: () => this.editTransaction(txMeta), }, 'EDIT'), h('div.confirm-screen-title', 'Confirm Transaction'), h('div.confirm-screen-header-tip'), @@ -422,6 +434,7 @@ ConfirmSendEther.prototype.onSubmit = function (event) { ConfirmSendEther.prototype.cancel = function (event, txMeta) { event.preventDefault() this.props.cancelTransaction(txMeta) + .then(() => this.props.history.push(DEFAULT_ROUTE)) } ConfirmSendEther.prototype.checkValidity = function () { diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 727cd260b..8087e431f 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -1,5 +1,7 @@ const Component = require('react').Component const { connect } = require('react-redux') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') const h = require('react-hyperscript') const inherits = require('util').inherits const ethAbi = require('ethereumjs-abi') @@ -27,8 +29,12 @@ const { getSelectedAddress, getSelectedTokenContract, } = require('../../selectors') +const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') -module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmSendToken) function mapStateToProps (state, ownProps) { const { token: { symbol }, txData } = ownProps @@ -99,6 +105,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.componentWillMount = function () { const { tokenContract, selectedAddress } = this.props tokenContract && tokenContract @@ -293,7 +305,6 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { } ConfirmSendToken.prototype.render = function () { - const { editTransaction } = this.props const txMeta = this.gatherTxMeta() const { from: { @@ -316,7 +327,7 @@ ConfirmSendToken.prototype.render = function () { h('div.confirm-screen-wrapper.flex-column.flex-grow', [ h('h3.flex-center.confirm-screen-header', [ h('button.confirm-screen-back-button', { - onClick: () => editTransaction(txMeta), + onClick: () => this.editTransaction(txMeta), }, 'EDIT'), h('div.confirm-screen-title', 'Confirm Transaction'), h('div.confirm-screen-header-tip'), @@ -416,6 +427,7 @@ ConfirmSendToken.prototype.onSubmit = function (event) { ConfirmSendToken.prototype.cancel = function (event, txMeta) { event.preventDefault() 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 655de8897..5998bd252 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) diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index 0edced119..32c9e4f24 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -4,31 +4,17 @@ const PropTypes = require('react').PropTypes 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 70722f43e..6f18ea814 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -10,8 +10,14 @@ 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 = connect(mapStateToProps, mapDispatchToProps)(TxList) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxList) function mapStateToProps (state) { return { @@ -88,7 +94,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa transactionHash, transactionNetworkId, } = props - const { showConfTxPage } = this.props + const { showConfTxPage, history } = this.props const opts = { key: transActionId || transactionHash, @@ -106,7 +112,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa const isUnapproved = transactionStatus === 'unapproved' if (isUnapproved) { - opts.onClick = () => showConfTxPage({id: transActionId}) + opts.onClick = () => history.push(CONFIRM_TRANSACTION_ROUTE) opts.transactionStatus = 'Not Started' } 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 e42a20c85..46029166e 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -3,14 +3,20 @@ 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 = connect(mapStateToProps, mapDispatchToProps)(TxView) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxView) function mapStateToProps (state) { const sidebarOpen = state.appState.sidebarOpen @@ -63,7 +69,7 @@ TxView.prototype.renderHeroBalance = function () { } TxView.prototype.renderButtons = function () { - const {selectedToken, showModal, showSendPage, showSendTokenPage } = this.props + const {selectedToken, showModal, history } = this.props return !selectedToken ? ( @@ -82,7 +88,7 @@ TxView.prototype.renderButtons = function () { textAlign: 'center', marginLeft: '0.8em', }, - onClick: showSendPage, + onClick: () => history.push(SEND_ROUTE), }, 'SEND'), ]) ) @@ -93,7 +99,7 @@ TxView.prototype.renderButtons = function () { textAlign: 'center', marginLeft: '0.8em', }, - onClick: showSendTokenPage, + onClick: () => history.push(SEND_ROUTE), }, 'SEND'), ]) ) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 3cb7a8b76..3b4443ef6 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -1,6 +1,8 @@ const Component = require('react').Component 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 Identicon = require('./identicon') // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns @@ -9,8 +11,12 @@ 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 = connect(mapStateToProps, mapDispatchToProps)(WalletView) +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(WalletView) function mapStateToProps (state) { @@ -89,6 +95,7 @@ WalletView.prototype.render = function () { showAccountDetailModal, hideSidebar, showAddTokenPage, + history, } = this.props // temporary logs + fake extra wallets // console.log('walletview, selectedAccount:', selectedAccount) @@ -152,10 +159,7 @@ WalletView.prototype.render = function () { h(TokenList), h('button.wallet-view__add-token-button', { - onClick: () => { - showAddTokenPage() - hideSidebar() - }, + onClick: () => history.push(ADD_TOKEN_ROUTE), }, 'Add Token'), ]) } diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 9f273aaec..ce0012d5b 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,17 +13,12 @@ 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) { return { @@ -48,6 +45,20 @@ function ConfirmTxScreen () { Component.call(this) } +ConfirmTxScreen.prototype.componentWillMount = function () { + const { unapprovedTxs = {} } = this.props + if (Object.keys(unapprovedTxs).length === 0) { + this.props.history.push(DEFAULT_ROUTE) + } +} + +ConfirmTxScreen.prototype.componentWillReceiveProps = function (nextProps) { + const { unapprovedTxs = {} } = nextProps + if (Object.keys(unapprovedTxs).length === 0) { + this.props.history.push(DEFAULT_ROUTE) + } +} + ConfirmTxScreen.prototype.render = function () { const props = this.props const { @@ -146,6 +157,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) { 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 dfb4f23f0..e1ec42c66 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -51,3 +51,5 @@ @import './account-dropdown-mini.scss'; @import './editable-label.scss'; + +@import './pages/index.scss'; 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 b4587f1ee..6832f5ddf 100644 --- a/ui/app/first-time/init-menu.js +++ b/ui/app/first-time/init-menu.js @@ -1,184 +1,191 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect +const { EventEmitter } = require('events') +const { Component } = require('react') +const { connect } = require('react-redux') const h = require('react-hyperscript') +const PropTypes = require('prop-types') 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') -let isSubmitting = false +class InitializeMenuScreen extends Component { + constructor (props) { + super(props) -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} + this.animationEventEmitter = new EventEmitter() + this.state = { + warning: null, + } + } -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, + componentWillMount () { + const { isInitialized, isUnlocked, history } = this.props + if (isInitialized || isUnlocked) { + history.push(DEFAULT_ROUTE) + } } -} -InitializeMenuScreen.prototype.render = function () { - var state = this.props + componentDidMount () { + document.getElementById('password-box').focus() + } - switch (state.currentView.name) { + render () { + const { history } = this.props + const { warning } = this.state - default: - return this.renderMenu(state) + return ( + h('.initialize-screen.flex-column.flex-center.flex-grow', [ - } -} + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), + h('span.error.in-progress-notification', warning), - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + 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: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), - h('div', [ - h('h3', { + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', + margin: 12, }, - }, 'Encrypt your new DEN'), + }, 'Create'), - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => history.push(RESTORE_VAULT_ROUTE), style: { - fontSize: '18px', - position: 'relative', + fontSize: '0.8em', color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', + textDecoration: 'underline', }, - }), + }, 'Import Existing DEN'), ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - 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: 'Confirm Password', - 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, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - ]) - ) -} + ]) + ) + } -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() + createVaultOnEnter (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } } -} -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() + 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: 'password not long enough' }) + return + } + if (password !== passwordConfirm) { + this.setState({ warning: 'passwords don\'t match' }) + return + } + + this.props.createNewVaultAndKeychain(password) + .then(() => history.push(DEFAULT_ROUTE)) + } + + 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.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) +InitializeMenuScreen.propTypes = { + history: PropTypes.object, + isInitialized: PropTypes.bool, + isUnlocked: PropTypes.bool, + createNewVaultAndKeychain: PropTypes.func, } -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 +const mapStateToProps = state => { + const { metamask: { isInitialized, isUnlocked } } = state - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return + return { + isInitialized, + isUnlocked, } +} - if (!isSubmitting) { - isSubmitting = true - this.props.dispatch(actions.createNewVaultAndKeychain(password)) +const mapDispatchToProps = dispatch => { + return { + createNewVaultAndKeychain: password => dispatch(actions.createNewVaultAndKeychain(password)), } } -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, - }) -} +module.exports = connect(mapStateToProps, mapDispatchToProps)(InitializeMenuScreen) diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index 5ab5d4c33..000000000 --- a/ui/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,91 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') -const exportAsFile = require('../../util').exportAsFile - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - 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: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords() - .then(account => this.showAccountDetail(account)), - style: { - margin: '24px', - fontSize: '0.9em', - marginBottom: '10px', - }, - }, 'I\'ve copied it somewhere safe'), - - h('button.primary', { - onClick: () => exportAsFile(`MetaMask Seed Words`, seed), - style: { - margin: '10px', - fontSize: '0.9em', - }, - }, 'Save Seed Words As File'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - return this.props.dispatch(actions.confirmSeedWords()) -} - -CreateVaultCompleteScreen.prototype.showAccountDetail = function (account) { - return this.props.dispatch(actions.showAccountDetail(account)) -} diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4335186a5..000000000 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,121 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = 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: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - style: { marginLeft: '10px' }, - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/ui/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - 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, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // 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.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 031f61e84..ad50ee13d 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -2,9 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const AccountAndTransactionDetails = require('./account-and-transaction-details') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const Settings = require('./settings') -const UnlockScreen = require('./unlock') +const UnlockScreen = require('./components/pages/unauthenticated/unlock') module.exports = MainContainer @@ -26,36 +24,6 @@ MainContainer.prototype.render = function () { style: {}, } - if (this.props.isUnlocked === false) { - switch (this.props.currentViewName) { - case 'restoreVault': - log.debug('rendering restore vault screen') - contents = { - component: HDRestoreVaultScreen, - key: 'HDRestoreVaultScreen', - } - break - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(Settings, {key: 'config'}) - default: - log.debug('rendering locked screen') - contents = { - component: UnlockScreen, - style: { - boxShadow: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - background: '#F7F7F7', - // must force 100%, because lock screen is full-width - width: '100%', - }, - key: 'locked', - } - } - } - return h('div.main-container', { style: contents.style, }, [ diff --git a/ui/app/root.js b/ui/app/root.js index 21d6d1829..64f365c9e 100644 --- a/ui/app/root.js +++ b/ui/app/root.js @@ -1,22 +1,28 @@ -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') +const { HashRouter } = require('react-router-dom') -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(HashRouter, { + hashType: 'noslash', + }, [ + 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..1305d6b1e --- /dev/null +++ b/ui/app/routes.js @@ -0,0 +1,27 @@ +const DEFAULT_ROUTE = '/' +const UNLOCK_ROUTE = '/unlock' +const SETTINGS_ROUTE = '/settings' +const INFO_ROUTE = '/settings/info' +const REVEAL_SEED_ROUTE = '/reveal-seed-confirm' +const RESTORE_VAULT_ROUTE = '/restore-vault' +const ADD_TOKEN_ROUTE = '/add-token' +const IMPORT_ACCOUNT_ROUTE = '/import-account' +const SEND_ROUTE = '/send' +const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' +const INITIALIZE_MENU_ROUTE = '/initialize-menu' +const NOTICE_ROUTE = '/notice' + +module.exports = { + DEFAULT_ROUTE, + UNLOCK_ROUTE, + SETTINGS_ROUTE, + INFO_ROUTE, + REVEAL_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + ADD_TOKEN_ROUTE, + IMPORT_ACCOUNT_ROUTE, + SEND_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + INITIALIZE_MENU_ROUTE, + NOTICE_ROUTE, +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index e1b88f0db..32bbdfe6e 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -28,6 +28,7 @@ const { isTokenBalanceSufficient, } = require('./components/send/send-utils') const { isValidAddress } = require('./util') +const { CONFIRM_TRANSACTION_ROUTE } = require('./routes') module.exports = SendTransactionScreen @@ -508,9 +509,9 @@ SendTransactionScreen.prototype.renderForm = function () { SendTransactionScreen.prototype.renderFooter = function () { const { - goHome, clearSend, errors: { amount: amountError, to: toError }, + history, } = this.props const noErrors = !amountError && toError === null @@ -520,7 +521,7 @@ SendTransactionScreen.prototype.renderFooter = function () { h('button.send-v2__cancel-btn', { onClick: () => { clearSend() - goHome() + history.goBack() }, }, 'Cancel'), h(`button.send-v2__next-btn${errorClass}`, { @@ -555,7 +556,7 @@ SendTransactionScreen.prototype.addToAddressBookIfNew = function (newAddress) { SendTransactionScreen.prototype.onSubmit = function (event) { event.preventDefault() const { - from: {address: from}, + from: { address: from }, to, amount, gasLimit: gas, @@ -578,6 +579,7 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (editingTransactionId) { backToConfirmScreen(editingTransactionId) + this.props.history.push(CONFIRM_TRANSACTION_ROUTE) return } @@ -596,4 +598,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) { selectedToken ? signTokenTx(selectedToken.address, to, amount, txParams) : signTx(txParams) + + this.props.history.push(CONFIRM_TRANSACTION_ROUTE) } diff --git a/ui/app/unlock.js b/ui/app/unlock.js deleted file mode 100644 index ec97b03bf..000000000 --- a/ui/app/unlock.js +++ /dev/null @@ -1,122 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', { - style: { - width: 'inherit', - }, - }, [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - background: 'white', - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Restore from seed phrase'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} |