diff --git a/.gitignore b/.gitignore index 9a407abc..0aaa39fe 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,7 @@ v8-compile-cache-0/ package-lock.json # Favicon cache -.wwp-cache \ No newline at end of file +.wwp-cache + +# MacOSX +.DS_Store \ No newline at end of file diff --git a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Mnemonic.tsx b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Mnemonic.tsx index b6d83c6f..019c592c 100644 --- a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Mnemonic.tsx +++ b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Mnemonic.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { generateMnemonic } from 'bip39'; import translate from 'translations'; +import shuffle from 'lodash/shuffle'; import Word from './Word'; import FinalSteps from '../FinalSteps'; import Template from '../Template'; @@ -10,8 +11,10 @@ import './Mnemonic.scss'; interface State { words: string[]; confirmValues: string[]; + confirmWords: WordTuple[][]; isConfirming: boolean; isConfirmed: boolean; + isRevealingNextWord: boolean; } interface WordTuple { @@ -23,8 +26,10 @@ export default class GenerateMnemonic extends React.Component<{}, State> { public state: State = { words: [], confirmValues: [], + confirmWords: [], isConfirming: false, - isConfirmed: false + isConfirmed: false, + isRevealingNextWord: false }; public componentDidMount() { @@ -32,63 +37,57 @@ export default class GenerateMnemonic extends React.Component<{}, State> { } public render() { - const { words, isConfirming, isConfirmed } = this.state; - let content; + const { words, confirmWords, isConfirming, isConfirmed } = this.state; + const defaultBtnClassName = 'GenerateMnemonic-buttons-btn btn btn-default'; + const canContinue = this.checkCanContinue(); + const [firstHalf, lastHalf] = + confirmWords.length === 0 ? this.splitWordsIntoHalves(words) : confirmWords; - if (isConfirmed) { - content = ; - } else { - const canContinue = this.checkCanContinue(); - const firstHalf: WordTuple[] = []; - const lastHalf: WordTuple[] = []; - words.forEach((word, index) => { - if (index < words.length / 2) { - firstHalf.push({ word, index }); - } else { - lastHalf.push({ word, index }); - } - }); + const content = isConfirmed ? ( + + ) : ( +
+

{translate('GENERATE_MNEMONIC_TITLE')}

