mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-18 05:56:54 +00:00
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
5
.gitignore
vendored
5
.gitignore
vendored
@ -58,4 +58,7 @@ v8-compile-cache-0/
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# Favicon cache
|
# Favicon cache
|
||||||
.wwp-cache
|
.wwp-cache
|
||||||
|
|
||||||
|
# MacOSX
|
||||||
|
.DS_Store
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { generateMnemonic } from 'bip39';
|
import { generateMnemonic } from 'bip39';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
|
import shuffle from 'lodash/shuffle';
|
||||||
import Word from './Word';
|
import Word from './Word';
|
||||||
import FinalSteps from '../FinalSteps';
|
import FinalSteps from '../FinalSteps';
|
||||||
import Template from '../Template';
|
import Template from '../Template';
|
||||||
@ -10,8 +11,10 @@ import './Mnemonic.scss';
|
|||||||
interface State {
|
interface State {
|
||||||
words: string[];
|
words: string[];
|
||||||
confirmValues: string[];
|
confirmValues: string[];
|
||||||
|
confirmWords: WordTuple[][];
|
||||||
isConfirming: boolean;
|
isConfirming: boolean;
|
||||||
isConfirmed: boolean;
|
isConfirmed: boolean;
|
||||||
|
isRevealingNextWord: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WordTuple {
|
interface WordTuple {
|
||||||
@ -23,8 +26,10 @@ export default class GenerateMnemonic extends React.Component<{}, State> {
|
|||||||
public state: State = {
|
public state: State = {
|
||||||
words: [],
|
words: [],
|
||||||
confirmValues: [],
|
confirmValues: [],
|
||||||
|
confirmWords: [],
|
||||||
isConfirming: false,
|
isConfirming: false,
|
||||||
isConfirmed: false
|
isConfirmed: false,
|
||||||
|
isRevealingNextWord: false
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
@ -32,63 +37,57 @@ export default class GenerateMnemonic extends React.Component<{}, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { words, isConfirming, isConfirmed } = this.state;
|
const { words, confirmWords, isConfirming, isConfirmed } = this.state;
|
||||||
let content;
|
const defaultBtnClassName = 'GenerateMnemonic-buttons-btn btn btn-default';
|
||||||
|
const canContinue = this.checkCanContinue();
|
||||||
|
const [firstHalf, lastHalf] =
|
||||||
|
confirmWords.length === 0 ? this.splitWordsIntoHalves(words) : confirmWords;
|
||||||
|
|
||||||
if (isConfirmed) {
|
const content = isConfirmed ? (
|
||||||
content = <FinalSteps walletType={WalletType.Mnemonic} />;
|
<FinalSteps walletType={WalletType.Mnemonic} />
|
||||||
} else {
|
) : (
|
||||||
const canContinue = this.checkCanContinue();
|
<div className="GenerateMnemonic">
|
||||||
const firstHalf: WordTuple[] = [];
|
<h1 className="GenerateMnemonic-title">{translate('GENERATE_MNEMONIC_TITLE')}</h1>
|
||||||
const lastHalf: WordTuple[] = [];
|
|
||||||
words.forEach((word, index) => {
|
|
||||||
if (index < words.length / 2) {
|
|
||||||
firstHalf.push({ word, index });
|
|
||||||
} else {
|
|
||||||
lastHalf.push({ word, index });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
content = (
|
<p className="GenerateMnemonic-help">
|
||||||
<div className="GenerateMnemonic">
|
{isConfirming ? translate('MNEMONIC_DESCRIPTION_1') : translate('MNEMONIC_DESCRIPTION_2')}
|
||||||
<h1 className="GenerateMnemonic-title">{translate('GENERATE_MNEMONIC_TITLE')}</h1>
|
</p>
|
||||||
|
|
||||||
<p className="GenerateMnemonic-help">
|
<div className="GenerateMnemonic-words">
|
||||||
{isConfirming
|
{[firstHalf, lastHalf].map((ws, i) => (
|
||||||
? translate('MNEMONIC_DESCRIPTION_1')
|
<div key={i} className="GenerateMnemonic-words-column">
|
||||||
: translate('MNEMONIC_DESCRIPTION_2')}
|
{ws.map(this.makeWord)}
|
||||||
</p>
|
</div>
|
||||||
|
))}
|
||||||
<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>
|
</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>;
|
return <Template>{content}</Template>;
|
||||||
}
|
}
|
||||||
@ -97,14 +96,6 @@ export default class GenerateMnemonic extends React.Component<{}, State> {
|
|||||||
this.setState({ words: generateMnemonic().split(' ') });
|
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 = () => {
|
private goToNextStep = () => {
|
||||||
if (!this.checkCanContinue()) {
|
if (!this.checkCanContinue()) {
|
||||||
return;
|
return;
|
||||||
@ -113,7 +104,13 @@ export default class GenerateMnemonic extends React.Component<{}, State> {
|
|||||||
if (this.state.isConfirming) {
|
if (this.state.isConfirming) {
|
||||||
this.setState({ isConfirmed: true });
|
this.setState({ isConfirmed: true });
|
||||||
} else {
|
} 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) => {
|
||||||
<Word
|
const { words, confirmValues, isRevealingNextWord, isConfirming } = this.state;
|
||||||
key={`${word.word}${word.index}`}
|
const confirmIndex = words.indexOf(word.word);
|
||||||
index={word.index}
|
const nextIndex = confirmValues.length;
|
||||||
word={word.word}
|
const isNext = confirmIndex === nextIndex;
|
||||||
value={this.state.confirmValues[word.index] || ''}
|
const isRevealed = isRevealingNextWord && isNext;
|
||||||
isReadOnly={!this.state.isConfirming}
|
const hasBeenConfirmed = this.getWordConfirmed(word.word);
|
||||||
onChange={this.handleConfirmChange}
|
|
||||||
/>
|
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 = () => {
|
private skip = () => {
|
||||||
this.setState({ isConfirmed: true });
|
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 {
|
.MnemonicWord {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: $width;
|
width: $width;
|
||||||
margin-bottom: $space-md;
|
margin-bottom: $space-xs;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-number {
|
& .input-group-addon {
|
||||||
display: inline-block;
|
margin-bottom: $space-md;
|
||||||
width: $number-width;
|
|
||||||
margin-right: $number-margin;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 100;
|
|
||||||
line-height: 40px;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-word {
|
&-word {
|
||||||
@ -39,14 +32,31 @@ $number-margin: 6px;
|
|||||||
&-input {
|
&-input {
|
||||||
animation: word-fade 400ms ease 1;
|
animation: word-fade 400ms ease 1;
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
|
transition: border 0.2s ease-in;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-toggle {
|
&-button {
|
||||||
color: $gray-light;
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: $space-sm;
|
||||||
|
|
||||||
&:hover {
|
&-index {
|
||||||
color: $gray;
|
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 React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { translateRaw } from 'translations';
|
|
||||||
import './Word.scss';
|
import './Word.scss';
|
||||||
import { Input } from 'components/ui';
|
import { Input } from 'components/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number;
|
index: number;
|
||||||
|
confirmIndex: number;
|
||||||
word: string;
|
word: string;
|
||||||
value: string;
|
value: string;
|
||||||
isReadOnly: boolean;
|
showIndex: boolean;
|
||||||
onChange(index: number, value: string): void;
|
isNext: boolean;
|
||||||
|
isBeingRevealed: boolean;
|
||||||
|
isConfirming: boolean;
|
||||||
|
hasBeenConfirmed: boolean;
|
||||||
|
onClick(index: number, value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
isShowingWord: boolean;
|
flashingError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MnemonicWord extends React.Component<Props, State> {
|
export default class MnemonicWord extends React.Component<Props, State> {
|
||||||
public state = {
|
public state = {
|
||||||
isShowingWord: false
|
flashingError: false
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { index, word, value, isReadOnly } = this.props;
|
const {
|
||||||
const { isShowingWord } = this.state;
|
hasBeenConfirmed,
|
||||||
const readOnly = isReadOnly || isShowingWord;
|
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 (
|
return (
|
||||||
<div className="input-group-wrapper MnemonicWord">
|
<div className="input-group-wrapper MnemonicWord">
|
||||||
<label className="input-group input-group-inline ENSInput-name">
|
<label className="input-group input-group-inline ENSInput-name">
|
||||||
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
|
{showIndex && <span className={indexClassName}>{index + 1}.</span>}
|
||||||
<Input
|
{hasBeenConfirmed && (
|
||||||
className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
|
<span className="MnemonicWord-button-index">{confirmIndex + 1}</span>
|
||||||
value={readOnly ? word : value}
|
)}
|
||||||
onChange={this.handleChange}
|
{isConfirming ? (
|
||||||
readOnly={readOnly}
|
<button
|
||||||
/>
|
className={`MnemonicWord-button ${btnClassName} ${
|
||||||
{!isReadOnly && (
|
hasBeenConfirmed ? 'disabled' : ''
|
||||||
<span
|
}`}
|
||||||
onClick={this.toggleShow}
|
onClick={() => this.handleClick(word)}
|
||||||
aria-label={translateRaw('GEN_ARIA_2')}
|
|
||||||
role="button"
|
|
||||||
className="MnemonicWord-word-toggle input-group-addon"
|
|
||||||
>
|
>
|
||||||
<i
|
{word}
|
||||||
className={classnames(
|
</button>
|
||||||
'fa',
|
) : (
|
||||||
isShowingWord && 'fa-eye-slash',
|
<Input className="MnemonicWord-word-input" value={word} readOnly={true} />
|
||||||
!isShowingWord && 'fa-eye'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
private handleClick = (value: string) => {
|
||||||
this.props.onChange(this.props.index, ev.currentTarget.value);
|
const { isNext, index, onClick } = this.props;
|
||||||
|
|
||||||
|
if (!isNext) {
|
||||||
|
this.flashError();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(index, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
private toggleShow = () => {
|
private flashError = () => {
|
||||||
this.setState({ isShowingWord: !this.state.isShowingWord });
|
const errorDuration = 200;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
flashingError: true
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
this.setState({
|
||||||
|
flashingError: false
|
||||||
|
}),
|
||||||
|
errorDuration
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -379,8 +379,9 @@
|
|||||||
"GENERATE_THING": "Generate a $thing",
|
"GENERATE_THING": "Generate a $thing",
|
||||||
"REGENERATE_MNEMONIC": "Regenerate Phrase",
|
"REGENERATE_MNEMONIC": "Regenerate Phrase",
|
||||||
"CONFIRM_MNEMONIC": "Confirm Phrase",
|
"CONFIRM_MNEMONIC": "Confirm Phrase",
|
||||||
|
"REVEAL_NEXT_MNEMONIC": "Reveal Next Word",
|
||||||
"MNEMONIC_CHOOSE_ADDR": "Choose address",
|
"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.",
|
"MNEMONIC_DESCRIPTION_2": "Write these words down. Do not copy them to your clipboard, or save them anywhere online.",
|
||||||
"MODAL_BACK": "Back",
|
"MODAL_BACK": "Back",
|
||||||
"WALLET_UNLOCKING": "Unlocking...",
|
"WALLET_UNLOCKING": "Unlocking...",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user