Mnemonic Redesign (#1501)
* Added a shuffle to swap out the numbers and made them read-only. * Add a border and an index when selecting a confirmation word * Add error flashing * Transform the inputs into buttons when confirming * Change naming of "peek" to "revealing" * If statement needs brackets * Linter complaints * Code cleanup * Formatting and removing unused styles. * Move out shuffle to a util * Additional test cases * Function call, forgot the parens * Adjust sizing and position of confirmation index in Mnemonic button * Implement requested style changes: selected buttons, margins on inputs * Use lodahs shuffle * No need to spread this array
This commit is contained in:
parent
a40b2cc499
commit
7320413dab
|
@ -58,4 +58,7 @@ v8-compile-cache-0/
|
|||
package-lock.json
|
||||
|
||||
# Favicon cache
|
||||
.wwp-cache
|
||||
.wwp-cache
|
||||
|
||||
# MacOSX
|
||||
.DS_Store
|
|
@ -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 = <FinalSteps walletType={WalletType.Mnemonic} />;
|
||||
} 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 ? (
|
||||
<FinalSteps walletType={WalletType.Mnemonic} />
|
||||
) : (
|
||||
<div className="GenerateMnemonic">
|
||||
<h1 className="GenerateMnemonic-title">{translate('GENERATE_MNEMONIC_TITLE')}</h1>
|
||||
|
||||
content = (
|
||||
<div className="GenerateMnemonic">
|
||||
<h1 className="GenerateMnemonic-title">{translate('GENERATE_MNEMONIC_TITLE')}</h1>
|
||||
<p className="GenerateMnemonic-help">
|
||||
{isConfirming ? translate('MNEMONIC_DESCRIPTION_1') : translate('MNEMONIC_DESCRIPTION_2')}
|
||||
</p>
|
||||
|
||||
<p className="GenerateMnemonic-help">
|
||||
{isConfirming
|
||||
? translate('MNEMONIC_DESCRIPTION_1')
|
||||
: translate('MNEMONIC_DESCRIPTION_2')}
|
||||
</p>
|
||||
|
||||
<div className="GenerateMnemonic-words">
|
||||
{[firstHalf, lastHalf].map((ws, i) => (
|
||||
<div key={i} className="GenerateMnemonic-words-column">
|
||||
{ws.map(this.makeWord)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="GenerateMnemonic-buttons">
|
||||
{!isConfirming && (
|
||||
<button
|
||||
className="GenerateMnemonic-buttons-btn btn btn-default"
|
||||
onClick={this.regenerateWordArray}
|
||||
>
|
||||
<i className="fa fa-refresh" /> {translate('REGENERATE_MNEMONIC')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="GenerateMnemonic-buttons-btn btn btn-primary"
|
||||
disabled={!canContinue}
|
||||
onClick={this.goToNextStep}
|
||||
>
|
||||
{translate('CONFIRM_MNEMONIC')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="GenerateMnemonic-skip" onClick={this.skip} />
|
||||
<div className="GenerateMnemonic-words">
|
||||
{[firstHalf, lastHalf].map((ws, i) => (
|
||||
<div key={i} className="GenerateMnemonic-words-column">
|
||||
{ws.map(this.makeWord)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="GenerateMnemonic-buttons">
|
||||
{!isConfirming && (
|
||||
<button className={defaultBtnClassName} onClick={this.regenerateWordArray}>
|
||||
<i className="fa fa-refresh" /> {translate('REGENERATE_MNEMONIC')}
|
||||
</button>
|
||||
)}
|
||||
{isConfirming && (
|
||||
<button
|
||||
className={defaultBtnClassName}
|
||||
disabled={canContinue}
|
||||
onClick={this.revealNextWord}
|
||||
>
|
||||
<i className="fa fa-eye" /> {translate('REVEAL_NEXT_MNEMONIC')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="GenerateMnemonic-buttons-btn btn btn-primary"
|
||||
disabled={!canContinue}
|
||||
onClick={this.goToNextStep}
|
||||
>
|
||||
{translate('CONFIRM_MNEMONIC')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="GenerateMnemonic-skip" onClick={this.skip} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return <Template>{content}</Template>;
|
||||
}
|
||||
|
@ -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) => (
|
||||
<Word
|
||||
key={`${word.word}${word.index}`}
|
||||
index={word.index}
|
||||
word={word.word}
|
||||
value={this.state.confirmValues[word.index] || ''}
|
||||
isReadOnly={!this.state.isConfirming}
|
||||
onChange={this.handleConfirmChange}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<Word
|
||||
key={`${word.word}${word.index}`}
|
||||
index={word.index}
|
||||
confirmIndex={confirmIndex}
|
||||
word={word.word}
|
||||
value={confirmValues[word.index] || ''}
|
||||
showIndex={!isConfirming}
|
||||
isNext={isNext}
|
||||
isBeingRevealed={isRevealed}
|
||||
isConfirming={isConfirming}
|
||||
hasBeenConfirmed={hasBeenConfirmed}
|
||||
onClick={this.handleWordClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<div className="input-group-wrapper MnemonicWord">
|
||||
<label className="input-group input-group-inline ENSInput-name">
|
||||
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
|
||||
<Input
|
||||
className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
|
||||
value={readOnly ? word : value}
|
||||
onChange={this.handleChange}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{!isReadOnly && (
|
||||
<span
|
||||
onClick={this.toggleShow}
|
||||
aria-label={translateRaw('GEN_ARIA_2')}
|
||||
role="button"
|
||||
className="MnemonicWord-word-toggle input-group-addon"
|
||||
{showIndex && <span className={indexClassName}>{index + 1}.</span>}
|
||||
{hasBeenConfirmed && (
|
||||
<span className="MnemonicWord-button-index">{confirmIndex + 1}</span>
|
||||
)}
|
||||
{isConfirming ? (
|
||||
<button
|
||||
className={`MnemonicWord-button ${btnClassName} ${
|
||||
hasBeenConfirmed ? 'disabled' : ''
|
||||
}`}
|
||||
onClick={() => this.handleClick(word)}
|
||||
>
|
||||
<i
|
||||
className={classnames(
|
||||
'fa',
|
||||
isShowingWord && 'fa-eye-slash',
|
||||
!isShowingWord && 'fa-eye'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
{word}
|
||||
</button>
|
||||
) : (
|
||||
<Input className="MnemonicWord-word-input" value={word} readOnly={true} />
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
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
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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...",
|
||||
|
|
Loading…
Reference in New Issue