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/app/components/pages/first-time-flow/create-password | |
parent | 69fcfa427bdee2ea287e9d9c23963dc1032685cd (diff) | |
download | tangerine-wallet-browser-fba17d77de9e60de0e02e90dc6dbcbbf7454158a.tar.gz 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/app/components/pages/first-time-flow/create-password')
10 files changed, 534 insertions, 0 deletions
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) |