diff options
author | Chi Kei Chan <chikeichan@gmail.com> | 2019-03-22 07:03:30 +0800 |
---|---|---|
committer | Dan J Miller <danjm.com@gmail.com> | 2019-03-22 07:03:30 +0800 |
commit | 31175625b446cb5d18b17db23018bca8b14d280c (patch) | |
tree | f54e159883deef003fb281267025edf796eb8004 /ui/app/pages/first-time-flow | |
parent | 7287133e15fab22299e07704206e85bc855d1064 (diff) | |
download | tangerine-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')
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) |