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 && (
-
- )}
-
-
-
-
+
+ {[firstHalf, lastHalf].map((ws, i) => (
+
+ {ws.map(this.makeWord)}
+
+ ))}
- );
- }
+
+
+ {!isConfirming && (
+
+ )}
+ {isConfirming && (
+
+ )}
+
+
+
+
+
+ );
return
{content};
}
@@ -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...",