diff options
Diffstat (limited to 'ui/app/components')
62 files changed, 2171 insertions, 195 deletions
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; } } |