aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages/first-time-flow
diff options
context:
space:
mode:
authorChi Kei Chan <chikeichan@gmail.com>2019-03-22 07:03:30 +0800
committerDan J Miller <danjm.com@gmail.com>2019-03-22 07:03:30 +0800
commit31175625b446cb5d18b17db23018bca8b14d280c (patch)
treef54e159883deef003fb281267025edf796eb8004 /ui/app/pages/first-time-flow
parent7287133e15fab22299e07704206e85bc855d1064 (diff)
downloadtangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar.gz
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar.bz2
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar.lz
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar.xz
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.tar.zst
tangerine-wallet-browser-31175625b446cb5d18b17db23018bca8b14d280c.zip
Folder restructure (#6304)
* Remove ui/app/keychains/ * Remove ui/app/img/ (unused images) * Move conversion-util to helpers/utils/ * Move token-util to helpers/utils/ * Move /helpers/*.js inside /helpers/utils/ * Move util tests inside /helpers/utils/ * Renameand move confirm-transaction/util.js to helpers/utils/ * Move higher-order-components to helpers/higher-order-components/ * Move infura-conversion.json to helpers/constants/ * Move all utility functions to helpers/utils/ * Move pages directory to top-level * Move all constants to helpers/constants/ * Move metametrics inside helpers/ * Move app and root inside pages/ * Move routes inside helpers/ * Re-organize ducks/ * Move reducers to ducks/ * Move selectors inside selectors/ * Move test out of test folder * Move action, reducer, store inside store/ * Move ui components inside ui/ * Move UI components inside ui/ * Move connected components inside components/app/ * Move i18n-helper inside helpers/ * Fix unit tests * Fix unit test * Move pages components * Rename routes component * Move reducers to ducks/index * Fix bad path in unit test
Diffstat (limited to 'ui/app/pages/first-time-flow')
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.component.js71
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.container.js12
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js256
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js225
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js55
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js12
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js93
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js25
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.js1
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.scss53
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js57
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js20
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/index.js1
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.component.js152
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.container.js31
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.selectors.js26
-rw-r--r--ui/app/pages/first-time-flow/index.js1
-rw-r--r--ui/app/pages/first-time-flow/index.scss159
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.js1
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.scss136
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js169
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js27
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js155
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js41
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss48
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.scss40
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss57
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js143
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js70
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.js1
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.scss88
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.component.js112
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.container.js23
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.js1
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.scss42
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.component.js69
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.container.js26
44 files changed, 2506 insertions, 0 deletions
diff --git a/ui/app/pages/first-time-flow/create-password/create-password.component.js b/ui/app/pages/first-time-flow/create-password/create-password.component.js
new file mode 100644
index 000000000..5e67a2244
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/create-password.component.js
@@ -0,0 +1,71 @@
+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 {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../helpers/constants/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_SEED_PHRASE_ROUTE)
+ }
+ }
+
+ render () {
+ const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <Switch>
+ <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/pages/first-time-flow/create-password/create-password.container.js b/ui/app/pages/first-time-flow/create-password/create-password.container.js
new file mode 100644
index 000000000..89106f016
--- /dev/null
+++ b/ui/app/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/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
new file mode 100644
index 000000000..433dad6e2
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
@@ -0,0 +1,256 @@
+import {validateMnemonic} from 'bip39'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TextField from '../../../../components/ui/text-field'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+} from '../../../../helpers/constants/routes'
+
+export default class ImportWithSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: PropTypes.func.isRequired,
+ }
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .trim()
+ .match(/\w+/g)
+ .join(' ')
+ }
+
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = ''
+
+ if (seedPhrase) {
+ const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase)
+ if (parsedSeedPhrase.split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ } else if (!validateMnemonic(parsedSeedPhrase)) {
+ 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, this.parseSeedPhrase(seedPhrase))
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Import Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_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
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <form
+ className="first-time-flow__form"
+ onSubmit={this.handleImport}
+ >
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Go Back from Onboarding Import',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_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
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleImport}
+ >
+ { t('import') }
+ </Button>
+ </form>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
new file mode 100644
index 000000000..e5ff1fde5
--- /dev/null
+++ b/ui/app/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/pages/first-time-flow/create-password/index.js b/ui/app/pages/first-time-flow/create-password/index.js
new file mode 100644
index 000000000..42e7436f9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/index.js
@@ -0,0 +1 @@
+export { default } from './create-password.container'
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/index.js b/ui/app/pages/first-time-flow/create-password/new-account/index.js
new file mode 100644
index 000000000..97db39cc3
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/index.js
@@ -0,0 +1 @@
+export { default } from './new-account.component'
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
new file mode 100644
index 000000000..c040cff88
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
@@ -0,0 +1,225 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+} from '../../../../helpers/constants/routes'
+import TextField from '../../../../components/ui/text-field'
+
+export default class NewAccount extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onSubmit: PropTypes.func.isRequired,
+ history: PropTypes.object.isRequired,
+ }
+
+ state = {
+ password: '',
+ confirmPassword: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ 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)
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Submit Password',
+ },
+ })
+
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ } catch (error) {
+ this.setState({ passwordError: error.message })
+ }
+ }
+
+ handleImportWithSeedPhrase = event => {
+ const { history } = this.props
+
+ event.preventDefault()
+ history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { password, confirmPassword, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <div>
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Go Back from Onboarding Create',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </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
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleCreate}
+ >
+ { t('create') }
+ </Button>
+ </form>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/index.js b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
new file mode 100644
index 000000000..0e97bf755
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
@@ -0,0 +1 @@
+export { default } from './unique-image.container'
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
new file mode 100644
index 000000000..3434d117a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
@@ -0,0 +1,55 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class UniqueImageScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div>
+ <img
+ src="/images/sleuth.svg"
+ height={42}
+ width={42}
+ />
+ <div className="first-time-flow__header">
+ { t('protectYourKeys') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage1') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage2') }
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Agree to Phishing Warning',
+ name: 'Agree to Phishing Warning',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ }}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
new file mode 100644
index 000000000..34874aaec
--- /dev/null
+++ b/ui/app/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/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
new file mode 100644
index 000000000..c4292331b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
@@ -0,0 +1,93 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
+
+export default class EndOfFlowScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ completeOnboarding: PropTypes.func,
+ completionMetaMetricsName: PropTypes.string,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history, completeOnboarding, completionMetaMetricsName } = this.props
+
+ return (
+ <div className="end-of-flow">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <div className="end-of-flow__emoji">🎉</div>
+ <div className="first-time-flow__header">
+ { t('congratulations') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-1">
+ { t('endOfFlowMessage1') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-2">
+ { t('endOfFlowMessage2') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage3') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage4') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage5') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage6') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage7') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-4">
+ *MetaMask cannot recover your seedphrase. <a
+ href="https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-Tips"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ Learn More
+ </span>
+ </a>.
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={async () => {
+ await completeOnboarding()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Onboarding Complete',
+ name: completionMetaMetricsName,
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { 'All Done' }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
new file mode 100644
index 000000000..38313806c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux'
+import EndOfFlow from './end-of-flow.component'
+import { setCompletedOnboarding } from '../../../store/actions'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'New Wallet Created',
+ 'import': 'New Wallet Imported',
+}
+
+const mapStateToProps = ({ metamask }) => {
+ const { firstTimeFlowType } = metamask
+
+ return {
+ completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ }
+}
+
+
+const mapDispatchToProps = dispatch => {
+ return {
+ completeOnboarding: () => dispatch(setCompletedOnboarding()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)
diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.js b/ui/app/pages/first-time-flow/end-of-flow/index.js
new file mode 100644
index 000000000..b0643d155
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.js
@@ -0,0 +1 @@
+export { default } from './end-of-flow.container'
diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.scss b/ui/app/pages/first-time-flow/end-of-flow/index.scss
new file mode 100644
index 000000000..d7eb4513b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.scss
@@ -0,0 +1,53 @@
+.end-of-flow {
+ color: black;
+ font-family: Roboto;
+ font-style: normal;
+
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ }
+ }
+
+ &__text-1, &__text-3 {
+ font-weight: normal;
+ font-size: 16px;
+ margin-top: 18px;
+ }
+
+ &__text-2 {
+ font-weight: bold;
+ font-size: 16px;
+ margin-top: 26px;
+ }
+
+ &__text-3 {
+ margin-top: 2px;
+ margin-bottom: 2px;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__text-4 {
+ margin-top: 26px;
+ }
+
+ button {
+ width: 207px;
+ }
+
+ &__start-over-button {
+ width: 744px;
+ }
+
+ &__emoji {
+ font-size: 80px;
+ margin-top: 70px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
new file mode 100644
index 000000000..4fd028482
--- /dev/null
+++ b/ui/app/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_UNLOCK_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class FirstTimeFlowSwitch extends PureComponent {
+ static propTypes = {
+ completedOnboarding: PropTypes.bool,
+ isInitialized: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ seedPhrase: PropTypes.string,
+ optInMetaMetrics: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ seedPhrase,
+ optInMetaMetrics,
+ } = 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 (seedPhrase) {
+ return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
+ }
+
+ if (optInMetaMetrics === null) {
+ return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ }
+
+ return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
+ }
+}
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
new file mode 100644
index 000000000..d68f7a153
--- /dev/null
+++ b/ui/app/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,
+ participateInMetaMetrics: optInMetaMetrics,
+ } = metamask
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ optInMetaMetrics,
+ }
+}
+
+export default connect(mapStateToProps)(FirstTimeFlowSwitch)
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/index.js b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
new file mode 100644
index 000000000..3647756ef
--- /dev/null
+++ b/ui/app/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/pages/first-time-flow/first-time-flow.component.js b/ui/app/pages/first-time-flow/first-time-flow.component.js
new file mode 100644
index 000000000..bf6e80ca9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.component.js
@@ -0,0 +1,152 @@
+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 SelectAction from './select-action'
+import EndOfFlow from './end-of-flow'
+import Unlock from '../unlock-page'
+import CreatePassword from './create-password'
+import SeedPhrase from './seed-phrase'
+import MetaMetricsOptInScreen from './metametrics-opt-in'
+import {
+ DEFAULT_ROUTE,
+ INITIALIZE_WELCOME_ROUTE,
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../helpers/constants/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,
+ unlockAccount: PropTypes.func,
+ nextRoute: 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, nextRoute } = this.props
+
+ try {
+ const seedPhrase = await unlockAccount(password)
+ this.setState({ seedPhrase }, () => {
+ history.push(nextRoute)
+ })
+ } 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
+ path={INITIALIZE_CREATE_PASSWORD_ROUTE}
+ render={props => (
+ <CreatePassword
+ { ...props }
+ isImportedKeyring={isImportedKeyring}
+ onCreateNewAccount={this.handleCreateNewAccount}
+ onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
+ />
+ )}
+ />
+ <Route
+ path={INITIALIZE_SELECT_ACTION_ROUTE}
+ component={SelectAction}
+ />
+ <Route
+ path={INITIALIZE_UNLOCK_ROUTE}
+ render={props => (
+ <Unlock
+ { ...props }
+ onSubmit={this.handleUnlock}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_END_OF_FLOW_ROUTE}
+ component={EndOfFlow}
+ />
+ <Route
+ exact
+ path={INITIALIZE_WELCOME_ROUTE}
+ component={Welcome}
+ />
+ <Route
+ exact
+ path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
+ component={MetaMetricsOptInScreen}
+ />
+ <Route
+ exact
+ path="*"
+ component={FirstTimeFlowSwitch}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/first-time-flow.container.js b/ui/app/pages/first-time-flow/first-time-flow.container.js
new file mode 100644
index 000000000..16025a489
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import FirstTimeFlow from './first-time-flow.component'
+import { getFirstTimeFlowTypeRoute } from './first-time-flow.selectors'
+import {
+ createNewVaultAndGetSeedPhrase,
+ createNewVaultAndRestore,
+ unlockAndGetSeedPhrase,
+} from '../../store/actions'
+
+const mapStateToProps = state => {
+ const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+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/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
new file mode 100644
index 000000000..e6cd5a84a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
@@ -0,0 +1,26 @@
+import {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ DEFAULT_ROUTE,
+} from '../../helpers/constants/routes'
+
+const selectors = {
+ getFirstTimeFlowTypeRoute,
+}
+
+module.exports = selectors
+
+function getFirstTimeFlowTypeRoute (state) {
+ const { firstTimeFlowType } = state.metamask
+
+ let nextRoute
+ if (firstTimeFlowType === 'create') {
+ nextRoute = INITIALIZE_CREATE_PASSWORD_ROUTE
+ } else if (firstTimeFlowType === 'import') {
+ nextRoute = INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE
+ } else {
+ nextRoute = DEFAULT_ROUTE
+ }
+
+ return nextRoute
+}
diff --git a/ui/app/pages/first-time-flow/index.js b/ui/app/pages/first-time-flow/index.js
new file mode 100644
index 000000000..5db42437c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.js
@@ -0,0 +1 @@
+export { default } from './first-time-flow.container'
diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss
new file mode 100644
index 000000000..6c65cfdae
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.scss
@@ -0,0 +1,159 @@
+@import 'welcome/index';
+
+@import 'select-action/index';
+
+@import 'seed-phrase/index';
+
+@import 'end-of-flow/index';
+
+@import 'metametrics-opt-in/index';
+
+
+.first-time-flow {
+ width: 100%;
+ background-color: $white;
+ display: flex;
+ justify-content: center;
+
+ &__wrapper {
+ @media screen and (min-width: $break-large) {
+ max-width: 742px;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: 2%;
+ }
+
+ .app-header__metafox-logo {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__form {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__create-back {
+ margin-bottom: 16px;
+ }
+
+ &__header {
+ font-size: 2.5rem;
+ margin-bottom: 24px;
+ color: black;
+ }
+
+ &__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;
+ color: black;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__button {
+ margin: 35px 0 14px;
+ width: 140px;
+ height: 44px;
+ }
+
+ &__checkbox-container {
+ display: flex;
+ align-items: center;
+ margin-top: 24px;
+ }
+
+ &__checkbox {
+ background: #FFFFFF;
+ border: 1px solid #CDCDCD;
+ box-sizing: border-box;
+ height: 34px;
+ width: 34px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ border: 1.5px solid #2f9ae0;
+ }
+
+ .fa-check {
+ color: #2f9ae0
+ }
+ }
+
+ &__checkbox-label {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 18px;
+ color: #939090;
+ margin-left: 18px;
+ }
+
+ &__link-text {
+ color: $curious-blue;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/index.js b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
new file mode 100644
index 000000000..4bc2fc3a7
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in.container'
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
new file mode 100644
index 000000000..6c2e37785
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
@@ -0,0 +1,136 @@
+.metametrics-opt-in {
+ position: relative;
+ width: 100%;
+
+ a {
+ color: #2f9ae0bf;
+ }
+
+ &__main {
+ display: flex;
+ flex-direction: column;
+ margin-left: 26.26%;
+ margin-right: 28%;
+ color: black;
+
+ @media screen and (max-width: 575px) {
+ justify-content: center;
+ margin-left: 2%;
+ margin-right: 0%;
+ }
+
+ .app-header__logo-container {
+ margin-top: 3%;
+ }
+ }
+
+ &__title {
+ position: relative;
+ margin-top: 20px;
+
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 42px;
+ }
+
+ &__body-graphic {
+ margin-top: 25px;
+
+ .fa-bar-chart {
+ color: #C4C4C4;
+ }
+ }
+
+ &__description {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 21px;
+ font-size: 16px;
+ margin-top: 12px;
+ }
+
+ &__committments {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__content {
+ overflow-y: scroll;
+ flex: 1;
+ }
+
+ &__row {
+ display: flex;
+ margin-top: 8px;
+
+ .fa-check {
+ margin-right: 12px;
+ color: #1ACC56;
+ }
+
+ .fa-times {
+ margin-right: 12px;
+ color: #D0021B;
+ }
+ }
+
+ &__bold {
+ font-weight: bold;
+ }
+
+ &__break-row {
+ margin-top: 30px;
+ }
+
+ &__body {
+ position: relative;
+ display: flex;
+ max-width: 730px;
+ flex-direction: column;
+ }
+
+ &__body-text {
+ max-width: 548px;
+ margin-left: 16px;
+ margin-right: 16px;
+ }
+
+ &__bottom-text {
+ margin-top: 10px;
+ color: #9a9a9a;
+ }
+
+ &__content {
+ overflow-y: auto;
+ }
+
+ &__footer {
+ margin-top: 26px;
+
+ @media screen and (max-width: 575px) {
+ margin-top: 10px;
+ justify-content: center;
+ margin-left: 2%;
+ max-height: 520px;
+ }
+
+ .page-container__footer {
+ border-top: none;
+ max-width: 535px;
+ margin-bottom: 15px;
+
+ button {
+ height: 44px;
+ min-height: 44px;
+ margin-right: 16px;
+ }
+
+ header {
+ padding: 0px;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
new file mode 100644
index 000000000..19c668278
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
@@ -0,0 +1,169 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'
+
+export default class MetaMetricsOptIn extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ setParticipateInMetaMetrics: PropTypes.func,
+ nextRoute: PropTypes.string,
+ firstTimeSelectionMetaMetricsName: PropTypes.string,
+ participateInMetaMetrics: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ render () {
+ const { metricsEvent } = this.context
+ const {
+ nextRoute,
+ history,
+ setParticipateInMetaMetrics,
+ firstTimeSelectionMetaMetricsName,
+ participateInMetaMetrics,
+ } = this.props
+
+ return (
+ <div className="metametrics-opt-in">
+ <div className="metametrics-opt-in__main">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <div className="metametrics-opt-in__body-graphic">
+ <img src="images/metrics-chart.svg" />
+ </div>
+ <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
+ <div className="metametrics-opt-in__body">
+ <div className="metametrics-opt-in__description">
+ MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
+ will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
+ </div>
+ <div className="metametrics-opt-in__description">
+ MetaMask will..
+ </div>
+
+ <div className="metametrics-opt-in__committments">
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Always allow you to opt-out via Settings
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Send anonymized click & pageview events
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Maintain a public aggregate dashboard to educate the community
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="metametrics-opt-in__footer">
+ <PageContainerFooter
+ onCancel={() => {
+ setParticipateInMetaMetrics(false)
+ .then(() => {
+ const promise = participateInMetaMetrics !== false
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt Out',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ cancelText={'No Thanks'}
+ hideCancel={false}
+ onSubmit={() => {
+ setParticipateInMetaMetrics(true)
+ .then(([participateStatus, metaMetricsId]) => {
+ const promise = participateInMetaMetrics !== true
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt In',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ return metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import or Create',
+ name: firstTimeSelectionMetaMetricsName,
+ },
+ isOptIn: true,
+ metaMetricsId,
+ })
+ })
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ submitText={'I agree'}
+ submitButtonType={'confirm'}
+ disabled={false}
+ />
+ <div className="metametrics-opt-in__bottom-text">
+ This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Privacy Policy here
+ </a>.
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
new file mode 100644
index 000000000..2566a2a56
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import MetaMetricsOptIn from './metametrics-opt-in.component'
+import { setParticipateInMetaMetrics } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'Selected Create New Wallet',
+ 'import': 'Selected Import Wallet',
+}
+
+const mapStateToProps = (state) => {
+ const { firstTimeFlowType, participateInMetaMetrics } = state.metamask
+
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ firstTimeSelectionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MetaMetricsOptIn)
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
new file mode 100644
index 000000000..59b4f73a6
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
@@ -0,0 +1,155 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import shuffle from 'lodash.shuffle'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
+
+export default class ConfirmSeedPhrase extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ seedPhrase: '',
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: 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 { history } = this.props
+
+ if (!this.isValid()) {
+ return
+ }
+
+ try {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Verify Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ } 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 { history } = this.props
+ const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
+
+ return (
+ <div className="confirm-seed-phrase">
+ <div className="confirm-seed-phrase__back-button">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </div>
+ <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="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleSubmit}
+ disabled={!this.isValid()}
+ >
+ { t('confirm') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/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/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/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
new file mode 100644
index 000000000..c7b511503
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
new file mode 100644
index 000000000..93137618c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
@@ -0,0 +1,48 @@
+.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;
+ }
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/index.js
new file mode 100644
index 000000000..185b3f089
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/index.scss
new file mode 100644
index 000000000..24da45ded
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.scss
@@ -0,0 +1,40 @@
+@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: 81px;
+ }
+
+ @media screen and (max-width: $break-small) {
+ margin-top: 24px;
+ }
+
+ .first-time-flow__text-block {
+ color: #5A5A5A;
+ }
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
new file mode 100644
index 000000000..4a1b191b5
--- /dev/null
+++ b/ui/app/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/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
new file mode 100644
index 000000000..8a47447ed
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
@@ -0,0 +1,57 @@
+.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;
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
new file mode 100644
index 000000000..ee352d74e
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
@@ -0,0 +1,143 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import LockIcon from '../../../../components/ui/lock-icon'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+
+export default class RevealSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ 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
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Advance to Verify',
+ },
+ })
+
+ 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.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Revealed Words',
+ },
+ })
+ 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 { isShowingSeedPhrase } = this.state
+
+ return (
+ <div className="reveal-seed-phrase">
+ <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="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleNext}
+ disabled={!isShowingSeedPhrase}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
new file mode 100644
index 000000000..9a9f84049
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
@@ -0,0 +1,70 @@
+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 '../../../helpers/constants/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 { seedPhrase } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
+ render={props => (
+ <ConfirmSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_SEED_PHRASE_ROUTE}
+ render={props => (
+ <RevealSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/select-action/index.js b/ui/app/pages/first-time-flow/select-action/index.js
new file mode 100644
index 000000000..4fbe1823b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.js
@@ -0,0 +1 @@
+export { default } from './select-action.container'
diff --git a/ui/app/pages/first-time-flow/select-action/index.scss b/ui/app/pages/first-time-flow/select-action/index.scss
new file mode 100644
index 000000000..e1b22d05b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.scss
@@ -0,0 +1,88 @@
+.select-action {
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__body-header {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 39px;
+ font-size: 28px;
+ text-align: center;
+ margin-top: 65px;
+ color: black;
+ }
+
+ &__select-buttons {
+ display: flex;
+ flex-direction: row;
+ margin-top: 40px;
+ }
+
+ &__select-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-evenly;
+ width: 388px;
+ height: 278px;
+
+ border: 1px solid #D8D8D8;
+ box-sizing: border-box;
+ border-radius: 10px;
+ margin-left: 22px;
+
+ .first-time-flow__button {
+ max-width: 221px;
+ height: 44px;
+ }
+ }
+
+ &__button-symbol {
+ color: #C4C4C4;
+ margin-top: 41px;
+ }
+
+ &__button-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 144px;
+ }
+
+ &__button-text-big {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 28px;
+ font-size: 20px;
+ color: #000000;
+ margin-top: 12px;
+ text-align: center;
+ }
+
+ &__button-text-small {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 20px;
+ font-size: 14px;
+ color: #7A7A7B;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ button {
+ font-weight: 500;
+ width: 221px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.component.js b/ui/app/pages/first-time-flow/select-action/select-action.component.js
new file mode 100644
index 000000000..b25a15514
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.component.js
@@ -0,0 +1,112 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import {
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class SelectAction extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ setFirstTimeFlowType: PropTypes.func,
+ nextRoute: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { history, isInitialized, nextRoute } = this.props
+
+ if (isInitialized) {
+ history.push(nextRoute)
+ }
+ }
+
+ handleCreate = () => {
+ this.props.setFirstTimeFlowType('create')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ handleImport = () => {
+ this.props.setFirstTimeFlowType('import')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="select-action">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+
+ <div className="select-action__wrapper">
+
+
+ <div className="select-action__body">
+ <div className="select-action__body-header">
+ { t('newToMetaMask') }
+ </div>
+ <div className="select-action__select-buttons">
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/download-alt.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('noAlreadyHaveSeed') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('importYourExisting') }
+ </div>
+ </div>
+ <Button
+ type="primary"
+ className="first-time-flow__button"
+ onClick={this.handleImport}
+ >
+ { t('importWallet') }
+ </Button>
+ </div>
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/thin-plus.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('letsGoSetUp') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('thisWillCreate') }
+ </div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleCreate}
+ >
+ { t('createAWallet') }
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.container.js b/ui/app/pages/first-time-flow/select-action/select-action.container.js
new file mode 100644
index 000000000..9dc988430
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.container.js
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { setFirstTimeFlowType } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+import Welcome from './select-action.component'
+
+const mapStateToProps = (state) => {
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setFirstTimeFlowType: type => dispatch(setFirstTimeFlowType(type)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)
diff --git a/ui/app/pages/first-time-flow/welcome/index.js b/ui/app/pages/first-time-flow/welcome/index.js
new file mode 100644
index 000000000..8abeddaa1
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.js
@@ -0,0 +1 @@
+export { default } from './welcome.container'
diff --git a/ui/app/pages/first-time-flow/welcome/index.scss b/ui/app/pages/first-time-flow/welcome/index.scss
new file mode 100644
index 000000000..3b5071480
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.scss
@@ -0,0 +1,42 @@
+.welcome-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ max-width: 442px;
+ padding: 0 18px;
+ color: black;
+
+ &__wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ height: 100%;
+ margin-top: 110px;
+ }
+
+ &__header {
+ font-size: 28px;
+ margin-bottom: 22px;
+ margin-top: 50px;
+ }
+
+ &__description {
+ text-align: center;
+
+ div {
+ font-size: 16px;
+ }
+
+ @media screen and (max-width: 575px) {
+ font-size: .9rem;
+ }
+ }
+
+ .first-time-flow__button {
+ width: 184px;
+ font-weight: 500;
+ margin-top: 44px;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.component.js b/ui/app/pages/first-time-flow/welcome/welcome.component.js
new file mode 100644
index 000000000..3b8d6eb17
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.component.js
@@ -0,0 +1,69 @@
+import EventEmitter from 'events'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Mascot from '../../../components/ui/mascot'
+import Button from '../../../components/ui/button'
+import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE } from '../../../helpers/constants/routes'
+
+export default class Welcome extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ participateInMetaMetrics: PropTypes.bool,
+ welcomeScreenSeen: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentDidMount () {
+ const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props
+
+ if (welcomeScreenSeen && participateInMetaMetrics !== null) {
+ history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
+ } else if (welcomeScreenSeen) {
+ history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+ }
+
+ handleContinue = () => {
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="welcome-page__wrapper">
+ <div className="welcome-page">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="125"
+ height="125"
+ />
+ <div className="welcome-page__header">
+ { t('welcome') }
+ </div>
+ <div className="welcome-page__description">
+ <div>{ t('metamaskDescription') }</div>
+ <div>{ t('happyToSeeYou') }</div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleContinue}
+ >
+ { t('getStarted') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.container.js b/ui/app/pages/first-time-flow/welcome/welcome.container.js
new file mode 100644
index 000000000..ce4b2b471
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { closeWelcomeScreen } from '../../../store/actions'
+import Welcome from './welcome.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { welcomeScreenSeen, isInitialized, participateInMetaMetrics } = metamask
+
+ return {
+ welcomeScreenSeen,
+ isInitialized,
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)