diff options
Diffstat (limited to 'ui/app/components/pages/first-time-flow/seed-phrase')
12 files changed, 560 insertions, 0 deletions
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js new file mode 100644 index 000000000..bc0f73a27 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js @@ -0,0 +1,161 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import shuffle from 'lodash.shuffle' +import Identicon from '../../../../identicon' +import Button from '../../../../button' +import Breadcrumbs from '../../../../breadcrumbs' +import { DEFAULT_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../../routes' +import { exportAsFile } from '../../../../../../app/util' +import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state' + +export default class ConfirmSeedPhrase extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + seedPhrase: '', + } + + static propTypes = { + address: PropTypes.string, + completeOnboarding: PropTypes.func, + history: PropTypes.object, + onSubmit: PropTypes.func, + openBuyEtherModal: PropTypes.func, + seedPhrase: PropTypes.string, + } + + state = { + selectedSeedWords: [], + shuffledSeedWords: [], + // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number} + selectedSeedWordsHash: {}, + } + + componentDidMount () { + const { seedPhrase = '' } = this.props + const shuffledSeedWords = shuffle(seedPhrase.split(' ')) || [] + this.setState({ shuffledSeedWords }) + } + + handleExport = () => { + exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') + } + + handleSubmit = async () => { + const { completeOnboarding, history, openBuyEtherModal } = this.props + + if (!this.isValid()) { + return + } + + try { + await completeOnboarding() + history.push(DEFAULT_ROUTE) + openBuyEtherModal() + } catch (error) { + console.error(error.message) + } + } + + handleSelectSeedWord = (word, shuffledIndex) => { + this.setState(selectSeedWord(word, shuffledIndex)) + } + + handleDeselectSeedWord = shuffledIndex => { + this.setState(deselectSeedWord(shuffledIndex)) + } + + isValid () { + const { seedPhrase } = this.props + const { selectedSeedWords } = this.state + return seedPhrase === selectedSeedWords.join(' ') + } + + render () { + const { t } = this.context + const { address, history } = this.props + const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state + + return ( + <div> + <div className="confirm-seed-phrase__back-button"> + <a + onClick={e => { + e.preventDefault() + history.push(INITIALIZE_SEED_PHRASE_ROUTE) + }} + href="#" + > + {`< Back`} + </a> + </div> + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="first-time-flow__header"> + { t('confirmSecretBackupPhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('selectEachPhrase') } + </div> + <div className="confirm-seed-phrase__selected-seed-words"> + { + selectedSeedWords.map((word, index) => ( + <div + key={index} + className="confirm-seed-phrase__seed-word" + > + { word } + </div> + )) + } + </div> + <div className="confirm-seed-phrase__shuffled-seed-words"> + { + shuffledSeedWords.map((word, index) => { + const isSelected = index in selectedSeedWordsHash + + return ( + <div + key={index} + className={classnames( + 'confirm-seed-phrase__seed-word', + 'confirm-seed-phrase__seed-word--shuffled', + { 'confirm-seed-phrase__seed-word--selected': isSelected } + )} + onClick={() => { + if (!isSelected) { + this.handleSelectSeedWord(word, index) + } else { + this.handleDeselectSeedWord(index) + } + }} + > + { word } + </div> + ) + }) + } + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={this.handleSubmit} + disabled={!this.isValid()} + > + { t('confirm') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={2} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js new file mode 100644 index 000000000..5fa2bec1e --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import ConfirmSeedPhrase from './confirm-seed-phrase.component' +import { setCompletedOnboarding, showModal } from '../../../../../actions' + +const mapDispatchToProps = dispatch => { + return { + completeOnboarding: () => dispatch(setCompletedOnboarding()), + openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})), + } +} + +export default connect(null, mapDispatchToProps)(ConfirmSeedPhrase) diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js new file mode 100644 index 000000000..f2476fc5c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js @@ -0,0 +1,41 @@ +export function selectSeedWord (word, shuffledIndex) { + return function update (state) { + const { selectedSeedWords, selectedSeedWordsHash } = state + const nextSelectedIndex = selectedSeedWords.length + + return { + selectedSeedWords: [ ...selectedSeedWords, word ], + selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex }, + } + } +} + +export function deselectSeedWord (shuffledIndex) { + return function update (state) { + const { + selectedSeedWords: prevSelectedSeedWords, + selectedSeedWordsHash: prevSelectedSeedWordsHash, + } = state + + const selectedSeedWords = [...prevSelectedSeedWords] + const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex] + selectedSeedWords.splice(indexToRemove, 1) + const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => { + const output = { ...acc } + const selectedSeedWordIndex = prevSelectedSeedWordsHash[index] + + if (selectedSeedWordIndex < indexToRemove) { + output[index] = selectedSeedWordIndex + } else if (selectedSeedWordIndex > indexToRemove) { + output[index] = selectedSeedWordIndex - 1 + } + + return output + }, {}) + + return { + selectedSeedWords, + selectedSeedWordsHash, + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js new file mode 100644 index 000000000..beb53b383 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './confirm-seed-phrase.container' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss new file mode 100644 index 000000000..e0444571f --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss @@ -0,0 +1,44 @@ +.confirm-seed-phrase { + &__back-button { + margin-bottom: 12px; + } + + &__selected-seed-words { + min-height: 190px; + max-width: 496px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + margin: 24px 0 36px; + padding: 12px; + } + + &__shuffled-seed-words { + max-width: 496px; + } + + &__seed-word { + display: inline-block; + color: #5B5D67; + background-color: #E7E7E7; + padding: 8px 18px; + min-width: 64px; + margin: 4px; + text-align: center; + + &--selected { + background-color: #85D1CC; + color: $white; + } + + &--shuffled { + cursor: pointer; + margin: 6px; + } + + @media screen and (max-width: 575px) { + font-size: .875rem; + padding: 6px 18px; + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/index.js new file mode 100644 index 000000000..7355bfb2c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './seed-phrase.container' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/index.scss new file mode 100644 index 000000000..88b28950c --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/index.scss @@ -0,0 +1,36 @@ +@import './confirm-seed-phrase/index'; + +@import './reveal-seed-phrase/index'; + +.seed-phrase { + + &__sections { + display: flex; + + @media screen and (min-width: $break-large) { + flex-direction: row; + } + + @media screen and (max-width: $break-small) { + flex-direction: column; + } + } + + &__main { + flex: 3; + min-width: 0; + } + + &__side { + flex: 2; + min-width: 0; + + @media screen and (min-width: $break-large) { + margin-left: 48px; + } + + @media screen and (max-width: $break-small) { + margin-top: 24px; + } + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js new file mode 100644 index 000000000..4a1b191b5 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js @@ -0,0 +1 @@ +export { default } from './reveal-seed-phrase.component' diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss new file mode 100644 index 000000000..568359d31 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss @@ -0,0 +1,53 @@ +.reveal-seed-phrase { + &__secret { + position: relative; + display: flex; + justify-content: center; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + padding: 18px; + margin-top: 36px; + max-width: 350px; + } + + &__secret-words { + width: 310px; + font-size: 1.25rem; + text-align: center; + + &--hidden { + filter: blur(5px); + } + } + + &__secret-blocker { + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 100%; + background-color: rgba(0,0,0,0.6); + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + padding: 8px 0 18px; + cursor: pointer; + } + + &__reveal-button { + color: $white; + font-size: .75rem; + font-weight: 500; + text-transform: uppercase; + margin-top: 8px; + text-align: center; + } + + &__export-text { + color: $curious-blue; + cursor: pointer; + font-weight: 500; + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js new file mode 100644 index 000000000..bb822d1d5 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js @@ -0,0 +1,139 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../../identicon' +import LockIcon from '../../../../lock-icon' +import Button from '../../../../button' +import Breadcrumbs from '../../../../breadcrumbs' +import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../../routes' +import { exportAsFile } from '../../../../../../app/util' + +export default class RevealSeedPhrase extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + address: PropTypes.string, + history: PropTypes.object, + seedPhrase: PropTypes.string, + } + + state = { + isShowingSeedPhrase: false, + } + + handleExport = () => { + exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') + } + + handleNext = event => { + event.preventDefault() + const { isShowingSeedPhrase } = this.state + const { history } = this.props + + if (!isShowingSeedPhrase) { + return + } + + history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE) + } + + renderSecretWordsContainer () { + const { t } = this.context + const { seedPhrase } = this.props + const { isShowingSeedPhrase } = this.state + + return ( + <div className="reveal-seed-phrase__secret"> + <div className={classnames( + 'reveal-seed-phrase__secret-words', + { 'reveal-seed-phrase__secret-words--hidden': !isShowingSeedPhrase } + )}> + { seedPhrase } + </div> + { + !isShowingSeedPhrase && ( + <div + className="reveal-seed-phrase__secret-blocker" + onClick={() => this.setState({ isShowingSeedPhrase: true })} + > + <LockIcon + width="28px" + height="35px" + fill="#FFFFFF" + /> + <div className="reveal-seed-phrase__reveal-button"> + { t('clickToRevealSeed') } + </div> + </div> + ) + } + </div> + ) + } + + render () { + const { t } = this.context + const { address } = this.props + const { isShowingSeedPhrase } = this.state + + return ( + <div> + <Identicon + className="first-time-flow__unique-image" + address={address} + diameter={70} + /> + <div className="seed-phrase__sections"> + <div className="seed-phrase__main"> + <div className="first-time-flow__header"> + { t('secretBackupPhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('secretBackupPhraseDescription') } + </div> + <div className="first-time-flow__text-block"> + { t('secretBackupPhraseWarning') } + </div> + { this.renderSecretWordsContainer() } + </div> + <div className="seed-phrase__side"> + <div className="first-time-flow__text-block"> + { `${t('tips')}:` } + </div> + <div className="first-time-flow__text-block"> + { t('storePhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('writePhrase') } + </div> + <div className="first-time-flow__text-block"> + { t('memorizePhrase') } + </div> + <div className="first-time-flow__text-block"> + <a + className="reveal-seed-phrase__export-text" + onClick={this.handleExport}> + { t('downloadSecretBackup') } + </a> + </div> + </div> + </div> + <Button + type="first-time" + className="first-time-flow__button" + onClick={this.handleNext} + disabled={!isShowingSeedPhrase} + > + { t('next') } + </Button> + <Breadcrumbs + className="first-time-flow__breadcrumbs" + total={3} + currentIndex={2} + /> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js new file mode 100644 index 000000000..5f5b8a0b2 --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -0,0 +1,59 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Switch, Route } from 'react-router-dom' +import RevealSeedPhrase from './reveal-seed-phrase' +import ConfirmSeedPhrase from './confirm-seed-phrase' +import { + INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, + DEFAULT_ROUTE, +} from '../../../../routes' + +export default class SeedPhrase extends PureComponent { + static propTypes = { + address: PropTypes.string, + history: PropTypes.object, + seedPhrase: PropTypes.string, + } + + componentDidMount () { + const { seedPhrase, history } = this.props + + if (!seedPhrase) { + history.push(DEFAULT_ROUTE) + } + } + + render () { + const { address, seedPhrase } = this.props + + return ( + <div className="first-time-flow__wrapper"> + <Switch> + <Route + exact + path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} + render={props => ( + <ConfirmSeedPhrase + { ...props } + address={address} + seedPhrase={seedPhrase} + /> + )} + /> + <Route + exact + path={INITIALIZE_SEED_PHRASE_ROUTE} + render={props => ( + <RevealSeedPhrase + { ...props } + address={address} + seedPhrase={seedPhrase} + /> + )} + /> + </Switch> + </div> + ) + } +} diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js new file mode 100644 index 000000000..4df024ffc --- /dev/null +++ b/ui/app/components/pages/first-time-flow/seed-phrase/seed-phrase.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import SeedPhrase from './seed-phrase.component' + +const mapStateToProps = state => { + const { metamask: { selectedAddress } } = state + + return { + address: selectedAddress, + } +} + +export default connect(mapStateToProps)(SeedPhrase) |