Skip to content

Commit

Permalink
feat(auth): add the ability to import existing seed (#1118)
Browse files Browse the repository at this point in the history
* feat(auth): add the ability to import existing seed

* chore(auth): fix register shape
  • Loading branch information
jeroenptrs authored and Maurice Dalderup committed Aug 25, 2019
1 parent 3f4acdb commit 6b698c8
Show file tree
Hide file tree
Showing 14 changed files with 367 additions and 69 deletions.
22 changes: 13 additions & 9 deletions src/renderer/register/actions/registerFormActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const ID = 'registerForm';
* Seed hex is then used to encrypt/decrypt things
*/
export const validateAndStoreFormData = async (data) => {
const { accountLabel, passphrase, passphraseConfirm, secretWord } = data;
const { accountLabel, passphrase, passphraseConfirm, secretWord, isImport } = data;

const accounts = await getStorage(ACCOUNT_ID);
const newAccount = accounts[accountLabel];
Expand All @@ -39,14 +39,18 @@ export const validateAndStoreFormData = async (data) => {
throw new Error('Fill in a Secret Word.');
}

// Generate bip39 Mnemonic - 256-bits entropy (24-word long mnemonic)
const mnemonic = bip39.generateMnemonic(256, null, bip39.wordlists[DEFAULT_LANGUAGE]);
const encryptedMnemonic = simpleEncrypt(mnemonic, passphrase);
return {
...data,
mnemonic,
encryptedMnemonic
};
const registerData = { ...data };
if (!isImport) {
const mnemonic = bip39.generateMnemonic(256, null, bip39.wordlists[DEFAULT_LANGUAGE]);
registerData.mnemonic = mnemonic;
}

if (registerData.mnemonic) {
const encryptedMnemonic = simpleEncrypt(registerData.mnemonic, passphrase);
registerData.encryptedMnemonic = encryptedMnemonic;
}

return registerData;
};

export default createActions(ID, (data) => {
Expand Down
29 changes: 11 additions & 18 deletions src/renderer/register/components/AccountView/AccountView.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,20 @@ import { func, bool } from 'prop-types';

import registerShape from 'register/shapes/registerShape';

import MnemonicView from './MnemonicView';
import ImportView from './ImportView';
import LedgerView from './LedgerView';
import MnemonicView from './MnemonicView';

const AccountView = ({ onCancel, account, previousStep, nextStep, loading }) => {
return account.isHardware ? (
<LedgerView
account={account}
nextStep={nextStep}
previousStep={previousStep}
onCancel={onCancel}
loading={loading}
/>
) : (
<MnemonicView
account={account}
nextStep={nextStep}
previousStep={previousStep}
onCancel={onCancel}
loading={loading}
/>
);
const { isHardware, isImport } = account;

const nextProps = { account, nextStep, previousStep, onCancel, loading };

if (isHardware) return <LedgerView {...nextProps} />;

if (isImport) return <ImportView {...nextProps} />;

return <MnemonicView {...nextProps} />;
};

AccountView.propTypes = {
Expand Down
111 changes: 111 additions & 0 deletions src/renderer/register/components/AccountView/ImportView/ImportView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as bip39 from 'bip39';
import { debounce } from 'lodash';
import React from 'react';
import { bool, func, arrayOf, string } from 'prop-types';

import AuthPanel from 'auth/components/AuthPanel';
import NavigationButtons from 'shared/components/NavigationButtons';
import { DEFAULT_LANGUAGE } from 'shared/values/languages';
import registerShape from 'register/shapes/registerShape';

import MnemonicWordInput from './MnemonicWordInput';
import styles from './ImportView.scss';

export default class ImportView extends React.PureComponent {
static propTypes = {
account: registerShape.isRequired,
loading: bool,
onCancel: func.isRequired,
previousStep: func.isRequired,
storeFormData: func.isRequired,
showErrorToast: func.isRequired,
mnemonic: arrayOf(string).isRequired,
setMnemonic: func.isRequired
};

static defaultProps = {
loading: false
};

state = {
validMnemonic: false
};

mnemonicRefs = Array(24)
.fill(undefined)
.map(() => React.createRef());

showErrorToast = debounce((e) => {
this.props.showErrorToast(e);
}, 1000);

render() {
const { onCancel, loading, mnemonic, previousStep } = this.props;

return (
<AuthPanel
sidePanel
step="2"
onCancel={onCancel}
className={styles.importView}
sidePanelText="Copy and paste your 24-word seed in the right order into the following form."
>
<form onSubmit={this.handleStoreMnemonic}>
<div className={styles.mnemonic}>
{mnemonic.map((word, count) => (
<MnemonicWordInput
key={`mnemonic-input-word-${count + 1}`}
count={count + 1}
word={word}
inputRef={this.mnemonicRefs[count]}
onChange={this.setMnemonic(count)}
/>
))}
</div>
<NavigationButtons
onBack={previousStep}
nextBtnText="Next: verify"
disabled={loading || !this.state.validMnemonic}
isSubmit
/>
</form>
</AuthPanel>
);
}

handleStoreMnemonic = (e) => {
e.preventDefault();

const { mnemonic: mnemonicArray, account, storeFormData } = this.props;
const mnemonic = mnemonicArray.join(' ');

storeFormData({
...account,
mnemonic
});
};

setMnemonic = (count) => (event) => {
event.preventDefault();
const { mnemonic, setMnemonic } = this.props;

let newMnemonic = event.target.value.trim().split(' ');
if (newMnemonic.length !== 24) {
newMnemonic = [...mnemonic];
newMnemonic[count] = event.target.value;
}

const validMnemonic = bip39.validateMnemonic(
newMnemonic.join(' '),
bip39.wordlists[DEFAULT_LANGUAGE]
);

this.setState({ validMnemonic }, () => {
setMnemonic(newMnemonic);

const hasEmptyWords = newMnemonic.filter((x) => x === '');
if (!this.state.validMnemonic && hasEmptyWords.length === 0)
this.showErrorToast('Seed is invalid');
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.importView {
.mnemonic {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;

width: 520px;
padding: 0 40px;
}

.button {
margin: 10px 40px 0 40px;
@extend %tertiaryButtonColor;

svg {
vertical-align: middle;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.wordInput {
margin: 0 !important;

// TODO: The way Input's styles are ordered doesn't allow for simple overrides. Style overhaul.
box-shadow: none !important;
background: transparent !important;

input {
padding: 3px 12px;
}
}

.focus {
box-shadow: inset 0 0 0 2px $brand !important;

.input {
outline: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import classNames from 'classnames';
import { func, string, number } from 'prop-types';
import React from 'react';

import Input from 'shared/components/Forms/Input';
import refShape from 'shared/shapes/refShape';

import mnemonicWordStyles from '../../MnemonicView/MnemonicWord/MnemonicWord.scss';
import styles from './MnemonicWordInput.scss';

const handleInputFocus = (setFocus, value) => (event) => {
event.preventDefault();
setFocus(value);
};

const MnemonicWordInput = ({ count, word, inputRef, onChange }) => {
const [focus, setFocus] = React.useState(false);
const useFocus = handleInputFocus(setFocus, true);
const useBlur = handleInputFocus(setFocus, false);

return (
<div
className={classNames(mnemonicWordStyles.mnemonicWord, {
[styles.focus]: focus
})}
>
<div className={mnemonicWordStyles.count}>{count}</div>
<Input
id={`mnemonic-word-${count}`}
className={classNames(mnemonicWordStyles.word, styles.wordInput)}
type="text"
value={word}
ref={inputRef}
onFocus={useFocus}
onBlur={useBlur}
onChange={onChange}
/>
</div>
);
};

MnemonicWordInput.propTypes = {
count: number.isRequired,
word: string.isRequired,
inputRef: refShape.isRequired,
onChange: func.isRequired
};

MnemonicWordInput.defaultProps = {};

export default MnemonicWordInput;
33 changes: 33 additions & 0 deletions src/renderer/register/components/AccountView/ImportView/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { compose, withState } from 'recompose';
import { withActions, progressValues } from 'spunky';

import withLoadingProp from 'shared/hocs/withLoadingProp';
import { withErrorToast } from 'shared/hocs/withToast';
import withProgressChange from 'shared/hocs/withProgressChange';
import pureStrategy from 'shared/hocs/strategies/pureStrategy';
import registerFormActions from 'register/actions/registerFormActions';

import ImportView from './ImportView';

const { FAILED, LOADED } = progressValues;

const emptyMnemonicWordArray = Array(24).fill('');

const mapRegisterActionsToProps = (actions) => ({
storeFormData: (data) => actions.call(data)
});

export default compose(
withActions(registerFormActions, mapRegisterActionsToProps),
withLoadingProp(registerFormActions, { strategy: pureStrategy }),

withState('mnemonic', 'setMnemonic', ({ mnemonic }) => mnemonic || emptyMnemonicWordArray),

withErrorToast(),
withProgressChange(registerFormActions, FAILED, (state, props) => {
props.showErrorToast(state.error);
}),
withProgressChange(registerFormActions, LOADED, (state, props) => {
props.nextStep();
})
)(ImportView);
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class LedgerView extends React.PureComponent {
const { onCancel, previousStep } = this.props;

const sidePanelText =
'Connect your ledger and launch the NEO app. This will enable you to select an address for wallet.';
'Connect your ledger and launch the NEO app. This will enable you to select an address for your wallet.';

return (
<AuthPanel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class MnemonicView extends React.PureComponent {
<CopyIcon /> Copy to Clipboard
</Button>
</CopyToClipboard>
<NavigationButtons onBack={previousStep} onNext={nextStep} nextBtnText="Next: Verify" />
<NavigationButtons onBack={previousStep} onNext={nextStep} nextBtnText="Next: verify" />
</AuthPanel>
);
}
Expand Down
Loading

0 comments on commit 6b698c8

Please sign in to comment.