diff options
author | Alexander Tseung <alextsg@users.noreply.github.com> | 2019-01-23 23:25:34 +0800 |
---|---|---|
committer | Whymarrh Whitby <whymarrh.whitby@gmail.com> | 2019-01-23 23:25:34 +0800 |
commit | fba17d77de9e60de0e02e90dc6dbcbbf7454158a (patch) | |
tree | 0a14f465c25b2b400f5706b55993dcf06d6633a3 /ui | |
parent | 69fcfa427bdee2ea287e9d9c23963dc1032685cd (diff) | |
download | tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.gz tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.bz2 tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.lz tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.xz tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.zst tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.zip |
Refactor first time flow, remove seed phrase from state (#5994)
* Refactor and fix styling for first time flow. Remove seed phrase from persisted metamask state
* Fix linting and tests
* Fix translations, initialization notice routing
* Fix drizzle tests
* Fix e2e tests
* Fix integration tests
* Fix styling
* Fix migration naming from 030 to 031
* Open extension in browser when user has not completed onboarding
Diffstat (limited to 'ui')
81 files changed, 2485 insertions, 448 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 7cc88e2b3..29cf4b2f2 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -85,6 +85,8 @@ var actions = { createNewVaultAndKeychain: createNewVaultAndKeychain, createNewVaultAndRestore: createNewVaultAndRestore, createNewVaultInProgress: createNewVaultInProgress, + createNewVaultAndGetSeedPhrase, + unlockAndGetSeedPhrase, addNewKeyring, importNewAccount, addNewAccount, @@ -312,6 +314,11 @@ var actions = { UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', setUseNativeCurrencyAsPrimaryCurrencyPreference, + // Onboarding + setCompletedOnboarding, + completeOnboarding, + COMPLETE_ONBOARDING: 'COMPLETE_ONBOARDING', + setMouseUserState, SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', @@ -451,6 +458,7 @@ function createNewVaultAndRestore (password, seed) { .catch(err => { dispatch(actions.displayWarning(err.message)) dispatch(actions.hideLoadingIndication()) + return Promise.reject(err) }) } } @@ -485,12 +493,71 @@ function createNewVaultAndKeychain (password) { } } +function createNewVaultAndGetSeedPhrase (password) { + return async dispatch => { + dispatch(actions.showLoadingIndication()) + + try { + await createNewVault(password) + const seedWords = await verifySeedPhrase() + dispatch(actions.hideLoadingIndication()) + return seedWords + } catch (error) { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(error.message)) + throw new Error(error.message) + } + } +} + +function unlockAndGetSeedPhrase (password) { + return async dispatch => { + dispatch(actions.showLoadingIndication()) + + try { + await submitPassword(password) + const seedWords = await verifySeedPhrase() + await forceUpdateMetamaskState(dispatch) + dispatch(actions.hideLoadingIndication()) + return seedWords + } catch (error) { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(error.message)) + throw new Error(error.message) + } + } +} + function revealSeedConfirmation () { return { type: this.REVEAL_SEED_CONFIRMATION, } } +function submitPassword (password) { + return new Promise((resolve, reject) => { + background.submitPassword(password, error => { + if (error) { + return reject(error) + } + + resolve() + }) + }) +} + +function createNewVault (password) { + return new Promise((resolve, reject) => { + background.createNewVaultAndKeychain(password, error => { + if (error) { + return reject(error) + } + + resolve(true) + }) + }) +} + function verifyPassword (password) { return new Promise((resolve, reject) => { background.submitPassword(password, error => { @@ -2356,6 +2423,31 @@ function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) { return setPreference('useNativeCurrencyAsPrimaryCurrency', value) } +function setCompletedOnboarding () { + return dispatch => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.completeOnboarding(err => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.completeOnboarding()) + resolve() + }) + }) + } +} + +function completeOnboarding () { + return { + type: actions.COMPLETE_ONBOARDING, + } +} + function setNetworkNonce (networkNonce) { return { type: actions.SET_NETWORK_NONCE, diff --git a/ui/app/app.js b/ui/app/app.js index f320ced0a..28f4860a8 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -1,16 +1,14 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const { Route, Switch, withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const h = require('react-hyperscript') -const actions = require('./actions') -const classnames = require('classnames') -const log = require('loglevel') -const { getMetaMaskAccounts, getNetworkIdentifier } = require('./selectors') +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Route, Switch, withRouter, matchPath } from 'react-router-dom' +import { compose } from 'recompose' +import actions from './actions' +import log from 'loglevel' +import { getMetaMaskAccounts, getNetworkIdentifier } from './selectors' // init -const InitializeScreen = require('../../mascara/src/app/first-time').default +import FirstTimeFlow from './components/pages/first-time-flow' // accounts const SendTransactionScreen = require('./components/send/send.container') const ConfirmTransaction = require('./components/pages/confirm-transaction') @@ -21,8 +19,9 @@ const Sidebar = require('./components/sidebars').default // other views import Home from './components/pages/home' import Settings from './components/pages/settings' -const Authenticated = require('./components/pages/authenticated') -const Initialized = require('./components/pages/initialized') +import Authenticated from './higher-order-components/authenticated' +import Initialized from './higher-order-components/initialized' +import Lock from './components/pages/lock' const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') @@ -49,8 +48,9 @@ import { } from './selectors/transactions' // Routes -const { +import { DEFAULT_ROUTE, + LOCK_ROUTE, UNLOCK_ROUTE, SETTINGS_ROUTE, REVEAL_SEED_ROUTE, @@ -62,8 +62,15 @@ const { SEND_ROUTE, CONFIRM_TRANSACTION_ROUTE, INITIALIZE_ROUTE, + INITIALIZE_UNLOCK_ROUTE, NOTICE_ROUTE, -} = require('./routes') +} from './routes' + +// enums +import { + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, +} from '../../app/scripts/lib/enums' class App extends Component { componentWillMount () { @@ -75,37 +82,67 @@ class App extends Component { } renderRoutes () { - const exact = true - return ( - h(Switch, [ - h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }), - h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }), - h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), - h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), - h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), - h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), - h(Authenticated, { - path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, - component: ConfirmTransaction, - }), - h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), - h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), - h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), - h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }), - h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), - h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), - ]) + <Switch> + <Route path={LOCK_ROUTE} component={Lock} exact /> + <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} /> + <Initialized path={UNLOCK_ROUTE} component={UnlockPage} exact /> + <Initialized path={RESTORE_VAULT_ROUTE} component={RestoreVaultPage} exact /> + <Authenticated path={REVEAL_SEED_ROUTE} component={RevealSeedConfirmation} exact /> + <Authenticated path={SETTINGS_ROUTE} component={Settings} /> + <Authenticated path={NOTICE_ROUTE} component={NoticeScreen} exact /> + <Authenticated path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransaction} /> + <Authenticated path={SEND_ROUTE} component={SendTransactionScreen} exact /> + <Authenticated path={ADD_TOKEN_ROUTE} component={AddTokenPage} exact /> + <Authenticated path={CONFIRM_ADD_TOKEN_ROUTE} component={ConfirmAddTokenPage} exact /> + <Authenticated path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE} component={ConfirmAddSuggestedTokenPage} exact /> + <Authenticated path={NEW_ACCOUNT_ROUTE} component={CreateAccountPage} /> + <Authenticated path={DEFAULT_ROUTE} component={Home} exact /> + </Switch> ) } + onInitializationUnlockPage () { + const { location } = this.props + return Boolean(matchPath(location.pathname, { path: INITIALIZE_UNLOCK_ROUTE, exact: true })) + } + + onConfirmPage () { + const { location } = this.props + return Boolean(matchPath(location.pathname, { path: CONFIRM_TRANSACTION_ROUTE, exact: false })) + } + + hasProviderRequests () { + const { providerRequests } = this.props + return Array.isArray(providerRequests) && providerRequests.length > 0 + } + + hideAppHeader () { + const { location } = this.props + + const isInitializing = Boolean(matchPath(location.pathname, { + path: INITIALIZE_ROUTE, exact: false, + })) + + if (isInitializing && !this.onInitializationUnlockPage()) { + return true + } + + if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { + return true + } + + if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP) { + return this.onConfirmPage() || this.hasProviderRequests() + } + } + render () { const { isLoading, alertMessage, loadingMessage, network, - isMouseUser, provider, frequentRpcListDetail, currentView, @@ -127,58 +164,47 @@ class App extends Component { const { transaction: sidebarTransaction } = props || {} return ( - h('.flex-column.full-height', { - className: classnames({ 'mouse-user-styles': isMouseUser }), - style: { - overflowX: 'hidden', - position: 'relative', - alignItems: 'center', - }, - tabIndex: '0', - onClick: () => setMouseUserState(true), - onKeyDown: (e) => { + <div + className="app" + onClick={() => setMouseUserState(true)} + onKeyDown={e => { if (e.keyCode === 9) { setMouseUserState(false) } - }, - }, [ - - // global modal - h(Modal, {}, []), - - // global alert - h(Alert, {visible: this.props.alertOpen, msg: alertMessage}), - - h(AppHeader), - - // sidebar - h(Sidebar, { - sidebarOpen: sidebarIsOpen, - sidebarShouldClose: sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id), - hideSidebar: this.props.hideSidebar, - transitionName: sidebarTransitionName, - type: sidebarType, - sidebarProps: sidebar.props, - }), - - // network dropdown - h(NetworkDropdown, { - provider, - frequentRpcListDetail, - }, []), - - h(AccountMenu), - - h('div.main-container-wrapper', [ - isLoading && h(Loading, { - loadingMessage: loadMessage, - }), - !isLoading && isLoadingNetwork && h(LoadingNetwork), - - // content - this.renderRoutes(), - ]), - ]) + }} + > + <Modal /> + <Alert + visible={this.props.alertOpen} + msg={alertMessage} + /> + { + !this.hideAppHeader() && ( + <AppHeader + hideNetworkIndicator={this.onInitializationUnlockPage()} + disabled={this.onConfirmPage()} + /> + ) + } + <Sidebar + sidebarOpen={sidebarIsOpen} + sidebarShouldClose={sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)} + hideSidebar={this.props.hideSidebar} + transitionName={sidebarTransitionName} + type={sidebarType} + sidebarProps={sidebar.props} + /> + <NetworkDropdown + provider={provider} + frequentRpcListDetail={frequentRpcListDetail} + /> + <AccountMenu /> + <div className="main-container-wrapper"> + { isLoading && <Loading loadingMessage={loadMessage} /> } + { !isLoading && isLoadingNetwork && <LoadingNetwork /> } + { this.renderRoutes() } + </div> + </div> ) } @@ -282,6 +308,7 @@ App.propTypes = { setMouseUserState: PropTypes.func, t: PropTypes.func, providerId: PropTypes.string, + providerRequests: PropTypes.array, } function mapStateToProps (state) { @@ -310,6 +337,7 @@ function mapStateToProps (state) { unapprovedMsgCount, unapprovedPersonalMsgCount, unapprovedTypedMessagesCount, + providerRequests, } = metamask const selected = address || Object.keys(accounts)[0] @@ -357,6 +385,7 @@ function mapStateToProps (state) { identities, selected, keyrings, + providerRequests, } } diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 83fcca620..f7d8c8598 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -1,20 +1,13 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { matchPath } from 'react-router-dom' import Identicon from '../identicon' - -const { - ENVIRONMENT_TYPE_NOTIFICATION, - ENVIRONMENT_TYPE_POPUP, -} = require('../../../../app/scripts/lib/enums') -const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes') +import { DEFAULT_ROUTE } from '../../routes' const NetworkIndicator = require('../network') export default class AppHeader extends PureComponent { static propTypes = { history: PropTypes.object, - location: PropTypes.object, network: PropTypes.string, provider: PropTypes.object, networkDropdownOpen: PropTypes.bool, @@ -23,7 +16,8 @@ export default class AppHeader extends PureComponent { toggleAccountMenu: PropTypes.func, selectedAddress: PropTypes.string, isUnlocked: PropTypes.bool, - providerRequests: PropTypes.array, + hideNetworkIndicator: PropTypes.bool, + disabled: PropTypes.bool, } static contextTypes = { @@ -41,34 +35,15 @@ export default class AppHeader extends PureComponent { : hideNetworkDropdown() } - /** - * Returns whether or not the user is in the middle of a confirmation prompt - * - * This accounts for both tx confirmations as well as provider approvals - * - * @returns {boolean} - */ - isConfirming () { - const { location, providerRequests } = this.props - const confirmTxRouteMatch = matchPath(location.pathname, { - exact: false, - path: CONFIRM_TRANSACTION_ROUTE, - }) - const isConfirmingTx = Boolean(confirmTxRouteMatch) - const hasPendingProviderApprovals = Array.isArray(providerRequests) && providerRequests.length > 0 - - return isConfirmingTx || hasPendingProviderApprovals - } - renderAccountMenu () { - const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props + const { isUnlocked, toggleAccountMenu, selectedAddress, disabled } = this.props return isUnlocked && ( <div className={classnames('account-menu__icon', { - 'account-menu__icon--disabled': this.isConfirming(), + 'account-menu__icon--disabled': disabled, })} - onClick={() => this.isConfirming() || toggleAccountMenu()} + onClick={() => disabled || toggleAccountMenu()} > <Identicon address={selectedAddress} @@ -78,38 +53,16 @@ export default class AppHeader extends PureComponent { ) } - hideAppHeader () { - const { location } = this.props - - const isInitializing = Boolean(matchPath(location.pathname, { - path: INITIALIZE_ROUTE, exact: false, - })) - - if (isInitializing) { - return true - } - - if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { - return true - } - - if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) { - return true - } - } - render () { const { + history, network, provider, - history, isUnlocked, + hideNetworkIndicator, + disabled, } = this.props - if (this.hideAppHeader()) { - return null - } - return ( <div className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}> @@ -131,14 +84,18 @@ export default class AppHeader extends PureComponent { /> </div> <div className="app-header__account-menu-container"> - <div className="app-header__network-component-wrapper"> - <NetworkIndicator - network={network} - provider={provider} - onClick={event => this.handleNetworkIndicatorClick(event)} - disabled={this.isConfirming()} - /> - </div> + { + !hideNetworkIndicator && ( + <div className="app-header__network-component-wrapper"> + <NetworkIndicator + network={network} + provider={provider} + onClick={event => this.handleNetworkIndicatorClick(event)} + disabled={disabled} + /> + </div> + ) + } { this.renderAccountMenu() } </div> </div> diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js index 8b719bdf6..30d3f8cc4 100644 --- a/ui/app/components/app-header/app-header.container.js +++ b/ui/app/components/app-header/app-header.container.js @@ -11,7 +11,6 @@ const mapStateToProps = state => { const { network, provider, - providerRequests, selectedAddress, isUnlocked, } = metamask @@ -20,7 +19,6 @@ const mapStateToProps = state => { networkDropdownOpen, network, provider, - providerRequests, selectedAddress, isUnlocked, } diff --git a/ui/app/components/breadcrumbs/breadcrumbs.component.js b/ui/app/components/breadcrumbs/breadcrumbs.component.js new file mode 100644 index 000000000..6644836db --- /dev/null +++ b/ui/app/components/breadcrumbs/breadcrumbs.component.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class Breadcrumbs extends PureComponent { + static propTypes = { + className: PropTypes.string, + currentIndex: PropTypes.number, + total: PropTypes.number, + } + + render () { + const { className, currentIndex, total } = this.props + + return ( + <div className={classnames('breadcrumbs', className)}> + { + Array(total).fill().map((_, i) => ( + <div + key={i} + className="breadcrumb" + style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}} + /> + )) + } + </div> + ) + } +} diff --git a/ui/app/components/breadcrumbs/index.js b/ui/app/components/breadcrumbs/index.js new file mode 100644 index 000000000..07a11574f --- /dev/null +++ b/ui/app/components/breadcrumbs/index.js @@ -0,0 +1 @@ +export { default } from './breadcrumbs.component' diff --git a/ui/app/components/breadcrumbs/index.scss b/ui/app/components/breadcrumbs/index.scss new file mode 100644 index 000000000..e23aa7970 --- /dev/null +++ b/ui/app/components/breadcrumbs/index.scss @@ -0,0 +1,15 @@ +.breadcrumbs { + display: flex; + flex-flow: row nowrap; +} + +.breadcrumb { + height: 10px; + width: 10px; + border: 1px solid #979797; + border-radius: 50%; +} + +.breadcrumb + .breadcrumb { + margin-left: 10px; +} diff --git a/ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js new file mode 100644 index 000000000..5013c5b60 --- /dev/null +++ b/ui/app/components/breadcrumbs/tests/breadcrumbs.component.test.js @@ -0,0 +1,22 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import Breadcrumbs from '../breadcrumbs.component' + +describe('Breadcrumbs Component', () => { + it('should render with the correct colors', () => { + const wrapper = shallow( + <Breadcrumbs + currentIndex={1} + total={3} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.breadcrumbs').length, 1) + assert.equal(wrapper.find('.breadcrumb').length, 3) + assert.equal(wrapper.find('.breadcrumb').at(0).props().style['backgroundColor'], '#FFFFFF') + assert.equal(wrapper.find('.breadcrumb').at(1).props().style['backgroundColor'], '#D8D8D8') + assert.equal(wrapper.find('.breadcrumb').at(2).props().style['backgroundColor'], '#FFFFFF') + }) +}) diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js index 5c617585d..5d19219b4 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/button/button.component.js @@ -8,6 +8,7 @@ const CLASSNAME_SECONDARY = 'btn-secondary' const CLASSNAME_CONFIRM = 'btn-confirm' const CLASSNAME_RAISED = 'btn-raised' const CLASSNAME_LARGE = 'btn--large' +const CLASSNAME_FIRST_TIME = 'btn--first-time' const typeHash = { default: CLASSNAME_DEFAULT, @@ -15,6 +16,7 @@ const typeHash = { secondary: CLASSNAME_SECONDARY, confirm: CLASSNAME_CONFIRM, raised: CLASSNAME_RAISED, + 'first-time': CLASSNAME_FIRST_TIME, } export default class Button extends Component { diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index f1ecbbc3d..33bbb4573 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -4,6 +4,8 @@ @import './app-header/index'; +@import './breadcrumbs/index'; + @import './button-group/index'; @import './card/index'; diff --git a/ui/app/components/lock-icon/index.js b/ui/app/components/lock-icon/index.js new file mode 100644 index 000000000..6b4df0e58 --- /dev/null +++ b/ui/app/components/lock-icon/index.js @@ -0,0 +1 @@ +export { default } from './lock-icon.component' diff --git a/ui/app/components/lock-icon/lock-icon.component.js b/ui/app/components/lock-icon/lock-icon.component.js new file mode 100644 index 000000000..d010cb6b2 --- /dev/null +++ b/ui/app/components/lock-icon/lock-icon.component.js @@ -0,0 +1,32 @@ +import React from 'react' + +export default function LockIcon (props) { + return ( + <svg + version="1.1" + id="Capa_1" + xmlns="http://www.w3.org/2000/svg" + xmlnsXlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + width="401.998px" + height="401.998px" + viewBox="0 0 401.998 401.998" + style={{enableBackground: 'new 0 0 401.998 401.998'}} + xmlSpace="preserve" + {...props} + > + <g> + <path + d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218 + C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821 + h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417 + c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135 + C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675 + c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728 + z" + /> + </g> + </svg> + ) +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 0a603db4e..990be260c 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -122,7 +122,8 @@ const MODALS = { display: 'flex', }, laptopModalStyle: { - width: '850px', + width: 'initial', + maxWidth: '850px', top: 'calc(10% + 10px)', left: '0', right: '0', diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss index 6fc97820a..b71a3cb9d 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/page-container/index.scss @@ -42,6 +42,12 @@ justify-content: space-between; } + &__bottom { + flex: 1; + display: flex; + flex-direction: column; + } + &__footer { display: flex; flex-flow: column; diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js deleted file mode 100644 index 1f6b0be49..000000000 --- a/ui/app/components/pages/authenticated.js +++ /dev/null @@ -1,34 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Redirect } = require('react-router-dom') -const h = require('react-hyperscript') -const MetamaskRoute = require('./metamask-route') -const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes') - -const Authenticated = props => { - const { isUnlocked, isInitialized } = props - - switch (true) { - case isUnlocked && isInitialized: - return h(MetamaskRoute, { ...props }) - case !isInitialized: - return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) - default: - return h(Redirect, { to: { pathname: UNLOCK_ROUTE } }) - } -} - -Authenticated.propTypes = { - isUnlocked: PropTypes.bool, - isInitialized: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isUnlocked, isInitialized } } = state - return { - isUnlocked, - isInitialized, - } -} - -module.exports = connect(mapStateToProps)(Authenticated) diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js new file mode 100644 index 000000000..69b1e549f --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Switch, Route } from 'react-router-dom' +import NewAccount from './new-account' +import ImportWithSeedPhrase from './import-with-seed-phrase' +import UniqueImage from './unique-image' +import { + INITIALIZE_CREATE_PASSWORD_ROUTE, + INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, + INITIALIZE_UNIQUE_IMAGE_ROUTE, + INITIALIZE_NOTICE_ROUTE, +} from '../../../../routes' + +export default class CreatePassword extends PureComponent { + static propTypes = { + history: PropTypes.object, + isInitialized: PropTypes.bool, + onCreateNewAccount: PropTypes.func, + onCreateNewAccountFromSeed: PropTypes.func, + } + + componentDidMount () { + const { isInitialized, history } = this.props + + if (isInitialized) { + history.push(INITIALIZE_NOTICE_ROUTE) + } + } + + render () { + const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props + + return ( + <div className="first-time-flow__wrapper"> + <Switch> + <Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImage} /> + <Route + exact + path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE} + render={props => ( + <ImportWithSeedPhrase + { ...props } + onSubmit={onCreateNewAccountFromSeed} + /> + )} + /> + <Route + exact + path={INITIALIZE_CREATE_PASSWORD_ROUTE} + render={props => ( + <NewAccount + { ...props } + onSubmit={onCreateNewAccount} + /> + )} + /> + </Switch> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.container.js b/ui/app/components/pages/first-time-flow/create-password/create-password.container.js new file mode 100644 index 000000000..89106f016 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/create-password.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import CreatePassword from './create-password.component' + +const mapStateToProps = state => { + const { metamask: { isInitialized } } = state + + return { + isInitialized, + } +} + +export default connect(mapStateToProps)(CreatePassword) diff --git a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js new file mode 100644 index 000000000..8d81e5d8e --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js @@ -0,0 +1,214 @@ +import {validateMnemonic} from 'bip39' +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TextField from '../../../../text-field' +import Button from '../../../../button' +import Breadcrumbs from '../../../../breadcrumbs' +import { + INITIALIZE_CREATE_PASSWORD_ROUTE, + INITIALIZE_NOTICE_ROUTE, +} from '../../../../../routes' + +export default class ImportWithSeedPhrase extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + history: PropTypes.object, + onSubmit: PropTypes.func.isRequired, + } + + state = { + seedPhrase: '', + password: '', + confirmPassword: '', + seedPhraseError: '', + passwordError: '', + confirmPasswordError: '', + } + + parseSeedPhrase = (seedPhrase) => { + return seedPhrase + .match(/\w+/g) + .join(' ') + } + + handleSeedPhraseChange (seedPhrase) { + let seedPhraseError = '' + + if (seedPhrase) { + if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { + seedPhraseError = this.context.t('seedPhraseReq') + } else if (!validateMnemonic(seedPhrase)) { + seedPhraseError = this.context.t('invalidSeedPhrase') + } + } + + this.setState({ seedPhrase, seedPhraseError }) + } + + handlePasswordChange (password) { + const { t } = this.context + + this.setState(state => { + const { confirmPassword } = state + let confirmPasswordError = '' + let passwordError = '' + + if (password && password.length < 8) { + passwordError = t('passwordNotLongEnough') + } + + if (confirmPassword && password !== confirmPassword) { + confirmPasswordError = t('passwordsDontMatch') + } + + return { + password, + passwordError, + confirmPasswordError, + } + }) + } + + handleConfirmPasswordChange (confirmPassword) { + const { t } = this.context + + this.setState(state => { + const { password } = state + let confirmPasswordError = '' + + if (password !== confirmPassword) { + confirmPasswordError = t('passwordsDontMatch') + } + + return { + confirmPassword, + confirmPasswordError, + } + }) + } + + handleImport = async event => { + event.preventDefault() + + if (!this.isValid()) { + return + } + + const { password, seedPhrase } = this.state + const { history, onSubmit } = this.props + + try { + await onSubmit(password, seedPhrase) + history.push(INITIALIZE_NOTICE_ROUTE) + } catch (error) { + this.setState({ seedPhraseError: error.message }) + } + } + + isValid () { + const { + seedPhrase, + password, + confirmPassword, + passwordError, + confirmPasswordError, + seedPhraseError, + } = this.state + + if (!password || !confirmPassword || !seedPhrase || password !== confirmPassword) { + return false + } + + if (password.length < 8) { + return false + } + + return !passwordError && !confirmPasswordError && !seedPhraseError + } + + render () { + const { t } = this.context + const { seedPhraseError, passwordError, confirmPasswordError } = this.state + + return ( + <form + className="first-time-flow__form" + onSubmit={this.handleImport} + > + <div> + <a + onClick={e => { + e.preventDefault() + this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) + }} + href="#" + > + {`< Back`} + </a> + </div> + <div className="first-time-flow__header"> + { t('importAccountSeedPhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('secretPhrase') } + </div> + <div className="first-time-flow__textarea-wrapper"> + <label>{ t('walletSeed') }</label> + <textarea + className="first-time-flow__textarea" + onChange={e => this.handleSeedPhraseChange(e.target.value)} + value={this.state.seedPhrase} + placeholder={t('seedPhrasePlaceholder')} + /> + </div> + { + seedPhraseError && ( + <span className="error"> + { seedPhraseError } + </span> + ) + } + <TextField + id="password" + label={t('newPassword')} + type="password" + className="first-time-flow__input" + value={this.state.password} + onChange={event => this.handlePasswordChange(event.target.value)} + error={passwordError} + autoComplete="new-password" + margin="normal" + largeLabel + /> + <TextField + id="confirm-password" + label={t('confirmPassword')} + type="password" + className="first-time-flow__input" + value={this.state.confirmPassword} + onChange={event => this.handleConfirmPasswordChange(event.target.value)} + error={confirmPasswordError} + autoComplete="confirm-password" + margin="normal" + largeLabel + /> + <Button + type="first-time" + className="first-time-flow__button" + disabled={!this.isValid()} + onClick={this.handleImport} + > + { t('import') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={2} + currentIndex={0} + /> + </form> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js new file mode 100644 index 000000000..e5ff1fde5 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './import-with-seed-phrase.component' diff --git a/ui/app/components/pages/first-time-flow/create-password/index.js b/ui/app/components/pages/first-time-flow/create-password/index.js new file mode 100644 index 000000000..42e7436f9 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/index.js @@ -0,0 +1 @@ +export { default } from './create-password.container' diff --git a/ui/app/components/pages/first-time-flow/create-password/new-account/index.js b/ui/app/components/pages/first-time-flow/create-password/new-account/index.js new file mode 100644 index 000000000..97db39cc3 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/new-account/index.js @@ -0,0 +1 @@ +export { default } from './new-account.component' diff --git a/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js new file mode 100644 index 000000000..54f8c1a70 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js @@ -0,0 +1,178 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Breadcrumbs from '../../../../breadcrumbs' +import Button from '../../../../button' +import { + INITIALIZE_UNIQUE_IMAGE_ROUTE, + INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, +} from '../../../../../routes' +import TextField from '../../../../text-field' + +export default class NewAccount extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + onSubmit: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + } + + state = { + password: '', + confirmPassword: '', + passwordError: '', + confirmPasswordError: '', + } + + isValid () { + const { + password, + confirmPassword, + passwordError, + confirmPasswordError, + } = this.state + + if (!password || !confirmPassword || password !== confirmPassword) { + return false + } + + if (password.length < 8) { + return false + } + + return !passwordError && !confirmPasswordError + } + + handlePasswordChange (password) { + const { t } = this.context + + this.setState(state => { + const { confirmPassword } = state + let passwordError = '' + let confirmPasswordError = '' + + if (password && password.length < 8) { + passwordError = t('passwordNotLongEnough') + } + + if (confirmPassword && password !== confirmPassword) { + confirmPasswordError = t('passwordsDontMatch') + } + + return { + password, + passwordError, + confirmPasswordError, + } + }) + } + + handleConfirmPasswordChange (confirmPassword) { + const { t } = this.context + + this.setState(state => { + const { password } = state + let confirmPasswordError = '' + + if (password !== confirmPassword) { + confirmPasswordError = t('passwordsDontMatch') + } + + return { + confirmPassword, + confirmPasswordError, + } + }) + } + + handleCreate = async event => { + event.preventDefault() + + if (!this.isValid()) { + return + } + + const { password } = this.state + const { onSubmit, history } = this.props + + try { + await onSubmit(password) + history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE) + } catch (error) { + this.setState({ passwordError: error.message }) + } + } + + handleImportWithSeedPhrase = event => { + const { history } = this.props + + event.preventDefault() + history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE) + } + + render () { + const { t } = this.context + const { password, confirmPassword, passwordError, confirmPasswordError } = this.state + + return ( + <div> + <div className="first-time-flow__header"> + { t('createPassword') } + </div> + <form + className="first-time-flow__form" + onSubmit={this.handleCreate} + > + <TextField + id="create-password" + label={t('newPassword')} + type="password" + className="first-time-flow__input" + value={password} + onChange={event => this.handlePasswordChange(event.target.value)} + error={passwordError} + autoFocus + autoComplete="new-password" + margin="normal" + fullWidth + largeLabel + /> + <TextField + id="confirm-password" + label={t('confirmPassword')} + type="password" + className="first-time-flow__input" + value={confirmPassword} + onChange={event => this.handleConfirmPasswordChange(event.target.value)} + error={confirmPasswordError} + autoComplete="confirm-password" + margin="normal" + fullWidth + largeLabel + /> + <Button + type="first-time" + className="first-time-flow__button" + disabled={!this.isValid()} + onClick={this.handleCreate} + > + { t('create') } + </Button> + </form> + <a + href="" + className="first-time-flow__link create-password__import-link" + onClick={this.handleImportWithSeedPhrase} + > + { t('importWithSeedPhrase') } + </a> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={0} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/index.js b/ui/app/components/pages/first-time-flow/create-password/unique-image/index.js new file mode 100644 index 000000000..0e97bf755 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/unique-image/index.js @@ -0,0 +1 @@ +export { default } from './unique-image.container' diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js new file mode 100644 index 000000000..41a566f0a --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js @@ -0,0 +1,53 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Identicon from '../../../../identicon' +import Breadcrumbs from '../../../../breadcrumbs' +import Button from '../../../../button' +import { INITIALIZE_NOTICE_ROUTE } from '../../../../../routes' + +export default class UniqueImageScreen extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + address: PropTypes.string, + history: PropTypes.object, + } + + render () { + const { t } = this.context + const { address, history } = this.props + + return ( + <div> + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="first-time-flow__header"> + { t('yourUniqueAccountImage') } + </div> + <div className="first-time-flow__text-block"> + { t('yourUniqueAccountImageDescription1') } + </div> + <div className="first-time-flow__text-block"> + { t('yourUniqueAccountImageDescription2') } + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={() => history.push(INITIALIZE_NOTICE_ROUTE)} + > + { t('next') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={0} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js new file mode 100644 index 000000000..34874aaec --- /dev/null +++ b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import UniqueImage from './unique-image.component' + +const mapStateToProps = ({ metamask }) => { + const { selectedAddress } = metamask + + return { + address: selectedAddress, + } +} + +export default connect(mapStateToProps)(UniqueImage) diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js new file mode 100644 index 000000000..9e8bce2c8 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js @@ -0,0 +1,57 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Redirect } from 'react-router-dom' +import { + DEFAULT_ROUTE, + LOCK_ROUTE, + INITIALIZE_WELCOME_ROUTE, + INITIALIZE_NOTICE_ROUTE, + INITIALIZE_UNLOCK_ROUTE, + INITIALIZE_SEED_PHRASE_ROUTE, +} from '../../../../routes' + +export default class FirstTimeFlowSwitch extends PureComponent { + static propTypes = { + completedOnboarding: PropTypes.bool, + isInitialized: PropTypes.bool, + isUnlocked: PropTypes.bool, + noActiveNotices: PropTypes.bool, + seedPhrase: PropTypes.string, + } + + render () { + const { + completedOnboarding, + isInitialized, + isUnlocked, + noActiveNotices, + seedPhrase, + } = this.props + + if (completedOnboarding) { + return <Redirect to={{ pathname: DEFAULT_ROUTE }} /> + } + + if (isUnlocked && !seedPhrase) { + return <Redirect to={{ pathname: LOCK_ROUTE }} /> + } + + if (!isInitialized) { + return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} /> + } + + if (!isUnlocked) { + return <Redirect to={{ pathname: INITIALIZE_UNLOCK_ROUTE }} /> + } + + if (!noActiveNotices) { + return <Redirect to={{ pathname: INITIALIZE_NOTICE_ROUTE }} /> + } + + if (seedPhrase) { + return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} /> + } + + return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} /> + } +} diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js new file mode 100644 index 000000000..8b7a74880 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import FirstTimeFlowSwitch from './first-time-flow-switch.component' + +const mapStateToProps = ({ metamask }) => { + const { + completedOnboarding, + isInitialized, + isUnlocked, + noActiveNotices, + } = metamask + + return { + completedOnboarding, + isInitialized, + isUnlocked, + noActiveNotices, + } +} + +export default connect(mapStateToProps)(FirstTimeFlowSwitch) diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js new file mode 100644 index 000000000..3647756ef --- /dev/null +++ b/ui/app/components/pages/first-time-flow/first-time-flow-switch/index.js @@ -0,0 +1 @@ +export { default } from './first-time-flow-switch.container' diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.component.js b/ui/app/components/pages/first-time-flow/first-time-flow.component.js new file mode 100644 index 000000000..cde077803 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/first-time-flow.component.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Switch, Route } from 'react-router-dom' +import FirstTimeFlowSwitch from './first-time-flow-switch' +import Welcome from './welcome' +import Unlock from '../unlock-page' +import CreatePassword from './create-password' +import Notices from './notices' +import SeedPhrase from './seed-phrase' +import { + DEFAULT_ROUTE, + INITIALIZE_WELCOME_ROUTE, + INITIALIZE_CREATE_PASSWORD_ROUTE, + INITIALIZE_NOTICE_ROUTE, + INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_UNLOCK_ROUTE, +} from '../../../routes' + +export default class FirstTimeFlow extends PureComponent { + static propTypes = { + completedOnboarding: PropTypes.bool, + createNewAccount: PropTypes.func, + createNewAccountFromSeed: PropTypes.func, + history: PropTypes.object, + isInitialized: PropTypes.bool, + isUnlocked: PropTypes.bool, + noActiveNotices: PropTypes.bool, + unlockAccount: PropTypes.func, + } + + state = { + seedPhrase: '', + isImportedKeyring: false, + } + + componentDidMount () { + const { completedOnboarding, history, isInitialized, isUnlocked } = this.props + + if (completedOnboarding) { + history.push(DEFAULT_ROUTE) + return + } + + if (isInitialized && !isUnlocked) { + history.push(INITIALIZE_UNLOCK_ROUTE) + return + } + } + + handleCreateNewAccount = async password => { + const { createNewAccount } = this.props + + try { + const seedPhrase = await createNewAccount(password) + this.setState({ seedPhrase }) + } catch (error) { + throw new Error(error.message) + } + } + + handleImportWithSeedPhrase = async (password, seedPhrase) => { + const { createNewAccountFromSeed } = this.props + + try { + await createNewAccountFromSeed(password, seedPhrase) + this.setState({ isImportedKeyring: true }) + } catch (error) { + throw new Error(error.message) + } + } + + handleUnlock = async password => { + const { unlockAccount, history, noActiveNotices } = this.props + + try { + const seedPhrase = await unlockAccount(password) + this.setState({ seedPhrase }, () => { + noActiveNotices + ? history.push(INITIALIZE_SEED_PHRASE_ROUTE) + : history.push(INITIALIZE_NOTICE_ROUTE) + }) + } catch (error) { + throw new Error(error.message) + } + } + + render () { + const { seedPhrase, isImportedKeyring } = this.state + + return ( + <div className="first-time-flow"> + <Switch> + <Route + path={INITIALIZE_SEED_PHRASE_ROUTE} + render={props => ( + <SeedPhrase + { ...props } + seedPhrase={seedPhrase} + /> + )} + /> + <Route + exact + path={INITIALIZE_NOTICE_ROUTE} + render={props => ( + <Notices + { ...props } + isImportedKeyring={isImportedKeyring} + /> + )} + /> + <Route + path={INITIALIZE_CREATE_PASSWORD_ROUTE} + render={props => ( + <CreatePassword + { ...props } + onCreateNewAccount={this.handleCreateNewAccount} + onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase} + /> + )} + /> + <Route + path={INITIALIZE_UNLOCK_ROUTE} + render={props => ( + <Unlock + { ...props } + onSubmit={this.handleUnlock} + /> + )} + /> + <Route + exact + path={INITIALIZE_WELCOME_ROUTE} + component={Welcome} + /> + <Route + exact + path="*" + component={FirstTimeFlowSwitch} + /> + </Switch> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.container.js b/ui/app/components/pages/first-time-flow/first-time-flow.container.js new file mode 100644 index 000000000..782eddb74 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/first-time-flow.container.js @@ -0,0 +1,30 @@ +import { connect } from 'react-redux' +import FirstTimeFlow from './first-time-flow.component' +import { + createNewVaultAndGetSeedPhrase, + createNewVaultAndRestore, + unlockAndGetSeedPhrase, +} from '../../../actions' + +const mapStateToProps = state => { + const { metamask: { completedOnboarding, isInitialized, isUnlocked, noActiveNotices } } = state + + return { + completedOnboarding, + isInitialized, + isUnlocked, + noActiveNotices, + } +} + +const mapDispatchToProps = dispatch => { + return { + createNewAccount: password => dispatch(createNewVaultAndGetSeedPhrase(password)), + createNewAccountFromSeed: (password, seedPhrase) => { + return dispatch(createNewVaultAndRestore(password, seedPhrase)) + }, + unlockAccount: password => dispatch(unlockAndGetSeedPhrase(password)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(FirstTimeFlow) diff --git a/ui/app/components/pages/first-time-flow/index.js b/ui/app/components/pages/first-time-flow/index.js new file mode 100644 index 000000000..5db42437c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/index.js @@ -0,0 +1 @@ +export { default } from './first-time-flow.container' diff --git a/ui/app/components/pages/first-time-flow/index.scss b/ui/app/components/pages/first-time-flow/index.scss new file mode 100644 index 000000000..e3aca0694 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/index.scss @@ -0,0 +1,99 @@ +@import './welcome/index'; + +@import './seed-phrase/index'; + +.first-time-flow { + width: 100%; + background-color: $white; + + &__wrapper { + @media screen and (min-width: $break-large) { + padding: 60px 275px 0 275px; + } + + @media screen and (max-width: 1100px) { + padding: 36px; + } + } + + &__form { + display: flex; + flex-direction: column; + } + + &__header { + font-size: 2.5rem; + margin-bottom: 24px; + } + + &__subheader { + margin-bottom: 16px; + } + + &__input { + max-width: 350px; + } + + &__textarea-wrapper { + margin-bottom: 8px; + display: inline-flex; + padding: 0; + position: relative; + min-width: 0; + flex-direction: column; + max-width: 350px; + } + + &__textarea-label { + margin-bottom: 9px; + color: #1B344D; + font-size: 18px; + } + + &__textarea { + font-size: 1rem; + font-family: Roboto; + height: 190px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: #FFFFFF; + padding: 16px; + margin-top: 8px; + } + + &__breadcrumbs { + margin: 36px 0; + } + + &__unique-image { + margin-bottom: 20px; + } + + &__markdown { + border: 1px solid #979797; + border-radius: 8px; + background-color: $white; + height: 200px; + overflow-y: auto; + color: #757575; + font-size: .75rem; + line-height: 15px; + text-align: justify; + margin: 0; + padding: 16px 20px; + height: 30vh; + } + + &__text-block { + margin-bottom: 24px; + + @media screen and (max-width: $break-small) { + margin-bottom: 16px; + font-size: .875rem; + } + } + + &__button { + margin: 35px 0 14px; + } +} diff --git a/ui/app/components/pages/first-time-flow/notices/index.js b/ui/app/components/pages/first-time-flow/notices/index.js new file mode 100644 index 000000000..024daaa68 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/notices/index.js @@ -0,0 +1 @@ +export { default } from './notices.container' diff --git a/ui/app/components/pages/first-time-flow/notices/notices.component.js b/ui/app/components/pages/first-time-flow/notices/notices.component.js new file mode 100644 index 000000000..fefaedd6f --- /dev/null +++ b/ui/app/components/pages/first-time-flow/notices/notices.component.js @@ -0,0 +1,124 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Markdown from 'react-markdown' +import debounce from 'lodash.debounce' +import Button from '../../../button' +import Identicon from '../../../identicon' +import Breadcrumbs from '../../../breadcrumbs' +import { DEFAULT_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../routes' + +export default class Notices extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + address: PropTypes.string.isRequired, + completeOnboarding: PropTypes.func, + history: PropTypes.object, + isImportedKeyring: PropTypes.bool, + markNoticeRead: PropTypes.func, + nextUnreadNotice: PropTypes.shape({ + title: PropTypes.string, + date: PropTypes.string, + body: PropTypes.string, + }), + noActiveNotices: PropTypes.bool, + } + + static defaultProps = { + nextUnreadNotice: {}, + } + + state = { + atBottom: false, + } + + componentDidMount () { + const { noActiveNotices, history } = this.props + + if (noActiveNotices) { + history.push(INITIALIZE_SEED_PHRASE_ROUTE) + } + + this.onScroll() + } + + acceptTerms = async () => { + const { + completeOnboarding, + history, + isImportedKeyring, + markNoticeRead, + nextUnreadNotice, + } = this.props + + const hasActiveNotices = await markNoticeRead(nextUnreadNotice) + + if (!hasActiveNotices) { + if (isImportedKeyring) { + await completeOnboarding() + history.push(DEFAULT_ROUTE) + } else { + history.push(INITIALIZE_SEED_PHRASE_ROUTE) + } + } else { + this.setState({ atBottom: false }, () => this.onScroll()) + } + } + + onScroll = debounce(() => { + if (this.state.atBottom) { + return + } + + const target = document.querySelector('.first-time-flow__markdown') + + if (target) { + const { scrollTop, offsetHeight, scrollHeight } = target + const atBottom = scrollTop + offsetHeight >= scrollHeight + + this.setState({ atBottom }) + } + }, 25) + + render () { + const { t } = this.context + const { isImportedKeyring, address, nextUnreadNotice: { title, body } } = this.props + const { atBottom } = this.state + + return ( + <div + className="first-time-flow__wrapper" + onScroll={this.onScroll} + > + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="first-time-flow__header"> + { title } + </div> + <Markdown + className="first-time-flow__markdown" + source={body} + skipHtml + /> + <Button + type="first-time" + className="first-time-flow__button" + onClick={atBottom && this.acceptTerms} + disabled={!atBottom} + > + { t('accept') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={isImportedKeyring ? 2 : 3} + currentIndex={1} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/notices/notices.container.js b/ui/app/components/pages/first-time-flow/notices/notices.container.js new file mode 100644 index 000000000..c65c5b7de --- /dev/null +++ b/ui/app/components/pages/first-time-flow/notices/notices.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import { markNoticeRead, setCompletedOnboarding } from '../../../../actions' +import Notices from './notices.component' + +const mapStateToProps = ({ metamask }) => { + const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask + + return { + address: selectedAddress, + nextUnreadNotice, + noActiveNotices, + } +} + +const mapDispatchToProps = dispatch => { + return { + markNoticeRead: notice => dispatch(markNoticeRead(notice)), + completeOnboarding: () => dispatch(setCompletedOnboarding()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(Notices) diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js new file mode 100644 index 000000000..bc0f73a27 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js @@ -0,0 +1,161 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import shuffle from 'lodash.shuffle' +import Identicon from '../../../../identicon' +import Button from '../../../../button' +import Breadcrumbs from '../../../../breadcrumbs' +import { DEFAULT_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../../routes' +import { exportAsFile } from '../../../../../../app/util' +import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state' + +export default class ConfirmSeedPhrase extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + seedPhrase: '', + } + + static propTypes = { + address: PropTypes.string, + completeOnboarding: PropTypes.func, + history: PropTypes.object, + onSubmit: PropTypes.func, + openBuyEtherModal: PropTypes.func, + seedPhrase: PropTypes.string, + } + + state = { + selectedSeedWords: [], + shuffledSeedWords: [], + // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number} + selectedSeedWordsHash: {}, + } + + componentDidMount () { + const { seedPhrase = '' } = this.props + const shuffledSeedWords = shuffle(seedPhrase.split(' ')) || [] + this.setState({ shuffledSeedWords }) + } + + handleExport = () => { + exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') + } + + handleSubmit = async () => { + const { completeOnboarding, history, openBuyEtherModal } = this.props + + if (!this.isValid()) { + return + } + + try { + await completeOnboarding() + history.push(DEFAULT_ROUTE) + openBuyEtherModal() + } catch (error) { + console.error(error.message) + } + } + + handleSelectSeedWord = (word, shuffledIndex) => { + this.setState(selectSeedWord(word, shuffledIndex)) + } + + handleDeselectSeedWord = shuffledIndex => { + this.setState(deselectSeedWord(shuffledIndex)) + } + + isValid () { + const { seedPhrase } = this.props + const { selectedSeedWords } = this.state + return seedPhrase === selectedSeedWords.join(' ') + } + + render () { + const { t } = this.context + const { address, history } = this.props + const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state + + return ( + <div> + <div className="confirm-seed-phrase__back-button"> + <a + onClick={e => { + e.preventDefault() + history.push(INITIALIZE_SEED_PHRASE_ROUTE) + }} + href="#" + > + {`< Back`} + </a> + </div> + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="first-time-flow__header"> + { t('confirmSecretBackupPhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('selectEachPhrase') } + </div> + <div className="confirm-seed-phrase__selected-seed-words"> + { + selectedSeedWords.map((word, index) => ( + <div + key={index} + className="confirm-seed-phrase__seed-word" + > + { word } + </div> + )) + } + </div> + <div className="confirm-seed-phrase__shuffled-seed-words"> + { + shuffledSeedWords.map((word, index) => { + const isSelected = index in selectedSeedWordsHash + + return ( + <div + key={index} + className={classnames( + 'confirm-seed-phrase__seed-word', + 'confirm-seed-phrase__seed-word--shuffled', + { 'confirm-seed-phrase__seed-word--selected': isSelected } + )} + onClick={() => { + if (!isSelected) { + this.handleSelectSeedWord(word, index) + } else { + this.handleDeselectSeedWord(index) + } + }} + > + { word } + </div> + ) + }) + } + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={this.handleSubmit} + disabled={!this.isValid()} + > + { t('confirm') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={2} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js new file mode 100644 index 000000000..5fa2bec1e --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import ConfirmSeedPhrase from './confirm-seed-phrase.component' +import { setCompletedOnboarding, showModal } from '../../../../../actions' + +const mapDispatchToProps = dispatch => { + return { + completeOnboarding: () => dispatch(setCompletedOnboarding()), + openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})), + } +} + +export default connect(null, mapDispatchToProps)(ConfirmSeedPhrase) diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js new file mode 100644 index 000000000..f2476fc5c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js @@ -0,0 +1,41 @@ +export function selectSeedWord (word, shuffledIndex) { + return function update (state) { + const { selectedSeedWords, selectedSeedWordsHash } = state + const nextSelectedIndex = selectedSeedWords.length + + return { + selectedSeedWords: [ ...selectedSeedWords, word ], + selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex }, + } + } +} + +export function deselectSeedWord (shuffledIndex) { + return function update (state) { + const { + selectedSeedWords: prevSelectedSeedWords, + selectedSeedWordsHash: prevSelectedSeedWordsHash, + } = state + + const selectedSeedWords = [...prevSelectedSeedWords] + const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex] + selectedSeedWords.splice(indexToRemove, 1) + const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => { + const output = { ...acc } + const selectedSeedWordIndex = prevSelectedSeedWordsHash[index] + + if (selectedSeedWordIndex < indexToRemove) { + output[index] = selectedSeedWordIndex + } else if (selectedSeedWordIndex > indexToRemove) { + output[index] = selectedSeedWordIndex - 1 + } + + return output + }, {}) + + return { + selectedSeedWords, + selectedSeedWordsHash, + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js new file mode 100644 index 000000000..beb53b383 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './confirm-seed-phrase.container' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss new file mode 100644 index 000000000..e0444571f --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss @@ -0,0 +1,44 @@ +.confirm-seed-phrase { + &__back-button { + margin-bottom: 12px; + } + + &__selected-seed-words { + min-height: 190px; + max-width: 496px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + margin: 24px 0 36px; + padding: 12px; + } + + &__shuffled-seed-words { + max-width: 496px; + } + + &__seed-word { + display: inline-block; + color: #5B5D67; + background-color: #E7E7E7; + padding: 8px 18px; + min-width: 64px; + margin: 4px; + text-align: center; + + &--selected { + background-color: #85D1CC; + color: $white; + } + + &--shuffled { + cursor: pointer; + margin: 6px; + } + + @media screen and (max-width: 575px) { + font-size: .875rem; + padding: 6px 18px; + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/index.js new file mode 100644 index 000000000..7355bfb2c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './seed-phrase.container' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/index.scss new file mode 100644 index 000000000..88b28950c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/index.scss @@ -0,0 +1,36 @@ +@import './confirm-seed-phrase/index'; + +@import './reveal-seed-phrase/index'; + +.seed-phrase { + + &__sections { + display: flex; + + @media screen and (min-width: $break-large) { + flex-direction: row; + } + + @media screen and (max-width: $break-small) { + flex-direction: column; + } + } + + &__main { + flex: 3; + min-width: 0; + } + + &__side { + flex: 2; + min-width: 0; + + @media screen and (min-width: $break-large) { + margin-left: 48px; + } + + @media screen and (max-width: $break-small) { + margin-top: 24px; + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js new file mode 100644 index 000000000..4a1b191b5 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './reveal-seed-phrase.component' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss new file mode 100644 index 000000000..568359d31 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss @@ -0,0 +1,53 @@ +.reveal-seed-phrase { + &__secret { + position: relative; + display: flex; + justify-content: center; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + padding: 18px; + margin-top: 36px; + max-width: 350px; + } + + &__secret-words { + width: 310px; + font-size: 1.25rem; + text-align: center; + + &--hidden { + filter: blur(5px); + } + } + + &__secret-blocker { + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 100%; + background-color: rgba(0,0,0,0.6); + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + padding: 8px 0 18px; + cursor: pointer; + } + + &__reveal-button { + color: $white; + font-size: .75rem; + font-weight: 500; + text-transform: uppercase; + margin-top: 8px; + text-align: center; + } + + &__export-text { + color: $curious-blue; + cursor: pointer; + font-weight: 500; + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js new file mode 100644 index 000000000..bb822d1d5 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js @@ -0,0 +1,139 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../../identicon' +import LockIcon from '../../../../lock-icon' +import Button from '../../../../button' +import Breadcrumbs from '../../../../breadcrumbs' +import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../../routes' +import { exportAsFile } from '../../../../../../app/util' + +export default class RevealSeedPhrase extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + address: PropTypes.string, + history: PropTypes.object, + seedPhrase: PropTypes.string, + } + + state = { + isShowingSeedPhrase: false, + } + + handleExport = () => { + exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') + } + + handleNext = event => { + event.preventDefault() + const { isShowingSeedPhrase } = this.state + const { history } = this.props + + if (!isShowingSeedPhrase) { + return + } + + history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE) + } + + renderSecretWordsContainer () { + const { t } = this.context + const { seedPhrase } = this.props + const { isShowingSeedPhrase } = this.state + + return ( + <div className="reveal-seed-phrase__secret"> + <div className={classnames( + 'reveal-seed-phrase__secret-words', + { 'reveal-seed-phrase__secret-words--hidden': !isShowingSeedPhrase } + )}> + { seedPhrase } + </div> + { + !isShowingSeedPhrase && ( + <div + className="reveal-seed-phrase__secret-blocker" + onClick={() => this.setState({ isShowingSeedPhrase: true })} + > + <LockIcon + width="28px" + height="35px" + fill="#FFFFFF" + /> + <div className="reveal-seed-phrase__reveal-button"> + { t('clickToRevealSeed') } + </div> + </div> + ) + } + </div> + ) + } + + render () { + const { t } = this.context + const { address } = this.props + const { isShowingSeedPhrase } = this.state + + return ( + <div> + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="seed-phrase__sections"> + <div className="seed-phrase__main"> + <div className="first-time-flow__header"> + { t('secretBackupPhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('secretBackupPhraseDescription') } + </div> + <div className="first-time-flow__text-block"> + { t('secretBackupPhraseWarning') } + </div> + { this.renderSecretWordsContainer() } + </div> + <div className="seed-phrase__side"> + <div className="first-time-flow__text-block"> + { `${t('tips')}:` } + </div> + <div className="first-time-flow__text-block"> + { t('storePhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('writePhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('memorizePhrase') } + </div> + <div className="first-time-flow__text-block"> + <a + className="reveal-seed-phrase__export-text" + onClick={this.handleExport}> + { t('downloadSecretBackup') } + </a> + </div> + </div> + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={this.handleNext} + disabled={!isShowingSeedPhrase} + > + { t('next') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={2} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js new file mode 100644 index 000000000..5f5b8a0b2 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -0,0 +1,59 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Switch, Route } from 'react-router-dom' +import RevealSeedPhrase from './reveal-seed-phrase' +import ConfirmSeedPhrase from './confirm-seed-phrase' +import { + INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, + DEFAULT_ROUTE, +} from '../../../../routes' + +export default class SeedPhrase extends PureComponent { + static propTypes = { + address: PropTypes.string, + history: PropTypes.object, + seedPhrase: PropTypes.string, + } + + componentDidMount () { + const { seedPhrase, history } = this.props + + if (!seedPhrase) { + history.push(DEFAULT_ROUTE) + } + } + + render () { + const { address, seedPhrase } = this.props + + return ( + <div className="first-time-flow__wrapper"> + <Switch> + <Route + exact + path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} + render={props => ( + <ConfirmSeedPhrase + { ...props } + address={address} + seedPhrase={seedPhrase} + /> + )} + /> + <Route + exact + path={INITIALIZE_SEED_PHRASE_ROUTE} + render={props => ( + <RevealSeedPhrase + { ...props } + address={address} + seedPhrase={seedPhrase} + /> + )} + /> + </Switch> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js new file mode 100644 index 000000000..4df024ffc --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import SeedPhrase from './seed-phrase.component' + +const mapStateToProps = state => { + const { metamask: { selectedAddress } } = state + + return { + address: selectedAddress, + } +} + +export default connect(mapStateToProps)(SeedPhrase) diff --git a/ui/app/components/pages/first-time-flow/welcome/index.js b/ui/app/components/pages/first-time-flow/welcome/index.js new file mode 100644 index 000000000..8abeddaa1 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/welcome/index.js @@ -0,0 +1 @@ +export { default } from './welcome.container' diff --git a/ui/app/components/pages/first-time-flow/welcome/index.scss b/ui/app/components/pages/first-time-flow/welcome/index.scss new file mode 100644 index 000000000..7527ceb35 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/welcome/index.scss @@ -0,0 +1,43 @@ +.welcome-page { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 400px; + padding: 0 18px; + + &__wrapper { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 100%; + } + + &__header { + font-size: 1.5rem; + margin-bottom: 14px; + } + + &__description { + text-align: center; + + @media screen and (max-width: 575px) { + font-size: .9rem; + } + } + + &__button { + height: 54px; + width: 198px; + font-family: Roboto; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); + color: $white; + font-size: 1.25rem; + font-weight: 500; + text-transform: uppercase; + margin: 35px 0 14px; + transition: 200ms ease-in-out; + background-color: rgba(247, 134, 28, .9); + } +} diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.component.js b/ui/app/components/pages/first-time-flow/welcome/welcome.component.js new file mode 100644 index 000000000..f28a8210d --- /dev/null +++ b/ui/app/components/pages/first-time-flow/welcome/welcome.component.js @@ -0,0 +1,65 @@ +import EventEmitter from 'events' +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Mascot from '../../../mascot' +import Button from '../../../button' +import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_NOTICE_ROUTE } from '../../../../routes' + +export default class Welcome extends PureComponent { + static propTypes = { + history: PropTypes.object, + isInitialized: PropTypes.bool, + } + + static contextTypes = { + t: PropTypes.func, + } + + constructor (props) { + super(props) + + this.animationEventEmitter = new EventEmitter() + } + + componentDidMount () { + const { history, isInitialized } = this.props + + if (isInitialized) { + history.push(INITIALIZE_NOTICE_ROUTE) + } + } + + handleContinue = () => { + this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) + } + + render () { + const { t } = this.context + + return ( + <div className="welcome-page__wrapper"> + <div className="welcome-page"> + <Mascot + animationEventEmitter={this.animationEventEmitter} + width="225" + height="225" + /> + <div className="welcome-page__header"> + { t('welcome') } + </div> + <div className="welcome-page__description"> + <div>{ t('metamaskDescription') }</div> + <div>{ t('holdEther') }</div> + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={this.handleContinue} + > + { t('continue') } + </Button> + </div> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.container.js b/ui/app/components/pages/first-time-flow/welcome/welcome.container.js new file mode 100644 index 000000000..4362d89cb --- /dev/null +++ b/ui/app/components/pages/first-time-flow/welcome/welcome.container.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import { closeWelcomeScreen } from '../../../../actions' +import Welcome from './welcome.component' + +const mapStateToProps = ({ metamask }) => { + const { welcomeScreenSeen, isInitialized } = metamask + + return { + welcomeScreenSeen, + isInitialized, + } +} + +const mapDispatchToProps = dispatch => { + return { + closeWelcomeScreen: () => dispatch(closeWelcomeScreen()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(Welcome) diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js index b9ec3c258..469c760a6 100644 --- a/ui/app/components/pages/home/home.component.js +++ b/ui/app/components/pages/home/home.component.js @@ -7,7 +7,7 @@ import TransactionView from '../../transaction-view' import ProviderApproval from '../provider-approval' import { - INITIALIZE_BACKUP_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_ROUTE, RESTORE_VAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE, NOTICE_ROUTE, @@ -59,7 +59,7 @@ export default class Home extends PureComponent { // seed words if (seedWords) { - return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/> + return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/> } if (forgottenPassword) { diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss index 6551278f5..6a0680f32 100644 --- a/ui/app/components/pages/index.scss +++ b/ui/app/components/pages/index.scss @@ -5,3 +5,7 @@ @import './confirm-add-token/index'; @import './settings/index'; + +@import './first-time-flow/index'; + +@import './keychains/index'; diff --git a/ui/app/components/pages/initialized.js b/ui/app/components/pages/initialized.js deleted file mode 100644 index 3adf67b28..000000000 --- a/ui/app/components/pages/initialized.js +++ /dev/null @@ -1,25 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Redirect } = require('react-router-dom') -const h = require('react-hyperscript') -const { INITIALIZE_ROUTE } = require('../../routes') -const MetamaskRoute = require('./metamask-route') - -const Initialized = props => { - return props.isInitialized - ? h(MetamaskRoute, { ...props }) - : h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) -} - -Initialized.propTypes = { - isInitialized: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isInitialized } } = state - return { - isInitialized, - } -} - -module.exports = connect(mapStateToProps)(Initialized) diff --git a/ui/app/components/pages/keychains/index.scss b/ui/app/components/pages/keychains/index.scss new file mode 100644 index 000000000..868185419 --- /dev/null +++ b/ui/app/components/pages/keychains/index.scss @@ -0,0 +1,197 @@ +.first-view-main-wrapper { + display: flex; + width: 100%; + height: 100%; + justify-content: center; + padding: 0 10px; +} + +.first-view-main { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +@media screen and (min-width: 1281px) { + .first-view-main { + width: 62vw; + } +} + +.import-account { + display: flex; + flex-flow: column nowrap; + margin: 60px 0 30px 0; + position: relative; + max-width: initial; +} + +@media only screen and (max-width: 575px) { + .import-account{ + margin: 24px; + display: flex; + flex-flow: column nowrap; + width: calc(100vw - 80px); + } + + .import-account__title { + width: initial !important; + } + + .first-view-main { + height: 100%; + flex-direction: column; + align-items: center; + justify-content: flex-start; + margin-top: 12px; + } + + .first-view-phone-invisible { + display: none; + } + + .first-time-flow__input { + width: 100%; + } + + .import-account__secret-phrase { + width: initial !important; + height: initial !important; + min-height: 190px; + } +} + +.import-account__title { + color: #1B344D; + font-size: 40px; + line-height: 51px; + margin-bottom: 10px; +} + +.import-account__back-button { + margin-bottom: 18px; + color: #22232c; + font-size: 16px; + line-height: 21px; + position: absolute; + top: -25px; +} + +.import-account__secret-phrase { + height: 190px; + width: 495px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: #FFFFFF; + padding: 17px; + font-size: 16px; +} + +.import-account__secret-phrase::placeholder { + color: #9B9B9B; + font-weight: 200; +} + +.import-account__faq-link { + font-size: 18px; + line-height: 23px; + font-family: Roboto; +} + +.import-account__selector-label { + color: #1B344D; + font-size: 16px; +} + +.import-account__dropdown { + width: 325px; + border: 1px solid #CDCDCD; + border-radius: 4px; + background-color: #FFFFFF; + margin-top: 14px; + color: #5B5D67; + font-family: Roboto; + font-size: 18px; + line-height: 23px; + padding: 14px 21px; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; +} + +.import-account__description-text { + color: #757575; + font-size: 18px; + line-height: 23px; + margin-top: 21px; + font-family: Roboto; +} + +.import-account__input-wrapper { + display: flex; + flex-flow: column nowrap; + margin-top: 30px; +} + +.import-account__input-error-message { + margin-top: 10px; + width: 422px; + color: #FF001F; + font-size: 16px; + line-height: 21px; +} + +.import-account__input-label { + margin-bottom: 9px; + color: #1B344D; + font-size: 18px; + line-height: 23px; +} + +.import-account__input-label__disabled { + opacity: 0.5; +} + +.import-account__input { + width: 350px; +} + +@media only screen and (max-width: 575px) { + .import-account__input { + width: 100%; + } +} + +.import-account__file-input { + display: none; +} + +.import-account__file-input-label { + height: 53px; + width: 148px; + border: 1px solid #1B344D; + border-radius: 4px; + color: #1B344D; + font-family: Roboto; + font-size: 18px; + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.import-account__file-picker-wrapper { + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +.import-account__file-name { + color: #000000; + font-family: Roboto; + font-size: 18px; + line-height: 23px; + margin-left: 22px; +} diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js index d90a33e49..ce18d998c 100644 --- a/ui/app/components/pages/keychains/restore-vault.js +++ b/ui/app/components/pages/keychains/restore-vault.js @@ -7,6 +7,7 @@ import { } from '../../../actions' import { DEFAULT_ROUTE } from '../../../routes' import TextField from '../../text-field' +import Button from '../../button' class RestoreVaultPage extends Component { static contextTypes = { @@ -160,13 +161,14 @@ class RestoreVaultPage extends Component { margin="normal" largeLabel /> - <button + <Button + type="first-time" className="first-time-flow__button" onClick={() => !disabled && this.onClick()} disabled={disabled} > {this.context.t('restore')} - </button> + </Button> </div> </div> </div> diff --git a/ui/app/components/pages/lock/index.js b/ui/app/components/pages/lock/index.js new file mode 100644 index 000000000..7bfe2a61f --- /dev/null +++ b/ui/app/components/pages/lock/index.js @@ -0,0 +1 @@ +export { default } from './lock.container' diff --git a/ui/app/components/pages/lock/lock.component.js b/ui/app/components/pages/lock/lock.component.js new file mode 100644 index 000000000..51f8742ed --- /dev/null +++ b/ui/app/components/pages/lock/lock.component.js @@ -0,0 +1,26 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Loading from '../../loading-screen' +import { DEFAULT_ROUTE } from '../../../routes' + +export default class Lock extends PureComponent { + static propTypes = { + history: PropTypes.object, + isUnlocked: PropTypes.bool, + lockMetamask: PropTypes.func, + } + + componentDidMount () { + const { lockMetamask, isUnlocked, history } = this.props + + if (isUnlocked) { + lockMetamask().then(() => history.push(DEFAULT_ROUTE)) + } else { + history.replace(DEFAULT_ROUTE) + } + } + + render () { + return <Loading /> + } +} diff --git a/ui/app/components/pages/lock/lock.container.js b/ui/app/components/pages/lock/lock.container.js new file mode 100644 index 000000000..81d89ba21 --- /dev/null +++ b/ui/app/components/pages/lock/lock.container.js @@ -0,0 +1,24 @@ +import Lock from './lock.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { lockMetamask } from '../../../actions' + +const mapStateToProps = state => { + const { metamask: { isUnlocked } } = state + + return { + isUnlocked, + } +} + +const mapDispatchToProps = dispatch => { + return { + lockMetamask: () => dispatch(lockMetamask()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(Lock) diff --git a/ui/app/components/pages/metamask-route.js b/ui/app/components/pages/metamask-route.js deleted file mode 100644 index 23c5b5199..000000000 --- a/ui/app/components/pages/metamask-route.js +++ /dev/null @@ -1,28 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Route } = require('react-router-dom') -const h = require('react-hyperscript') - -const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => { - return ( - h(Route, { - ...props, - component: isMascara && mascaraComponent ? mascaraComponent : component, - }) - ) -} - -MetamaskRoute.propTypes = { - component: PropTypes.func, - mascaraComponent: PropTypes.func, - isMascara: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isMascara } } = state - return { - isMascara, - } -} - -module.exports = connect(mapStateToProps)(MetamaskRoute) diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/components/pages/unlock-page/index.scss index 6bd52282d..3d44bd037 100644 --- a/ui/app/components/pages/unlock-page/index.scss +++ b/ui/app/components/pages/unlock-page/index.scss @@ -14,7 +14,6 @@ align-self: stretch; justify-content: center; flex: 1 0 auto; - height: 100vh; } &__mascot-container { diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js index 94915df76..58a8b0566 100644 --- a/ui/app/components/pages/unlock-page/unlock-page.component.js +++ b/ui/app/components/pages/unlock-page/unlock-page.component.js @@ -2,12 +2,10 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import Button from '@material-ui/core/Button' import TextField from '../../text-field' -import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' -import { getEnvironmentType } from '../../../../../app/scripts/lib/util' import getCaretCoordinates from 'textarea-caret' import { EventEmitter } from 'events' import Mascot from '../../mascot' -import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes' +import { DEFAULT_ROUTE } from '../../../routes' export default class UnlockPage extends Component { static contextTypes = { @@ -15,12 +13,11 @@ export default class UnlockPage extends Component { } static propTypes = { - forgotPassword: PropTypes.func, - tryUnlockMetamask: PropTypes.func, - markPasswordForgotten: PropTypes.func, history: PropTypes.object, isUnlocked: PropTypes.bool, - useOldInterface: PropTypes.func, + onImport: PropTypes.func, + onRestore: PropTypes.func, + onSubmit: PropTypes.func, } constructor (props) { @@ -43,12 +40,12 @@ export default class UnlockPage extends Component { } } - async handleSubmit (event) { + handleSubmit = async event => { event.preventDefault() event.stopPropagation() const { password } = this.state - const { tryUnlockMetamask, history } = this.props + const { onSubmit } = this.props if (password === '' || this.submitting) { return @@ -58,9 +55,7 @@ export default class UnlockPage extends Component { this.submitting = true try { - await tryUnlockMetamask(password) - this.submitting = false - history.push(DEFAULT_ROUTE) + await onSubmit(password) } catch ({ message }) { this.setState({ error: message }) this.submitting = false @@ -99,7 +94,7 @@ export default class UnlockPage extends Component { fullWidth variant="raised" size="large" - onClick={event => this.handleSubmit(event)} + onClick={this.handleSubmit} disableRipple > { this.context.t('login') } @@ -110,7 +105,7 @@ export default class UnlockPage extends Component { render () { const { password, error } = this.state const { t } = this.context - const { markPasswordForgotten, history } = this.props + const { onImport, onRestore } = this.props return ( <div className="unlock-page__container"> @@ -128,7 +123,7 @@ export default class UnlockPage extends Component { <div>{ t('unlockMessage') }</div> <form className="unlock-page__form" - onSubmit={event => this.handleSubmit(event)} + onSubmit={this.handleSubmit} > <TextField id="password" @@ -147,27 +142,13 @@ export default class UnlockPage extends Component { <div className="unlock-page__links"> <div className="unlock-page__link" - onClick={() => { - markPasswordForgotten() - history.push(RESTORE_VAULT_ROUTE) - - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }} + onClick={() => onRestore()} > { t('restoreFromSeed') } </div> <div className="unlock-page__link unlock-page__link--import" - onClick={() => { - markPasswordForgotten() - history.push(RESTORE_VAULT_ROUTE) - - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }} + onClick={() => onImport()} > { t('importUsingSeed') } </div> diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js index 18fed9b2e..5f302dc37 100644 --- a/ui/app/components/pages/unlock-page/unlock-page.container.js +++ b/ui/app/components/pages/unlock-page/unlock-page.container.js @@ -1,13 +1,14 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { compose } from 'recompose' - -const { +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes' +import { tryUnlockMetamask, forgotPassword, markPasswordForgotten, -} = require('../../../actions') - +} from '../../../actions' import UnlockPage from './unlock-page.component' const mapStateToProps = state => { @@ -25,7 +26,35 @@ const mapDispatchToProps = dispatch => { } } +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { markPasswordForgotten, tryUnlockMetamask, ...restDispatchProps } = dispatchProps + const { history, onSubmit: ownPropsSubmit, ...restOwnProps } = ownProps + + const onImport = () => { + markPasswordForgotten() + history.push(RESTORE_VAULT_ROUTE) + + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser() + } + } + + const onSubmit = async password => { + await tryUnlockMetamask(password) + history.push(DEFAULT_ROUTE) + } + + return { + ...stateProps, + ...restDispatchProps, + ...restOwnProps, + onImport, + onRestore: onImport, + onSubmit: ownPropsSubmit || onSubmit, + } +} + export default compose( withRouter, - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, mapDispatchToProps, mergeProps) )(UnlockPage) diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss index 43e87459b..f3fd580d7 100644 --- a/ui/app/components/transaction-view-balance/index.scss +++ b/ui/app/components/transaction-view-balance/index.scss @@ -32,6 +32,7 @@ @media screen and (max-width: $break-small) { font-size: 1.75rem; width: 100%; + justify-content: center; } } diff --git a/ui/app/css/index.scss b/ui/app/css/index.scss index c068028f8..ffccbd64f 100644 --- a/ui/app/css/index.scss +++ b/ui/app/css/index.scss @@ -11,8 +11,6 @@ @import './itcss/generic/index.scss'; -@import './itcss/base/index.scss'; - @import './itcss/objects/index.scss'; @import './itcss/components/index.scss'; diff --git a/ui/app/css/itcss/base/index.scss b/ui/app/css/itcss/base/index.scss deleted file mode 100644 index 1475e8bb5..000000000 --- a/ui/app/css/itcss/base/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Base - -.mouse-user-styles { - button:focus { - outline: 0; - } -} diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 5826a8b49..3e99d0ac6 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -87,6 +87,18 @@ min-width: initial; } +.btn--first-time { + height: 54px; + width: 198px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); + color: $white; + font-size: 1.25rem; + font-weight: 500; + transition: 200ms ease-in-out; + background-color: rgba(247, 134, 28, .9); + border-radius: 0; +} + .btn--large { min-height: 54px; } diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index b11b76f35..7eaf60ce8 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -52,6 +52,4 @@ @import './tooltip.scss'; -@import './welcome-screen.scss'; - @import '../../../components/index'; diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss index d7ff0b8ed..a99c58a23 100644 --- a/ui/app/css/itcss/components/loading-overlay.scss +++ b/ui/app/css/itcss/components/loading-overlay.scss @@ -1,7 +1,7 @@ .loading-overlay { left: 0; z-index: 51; - position: absolute; + position: fixed; flex-direction: column; display: flex; justify-content: center; diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index a016fdce3..9a0b81aed 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -8,6 +8,15 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma // Component Colors $wallet-view-bg: $alabaster; +.app { + display: flex; + flex-direction: column; + height: 100%; + overflow-x: hidden; + position: relative; + align-items: center; +} + // Main container .main-container { // position: absolute; @@ -24,8 +33,10 @@ $wallet-view-bg: $alabaster; .main-container-wrapper { display: flex; - width: 100vw; justify-content: center; + flex: 1 0 auto; + min-height: 0; + width: 100%; } //Account and transaction details @@ -207,8 +218,6 @@ $wallet-view-bg: $alabaster; } .main-container { - // margin-top: 41px; - height: 100%; width: 100%; overflow-y: auto; background-color: $white; @@ -216,8 +225,6 @@ $wallet-view-bg: $alabaster; .main-container-wrapper { flex: 1; - min-height: 0; - width: 100%; } } diff --git a/ui/app/css/itcss/components/welcome-screen.scss b/ui/app/css/itcss/components/welcome-screen.scss deleted file mode 100644 index af1d67398..000000000 --- a/ui/app/css/itcss/components/welcome-screen.scss +++ /dev/null @@ -1,60 +0,0 @@ -.welcome-screen { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - font-family: Roboto; - font-weight: 400; - width: 100%; - flex: 1 0 auto; - padding: 70px 0; - background: $white; - - @media screen and (max-width: 575px) { - padding: 0; - } - - &__info { - display: flex; - flex-flow: column; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - - &__header { - font-size: 1.65em; - margin-bottom: 14px; - - @media screen and (max-width: 575px) { - font-size: 1.5em; - } - } - - &__copy { - font-size: 1em; - width: 400px; - max-width: 90vw; - text-align: center; - - @media screen and (max-width: 575px) { - font-size: .9em; - } - } - } - - &__button { - height: 54px; - width: 198px; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); - color: #fff; - font-size: 20px; - font-weight: 500; - line-height: 26px; - text-align: center; - text-transform: uppercase; - margin: 35px 0 14px; - transition: 200ms ease-in-out; - background-color: rgba(247, 134, 28, .9); - } -} diff --git a/ui/app/higher-order-components/authenticated/authenticated.component.js b/ui/app/higher-order-components/authenticated/authenticated.component.js new file mode 100644 index 000000000..7b64d4895 --- /dev/null +++ b/ui/app/higher-order-components/authenticated/authenticated.component.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Redirect, Route } from 'react-router-dom' +import { UNLOCK_ROUTE, INITIALIZE_ROUTE } from '../../routes' + +export default function Authenticated (props) { + const { isUnlocked, completedOnboarding } = props + + switch (true) { + case isUnlocked && completedOnboarding: + return <Route { ...props } /> + case !completedOnboarding: + return <Redirect to={{ pathname: INITIALIZE_ROUTE }} /> + default: + return <Redirect to={{ pathname: UNLOCK_ROUTE }} /> + } +} + +Authenticated.propTypes = { + isUnlocked: PropTypes.bool, + completedOnboarding: PropTypes.bool, +} diff --git a/ui/app/higher-order-components/authenticated/authenticated.container.js b/ui/app/higher-order-components/authenticated/authenticated.container.js new file mode 100644 index 000000000..6124b0fcd --- /dev/null +++ b/ui/app/higher-order-components/authenticated/authenticated.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Authenticated from './authenticated.component' + +const mapStateToProps = state => { + const { metamask: { isUnlocked, completedOnboarding } } = state + return { + isUnlocked, + completedOnboarding, + } +} + +export default connect(mapStateToProps)(Authenticated) diff --git a/ui/app/higher-order-components/authenticated/index.js b/ui/app/higher-order-components/authenticated/index.js new file mode 100644 index 000000000..05632ed21 --- /dev/null +++ b/ui/app/higher-order-components/authenticated/index.js @@ -0,0 +1 @@ +export { default } from './authenticated.container' diff --git a/ui/app/higher-order-components/initialized/index.js b/ui/app/higher-order-components/initialized/index.js new file mode 100644 index 000000000..863fcb389 --- /dev/null +++ b/ui/app/higher-order-components/initialized/index.js @@ -0,0 +1 @@ +export { default } from './initialized.container.js' diff --git a/ui/app/higher-order-components/initialized/initialized.component.js b/ui/app/higher-order-components/initialized/initialized.component.js new file mode 100644 index 000000000..0736ceff4 --- /dev/null +++ b/ui/app/higher-order-components/initialized/initialized.component.js @@ -0,0 +1,14 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Redirect, Route } from 'react-router-dom' +import { INITIALIZE_ROUTE } from '../../routes' + +export default function Initialized (props) { + return props.completedOnboarding + ? <Route { ...props } /> + : <Redirect to={{ pathname: INITIALIZE_ROUTE }} /> +} + +Initialized.propTypes = { + completedOnboarding: PropTypes.bool, +} diff --git a/ui/app/higher-order-components/initialized/initialized.container.js b/ui/app/higher-order-components/initialized/initialized.container.js new file mode 100644 index 000000000..0e7f72bcb --- /dev/null +++ b/ui/app/higher-order-components/initialized/initialized.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Initialized from './initialized.component' + +const mapStateToProps = state => { + const { metamask: { completedOnboarding } } = state + + return { + completedOnboarding, + } +} + +export default connect(mapStateToProps)(Initialized) diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 97052ab87..632ec18f8 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -54,6 +54,7 @@ function reduceMetamask (state, action) { preferences: { useNativeCurrencyAsPrimaryCurrency: true, }, + completedOnboarding: false, knownMethodData: {}, }, state.metamask) @@ -378,6 +379,12 @@ function reduceMetamask (state, action) { }) } + case actions.COMPLETE_ONBOARDING: { + return extend(metamaskState, { + completedOnboarding: true, + }) + } + default: return metamaskState diff --git a/ui/app/routes.js b/ui/app/routes.js index 76afed5db..fcf3d3e68 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -1,5 +1,6 @@ const DEFAULT_ROUTE = '/' const UNLOCK_ROUTE = '/unlock' +const LOCK_ROUTE = '/lock' const SETTINGS_ROUTE = '/settings' const INFO_ROUTE = '/settings/info' const REVEAL_SEED_ROUTE = '/seed' @@ -14,14 +15,17 @@ const CONNECT_HARDWARE_ROUTE = '/new-account/connect' const SEND_ROUTE = '/send' const NOTICE_ROUTE = '/notice' const WELCOME_ROUTE = '/welcome' + const INITIALIZE_ROUTE = '/initialize' +const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome' +const INITIALIZE_UNLOCK_ROUTE = '/initialize/unlock' const INITIALIZE_CREATE_PASSWORD_ROUTE = '/initialize/create-password' -const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/import-account' -const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/import-with-seed-phrase' -const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/unique-image' +const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/create-password/import-account' +const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/create-password/import-with-seed-phrase' +const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/create-password/unique-image' const INITIALIZE_NOTICE_ROUTE = '/initialize/notice' -const INITIALIZE_BACKUP_PHRASE_ROUTE = '/initialize/backup-phrase' -const INITIALIZE_CONFIRM_SEED_ROUTE = '/initialize/confirm-phrase' +const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase' +const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm' const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' const CONFIRM_SEND_ETHER_PATH = '/send-ether' @@ -35,6 +39,7 @@ const SIGNATURE_REQUEST_PATH = '/signature-request' module.exports = { DEFAULT_ROUTE, UNLOCK_ROUTE, + LOCK_ROUTE, SETTINGS_ROUTE, INFO_ROUTE, REVEAL_SEED_ROUTE, @@ -50,13 +55,15 @@ module.exports = { NOTICE_ROUTE, WELCOME_ROUTE, INITIALIZE_ROUTE, + INITIALIZE_WELCOME_ROUTE, + INITIALIZE_UNLOCK_ROUTE, INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_IMPORT_ACCOUNT_ROUTE, INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, INITIALIZE_UNIQUE_IMAGE_ROUTE, INITIALIZE_NOTICE_ROUTE, - INITIALIZE_BACKUP_PHRASE_ROUTE, - INITIALIZE_CONFIRM_SEED_ROUTE, + INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, CONFIRM_TRANSACTION_ROUTE, CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_TOKEN_PATH, diff --git a/ui/app/welcome-screen.js b/ui/app/welcome-screen.js deleted file mode 100644 index 146661eb3..000000000 --- a/ui/app/welcome-screen.js +++ /dev/null @@ -1,83 +0,0 @@ -import EventEmitter from 'events' -import h from 'react-hyperscript' -import { Component } from 'react' -import PropTypes from 'prop-types' -import {connect} from 'react-redux' -import { withRouter } from 'react-router-dom' -import { compose } from 'recompose' -import {closeWelcomeScreen} from './actions' -import Mascot from './components/mascot' -import { INITIALIZE_CREATE_PASSWORD_ROUTE } from './routes' - -class WelcomeScreen extends Component { - static propTypes = { - closeWelcomeScreen: PropTypes.func.isRequired, - welcomeScreenSeen: PropTypes.bool, - history: PropTypes.object, - t: PropTypes.func, - } - - static contextTypes = { - t: PropTypes.func, - } - - constructor (props) { - super(props) - this.animationEventEmitter = new EventEmitter() - } - - componentWillMount () { - const { history, welcomeScreenSeen } = this.props - - if (welcomeScreenSeen) { - history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) - } - } - - initiateAccountCreation = () => { - this.props.closeWelcomeScreen() - this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE) - } - - render () { - return h('div.welcome-screen', [ - - h('div.welcome-screen__info', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - width: '225', - height: '225', - }), - - h('div.welcome-screen__info__header', this.context.t('welcome')), - - h('div.welcome-screen__info__copy', this.context.t('metamaskDescription')), - - h('div.welcome-screen__info__copy', this.context.t('holdEther')), - - h('button.welcome-screen__button', { - onClick: this.initiateAccountCreation, - }, this.context.t('continue')), - - ]), - - ]) - } -} - -const mapStateToProps = ({ metamask: { welcomeScreenSeen } }) => { - return { - welcomeScreenSeen, - } -} - -export default compose( - withRouter, - connect( - mapStateToProps, - dispatch => ({ - closeWelcomeScreen: () => dispatch(closeWelcomeScreen()), - }) - ) -)(WelcomeScreen) @@ -5,7 +5,6 @@ module.exports = bundleCss var cssFiles = { 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/output/index.css'), 'utf8'), - 'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'), 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } |