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 ( + +
+
+ {mnemonic.map((word, count) => ( + + ))} +
+ + +
+ ); + } + + 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 ( +
+
{count}
+ +
+ ); +}; + +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 { />
- +
+ + +
- {isHardware ? 'Next: Connect Ledger' : 'Next: Recovery Seed'} + {this.renderButtonMessage()}
); }; - renderCheckboxLabel = () => { - return