- content = ( -
-

{translate('GENERATE_MNEMONIC_TITLE')}

+

+ {isConfirming ? translate('MNEMONIC_DESCRIPTION_1') : translate('MNEMONIC_DESCRIPTION_2')} +

-

- {isConfirming - ? translate('MNEMONIC_DESCRIPTION_1') - : translate('MNEMONIC_DESCRIPTION_2')} -

- -
- {[firstHalf, lastHalf].map((ws, i) => ( -
- {ws.map(this.makeWord)} -
- ))} -
- -
- {!isConfirming && ( - - )} - -
- - + )} + {isConfirming && ( + + )} + +
+ +
+ ); return ; } @@ -97,14 +96,6 @@ export default class GenerateMnemonic extends React.Component<{}, State> { this.setState({ words: generateMnemonic().split(' ') }); }; - private handleConfirmChange = (index: number, value: string) => { - this.setState((state: State) => { - const confirmValues = [...state.confirmValues]; - confirmValues[index] = value; - this.setState({ confirmValues }); - }); - }; - private goToNextStep = () => { if (!this.checkCanContinue()) { return; @@ -113,7 +104,13 @@ export default class GenerateMnemonic extends React.Component<{}, State> { if (this.state.isConfirming) { this.setState({ isConfirmed: true }); } else { - this.setState({ isConfirming: true }); + const shuffledWords = shuffle(this.state.words); + const confirmWords = this.splitWordsIntoHalves(shuffledWords); + + this.setState({ + isConfirming: true, + confirmWords + }); } }; @@ -129,18 +126,79 @@ export default class GenerateMnemonic extends React.Component<{}, State> { } }; - private makeWord = (word: WordTuple) => ( - - ); + private makeWord = (word: WordTuple) => { + const { words, confirmValues, isRevealingNextWord, isConfirming } = this.state; + const confirmIndex = words.indexOf(word.word); + const nextIndex = confirmValues.length; + const isNext = confirmIndex === nextIndex; + const isRevealed = isRevealingNextWord && isNext; + const hasBeenConfirmed = this.getWordConfirmed(word.word); + + return ( + + ); + }; + + private handleWordClick = (_: number, value: string) => { + const { confirmValues: previousConfirmValues, words, isConfirming } = this.state; + const wordAlreadyConfirmed = previousConfirmValues.includes(value); + const activeIndex = previousConfirmValues.length; + const isCorrectChoice = words[activeIndex] === value; + + if (isConfirming && !wordAlreadyConfirmed && isCorrectChoice) { + const confirmValues = previousConfirmValues.concat(value); + + this.setState({ confirmValues }); + } + }; + + private getWordConfirmed = (word: string) => this.state.confirmValues.includes(word); private skip = () => { this.setState({ isConfirmed: true }); }; + + private revealNextWord = () => { + const revealDuration = 400; + + this.setState( + { + isRevealingNextWord: true + }, + () => + setTimeout( + () => + this.setState({ + isRevealingNextWord: false + }), + revealDuration + ) + ); + }; + + private splitWordsIntoHalves = (words: string[]) => { + const firstHalf: WordTuple[] = []; + const lastHalf: WordTuple[] = []; + + words.forEach((word: string, index: number) => { + const inFirstColumn = index < words.length / 2; + const half = inFirstColumn ? firstHalf : lastHalf; + + half.push({ word, index }); + }); + + return [firstHalf, lastHalf]; + }; } diff --git a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.scss b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.scss index 63c38c43..b717e00e 100644 --- a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.scss +++ b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.scss @@ -16,21 +16,14 @@ $number-margin: 6px; .MnemonicWord { display: flex; width: $width; - margin-bottom: $space-md; + margin-bottom: $space-xs; &:last-child { margin-bottom: 0; } - &-number { - display: inline-block; - width: $number-width; - margin-right: $number-margin; - text-align: right; - font-size: 26px; - font-weight: 100; - line-height: 40px; - vertical-align: bottom; + & .input-group-addon { + margin-bottom: $space-md; } &-word { @@ -39,14 +32,31 @@ $number-margin: 6px; &-input { animation: word-fade 400ms ease 1; animation-fill-mode: both; + transition: border 0.2s ease-in; + margin-bottom: 0; } + } - &-toggle { - color: $gray-light; + &-button { + position: relative; + width: 300px; + margin-bottom: $space-sm; - &:hover { - color: $gray; - } + &-index { + position: absolute; + top: -4px; + left: -7px; + z-index: 1; + color: #fff; + width: 26px; + height: 26px; + border-radius: 100%; + background: linear-gradient( + to top, + lighten($brand-success, 10%), + lighten($brand-success, 5%) + ); + line-height: 24px; } } diff --git a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.tsx b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.tsx index e9e351db..9633d6bb 100644 --- a/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.tsx +++ b/common/containers/Tabs/GenerateWallet/components/Mnemonic/Word.tsx @@ -1,67 +1,98 @@ import React from 'react'; import classnames from 'classnames'; -import { translateRaw } from 'translations'; import './Word.scss'; import { Input } from 'components/ui'; interface Props { index: number; + confirmIndex: number; word: string; value: string; - isReadOnly: boolean; - onChange(index: number, value: string): void; + showIndex: boolean; + isNext: boolean; + isBeingRevealed: boolean; + isConfirming: boolean; + hasBeenConfirmed: boolean; + onClick(index: number, value: string): void; } interface State { - isShowingWord: boolean; + flashingError: boolean; } export default class MnemonicWord extends React.Component { public state = { - isShowingWord: false + flashingError: false }; public render() { - const { index, word, value, isReadOnly } = this.props; - const { isShowingWord } = this.state; - const readOnly = isReadOnly || isShowingWord; + const { + hasBeenConfirmed, + isBeingRevealed, + showIndex, + index, + isConfirming, + confirmIndex, + word + } = this.props; + const { flashingError } = this.state; + const btnClassName = classnames({ + btn: true, + 'btn-default': !(isBeingRevealed || flashingError), + 'btn-success': isBeingRevealed, + 'btn-danger': flashingError + }); + const indexClassName = 'input-group-addon input-group-addon--transparent'; return (
); } - private handleChange = (ev: React.FormEvent) => { - this.props.onChange(this.props.index, ev.currentTarget.value); + private handleClick = (value: string) => { + const { isNext, index, onClick } = this.props; + + if (!isNext) { + this.flashError(); + } + + onClick(index, value); }; - private toggleShow = () => { - this.setState({ isShowingWord: !this.state.isShowingWord }); + private flashError = () => { + const errorDuration = 200; + + this.setState( + { + flashingError: true + }, + () => + setTimeout( + () => + this.setState({ + flashingError: false + }), + errorDuration + ) + ); }; } diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index 7302a6b5..22b87e12 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -379,8 +379,9 @@ "GENERATE_THING": "Generate a $thing", "REGENERATE_MNEMONIC": "Regenerate Phrase", "CONFIRM_MNEMONIC": "Confirm Phrase", + "REVEAL_NEXT_MNEMONIC": "Reveal Next Word", "MNEMONIC_CHOOSE_ADDR": "Choose address", - "MNEMONIC_DESCRIPTION_1": "Re-enter your phrase to confirm you copied it correctly. If you forgot one of your words, just click the button beside the input to reveal it.", + "MNEMONIC_DESCRIPTION_1": "Click the words of your phrase in order. If you've forgotten the next word, click the 'Reveal Next Word' button below.", "MNEMONIC_DESCRIPTION_2": "Write these words down. Do not copy them to your clipboard, or save them anywhere online.", "MODAL_BACK": "Back", "WALLET_UNLOCKING": "Unlocking...",