diff --git a/src/renderer/register/actions/registerFormActions.js b/src/renderer/register/actions/registerFormActions.js
index c2a13b7e6..6ac4048b8 100644
--- a/src/renderer/register/actions/registerFormActions.js
+++ b/src/renderer/register/actions/registerFormActions.js
@@ -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];
@@ -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) => {
diff --git a/src/renderer/register/components/AccountView/AccountView.js b/src/renderer/register/components/AccountView/AccountView.js
index 2b71dc1b3..abdba766c 100644
--- a/src/renderer/register/components/AccountView/AccountView.js
+++ b/src/renderer/register/components/AccountView/AccountView.js
@@ -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 ? (
-
- ) : (
-
- );
+ const { isHardware, isImport } = account;
+
+ const nextProps = { account, nextStep, previousStep, onCancel, loading };
+
+ if (isHardware) return ;
+
+ if (isImport) return ;
+
+ return ;
};
AccountView.propTypes = {
diff --git a/src/renderer/register/components/AccountView/ImportView/ImportView.js b/src/renderer/register/components/AccountView/ImportView/ImportView.js
new file mode 100644
index 000000000..3d11aaeda
--- /dev/null
+++ b/src/renderer/register/components/AccountView/ImportView/ImportView.js
@@ -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 (
+
+
+
+ );
+ }
+
+ 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');
+ });
+ };
+}
diff --git a/src/renderer/register/components/AccountView/ImportView/ImportView.scss b/src/renderer/register/components/AccountView/ImportView/ImportView.scss
new file mode 100644
index 000000000..eed6be39c
--- /dev/null
+++ b/src/renderer/register/components/AccountView/ImportView/ImportView.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/MnemonicWordInput.scss b/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/MnemonicWordInput.scss
new file mode 100644
index 000000000..a2648a630
--- /dev/null
+++ b/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/MnemonicWordInput.scss
@@ -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;
+ }
+}
diff --git a/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/index.js b/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/index.js
new file mode 100644
index 000000000..8a43764e2
--- /dev/null
+++ b/src/renderer/register/components/AccountView/ImportView/MnemonicWordInput/index.js
@@ -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 (
+
+ );
+};
+
+MnemonicWordInput.propTypes = {
+ count: number.isRequired,
+ word: string.isRequired,
+ inputRef: refShape.isRequired,
+ onChange: func.isRequired
+};
+
+MnemonicWordInput.defaultProps = {};
+
+export default MnemonicWordInput;
diff --git a/src/renderer/register/components/AccountView/ImportView/index.js b/src/renderer/register/components/AccountView/ImportView/index.js
new file mode 100644
index 000000000..0ffa3ac3b
--- /dev/null
+++ b/src/renderer/register/components/AccountView/ImportView/index.js
@@ -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);
diff --git a/src/renderer/register/components/AccountView/LedgerView/LedgerView.js b/src/renderer/register/components/AccountView/LedgerView/LedgerView.js
index 34da5c17a..a52e1bc39 100644
--- a/src/renderer/register/components/AccountView/LedgerView/LedgerView.js
+++ b/src/renderer/register/components/AccountView/LedgerView/LedgerView.js
@@ -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 (
Copy to Clipboard
-
+
);
}
diff --git a/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.js b/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.js
index f8229f30b..8419debc6 100644
--- a/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.js
+++ b/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.js
@@ -18,12 +18,14 @@ export default class RegisterForm extends React.PureComponent {
passphraseConfirm: string,
secretWord: string,
isHardware: bool,
+ isImport: bool,
setAccountLabel: func,
setPassphrase: func,
setPassphraseConfirm: func,
setSecretWord: func,
storeFormData: func,
- setIsHardware: func
+ setIsHardware: func,
+ setIsImport: func
};
static defaultProps = {
@@ -38,7 +40,9 @@ export default class RegisterForm extends React.PureComponent {
setSecretWord: noop,
storeFormData: noop,
setIsHardware: noop,
- isHardware: false
+ setIsImport: noop,
+ isHardware: false,
+ isImport: false
};
render = () => {
@@ -48,7 +52,8 @@ export default class RegisterForm extends React.PureComponent {
passphraseConfirm,
secretWord,
loading,
- isHardware
+ isHardware,
+ isImport
} = this.props;
return (
@@ -96,34 +101,62 @@ export default class RegisterForm extends React.PureComponent {
/>
);
};
- renderCheckboxLabel = () => {
- return ;
+ renderButtonMessage = () => {
+ const { isHardware, isImport } = this.props;
+ if (isHardware) return 'Next: connect Ledger';
+ if (isImport) return 'Next: import seed';
+ return 'Next: recovery seed';
+ };
+
+ renderImportLabel = () => {
+ return ;
+ };
+
+ renderLedgerLabel = () => {
+ return ;
+ };
+
+ handleChangeIsImport = () => {
+ this.props.setIsImport(!this.props.isImport);
+ this.props.setIsHardware(false);
};
handleChangeIsHardware = () => {
this.props.setIsHardware(!this.props.isHardware);
+ this.props.setIsImport(false);
};
handleChangeSecretWord = (event) => {
@@ -149,7 +182,8 @@ export default class RegisterForm extends React.PureComponent {
secretWord,
passphraseConfirm,
storeFormData,
- isHardware
+ isHardware,
+ isImport
} = this.props;
event.preventDefault();
@@ -158,7 +192,8 @@ export default class RegisterForm extends React.PureComponent {
passphrase,
passphraseConfirm,
secretWord,
- isHardware
+ isHardware,
+ isImport
});
};
diff --git a/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.scss b/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.scss
index 8983eedcf..51bd266a5 100644
--- a/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.scss
+++ b/src/renderer/register/components/CreateAccount/RegisterForm/RegisterForm.scss
@@ -7,18 +7,25 @@
font-size: 12px;
}
- .checkbox {
- background: transparent;
- box-shadow: none;
- width: auto;
-
- @include theme(DARK) {
- background: transparent;
- box-shadow: none;
+ .checkboxContainer {
+ :first-child {
+ margin-bottom: 2px;
}
- input {
+ .checkbox {
+ background: transparent;
+ box-shadow: none;
width: auto;
+
+ @include theme(DARK) {
+ background: transparent;
+ box-shadow: none;
+ }
+
+ input {
+ width: auto;
+ max-width: 12px;
+ }
}
}
diff --git a/src/renderer/register/components/CreateAccount/RegisterForm/index.js b/src/renderer/register/components/CreateAccount/RegisterForm/index.js
index 46026c440..493f1365a 100644
--- a/src/renderer/register/components/CreateAccount/RegisterForm/index.js
+++ b/src/renderer/register/components/CreateAccount/RegisterForm/index.js
@@ -14,6 +14,7 @@ const { FAILED, LOADED } = progressValues;
const mapRegisterActionsToProps = (actions) => ({
storeFormData: (data) => actions.call(data)
});
+
const mapRegisterDataToProps = (data) => data;
export default compose(
@@ -26,6 +27,7 @@ export default compose(
withState('passphraseConfirm', 'setPassphraseConfirm', ''),
withState('secretWord', 'setSecretWord', ({ secretWord }) => secretWord || ''),
withState('isHardware', 'setIsHardware', ({ isHardware }) => isHardware || false),
+ withState('isImport', 'setIsImport', ({ isImport }) => isImport || false),
withErrorToast(),
withProgressChange(registerFormActions, FAILED, (state, props) => {
props.showErrorToast(state.error);
diff --git a/src/renderer/register/shapes/registerShape.js b/src/renderer/register/shapes/registerShape.js
index d0cb1b9cf..08dd60de4 100644
--- a/src/renderer/register/shapes/registerShape.js
+++ b/src/renderer/register/shapes/registerShape.js
@@ -1,9 +1,19 @@
import { string, shape, bool, oneOfType } from 'prop-types';
+export const registerImportShape = shape({
+ accountLabel: string.isRequired,
+ isHardware: bool.isRequired,
+ isImport: bool.isRequired,
+ passphrase: string.isRequired,
+ passphraseConfirm: string.isRequired,
+ secretWord: string.isRequired
+});
+
export const registerMnemonicShape = shape({
accountLabel: string.isRequired,
encryptedMnemonic: string.isRequired,
isHardware: bool.isRequired,
+ isImport: bool.isRequired,
mnemonic: string.isRequired,
passphrase: string.isRequired,
passphraseConfirm: string.isRequired,
@@ -14,6 +24,7 @@ export const registerLedgerShape = shape({
accountLabel: string.isRequired,
encryptedMnemonic: string.isRequired,
isHardware: bool.isRequired,
+ isImport: bool.isRequired,
mnemonic: string.isRequired,
passphrase: string.isRequired,
passphraseConfirm: string.isRequired,
@@ -21,4 +32,4 @@ export const registerLedgerShape = shape({
publicKey: string.isRequired
});
-export default oneOfType([registerMnemonicShape, registerLedgerShape]);
+export default oneOfType([registerMnemonicShape, registerLedgerShape, registerImportShape]);
diff --git a/src/renderer/shared/components/NavigationButtons/index.js b/src/renderer/shared/components/NavigationButtons/index.js
index d74e72ccd..735e8e500 100644
--- a/src/renderer/shared/components/NavigationButtons/index.js
+++ b/src/renderer/shared/components/NavigationButtons/index.js
@@ -1,3 +1,4 @@
+import { noop } from 'lodash';
import React from 'react';
import { string, func, bool } from 'prop-types';
@@ -6,26 +7,37 @@ import Button from 'shared/components/Forms/Button';
import styles from './NavigationButtons.scss';
-const NavigationButtons = ({ onBack, onNext, nextBtnText, disabled }) => (
-
-
-
- {nextBtnText}
-
-
-);
+const NavigationButtons = ({ onBack, onNext, nextBtnText, disabled, isSubmit }) => {
+ const PrimaryButtonProps = {
+ className: styles.nextBtn,
+ onClick: onNext,
+ disabled
+ };
+
+ if (isSubmit) PrimaryButtonProps.type = 'submit';
+
+ return (
+
+
+
{nextBtnText}
+
+ );
+};
NavigationButtons.propTypes = {
onBack: func.isRequired,
- onNext: func.isRequired,
nextBtnText: string.isRequired,
- disabled: bool
+ onNext: func,
+ disabled: bool,
+ isSubmit: bool
};
NavigationButtons.defaultProps = {
- disabled: false
+ disabled: false,
+ isSubmit: false,
+ onNext: noop
};
export default NavigationButtons;