Require phrase confirmation. (#5731)
* Require phrase confirmation. * Linting issues. * Fix dialog title. * Confirm a backup phrase. * Confirm recovery phrase on fist run as well.
This commit is contained in:
parent
06eb561af5
commit
5c3ea4ec29
@ -25,9 +25,16 @@ import styles from '../createAccount.css';
|
||||
@observer
|
||||
export default class AccountDetails extends Component {
|
||||
static propTypes = {
|
||||
isConfirming: PropTypes.bool,
|
||||
withRequiredBackup: PropTypes.bool,
|
||||
createStore: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static defaultPropTypes = {
|
||||
isConfirming: false,
|
||||
withRequiredBackup: false
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, description, name } = this.props.createStore;
|
||||
|
||||
@ -78,31 +85,103 @@ export default class AccountDetails extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPhrase () {
|
||||
const { phrase } = this.props.createStore;
|
||||
renderRequiredBackup () {
|
||||
const { phraseBackedUp, phraseBackedUpError } = this.props.createStore;
|
||||
|
||||
if (!phrase) {
|
||||
if (!this.props.withRequiredBackup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
allowCopy
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.hint'
|
||||
defaultMessage='the account recovery phrase'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.label'
|
||||
defaultMessage='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)'
|
||||
/>
|
||||
}
|
||||
readOnly
|
||||
value={ phrase }
|
||||
/>
|
||||
<div>
|
||||
<Input
|
||||
error={ phraseBackedUpError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.hint'
|
||||
defaultMessage='the account recovery phrase'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.backedUp'
|
||||
defaultMessage='Type "I have written down the phrase" below to confirm it is backed up.'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditPhraseBackedUp }
|
||||
value={ phraseBackedUp }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPhrase () {
|
||||
const { isConfirming } = this.props;
|
||||
const { isTest, phrase, backupPhraseError } = this.props.createStore;
|
||||
|
||||
const hint = (
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.hint'
|
||||
defaultMessage='the account recovery phrase'
|
||||
/>
|
||||
);
|
||||
const label = (
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.label'
|
||||
defaultMessage='owner recovery phrase'
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isConfirming) {
|
||||
if (!phrase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
allowCopy
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
readOnly
|
||||
value={ phrase }
|
||||
/>
|
||||
<div className={ styles.backupPhrase }>
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.backup'
|
||||
defaultMessage='Please back up the recovery phrase now. Make sure to keep it private and secure, it allows full and unlimited access to the account.'
|
||||
/>
|
||||
</div>
|
||||
{ this.renderRequiredBackup() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
allowPaste={ isTest }
|
||||
error={ backupPhraseError }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
onChange={ this.onEditPhrase }
|
||||
value={ phrase }
|
||||
/>
|
||||
<div className={ styles.backupPhrase }>
|
||||
<FormattedMessage
|
||||
id='createAccount.accountDetails.phrase.backupConfirm'
|
||||
defaultMessage='Type your recovery phrase now.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onEditPhraseBackedUp = (ev) => {
|
||||
this.props.createStore.setPhraseBackedUp(ev.target.value);
|
||||
}
|
||||
|
||||
onEditPhrase = (ev) => {
|
||||
this.props.createStore.setPhrase(ev.target.value);
|
||||
}
|
||||
}
|
||||
|
@ -131,3 +131,8 @@
|
||||
padding: 0 4em 1.5em 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.backupPhrase {
|
||||
line-height: 1.618em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ import NewImport from './NewImport';
|
||||
import NewQr from './NewQr';
|
||||
import RawKey from './RawKey';
|
||||
import RecoveryPhrase from './RecoveryPhrase';
|
||||
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store';
|
||||
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE, STAGE_CONFIRM_BACKUP } from './store';
|
||||
import TypeIcon from './TypeIcon';
|
||||
import print from './print';
|
||||
import recoveryPage from './recoveryPage.ejs';
|
||||
@ -61,6 +61,12 @@ const TITLES = {
|
||||
defaultMessage='account information'
|
||||
/>
|
||||
),
|
||||
backup: (
|
||||
<FormattedMessage
|
||||
id='createAccount.title.backupPhrase'
|
||||
defaultMessage='confirm recovery phrase'
|
||||
/>
|
||||
),
|
||||
import: (
|
||||
<FormattedMessage
|
||||
id='createAccount.title.importAccount'
|
||||
@ -80,7 +86,7 @@ const TITLES = {
|
||||
/>
|
||||
)
|
||||
};
|
||||
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
|
||||
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info, TITLES.backup];
|
||||
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
|
||||
const STAGE_RESTORE = [TITLES.restore, TITLES.info];
|
||||
const STAGE_QR = [TITLES.type, TITLES.qr, TITLES.info];
|
||||
@ -213,14 +219,25 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountDetails createStore={ this.createStore } />
|
||||
<AccountDetails
|
||||
createStore={ this.createStore }
|
||||
withRequiredBackup={ createType === 'fromNew' }
|
||||
/>
|
||||
);
|
||||
|
||||
case STAGE_CONFIRM_BACKUP:
|
||||
return (
|
||||
<AccountDetails
|
||||
createStore={ this.createStore }
|
||||
isConfirming
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { restore } = this.props;
|
||||
const { createType, canCreate, isBusy, stage } = this.createStore;
|
||||
const { createType, canCreate, isBusy, stage, phraseBackedUpError } = this.createStore;
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
@ -281,8 +298,8 @@ class CreateAccount extends Component {
|
||||
createType === 'fromNew'
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='createAccount.button.create'
|
||||
defaultMessage='Create'
|
||||
id='createAccount.button.next'
|
||||
defaultMessage='Next'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@ -292,7 +309,7 @@ class CreateAccount extends Component {
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={ this.onCreate }
|
||||
onClick={ createType === 'fromNew' ? this.createStore.nextStage : this.onCreate }
|
||||
/>
|
||||
];
|
||||
|
||||
@ -314,6 +331,7 @@ class CreateAccount extends Component {
|
||||
)
|
||||
: null,
|
||||
<Button
|
||||
disabled={ createType === 'fromNew' && !!phraseBackedUpError }
|
||||
icon={ <DoneIcon /> }
|
||||
key='done'
|
||||
label={
|
||||
@ -322,12 +340,55 @@ class CreateAccount extends Component {
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onClose }
|
||||
onClick={ createType === 'fromNew' ? this.onConfirmPhraseBackup : this.onClose }
|
||||
/>
|
||||
];
|
||||
|
||||
case STAGE_CONFIRM_BACKUP:
|
||||
return [
|
||||
<Button
|
||||
icon={ <DoneIcon /> }
|
||||
key='done'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.button.create'
|
||||
defaultMessage='Create'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onCreateNew }
|
||||
/>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmPhraseBackup = () => {
|
||||
this.createStore.clearPhrase();
|
||||
this.createStore.nextStage();
|
||||
}
|
||||
|
||||
onCreateNew = () => {
|
||||
this.createStore.setBusy(true);
|
||||
this.createStore.computeBackupPhraseAddress()
|
||||
.then(err => {
|
||||
if (err) {
|
||||
this.createStore.setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.createStore.createAccount(this.vaultStore)
|
||||
.then(() => {
|
||||
this.createStore.clearPhrase();
|
||||
this.createStore.setBusy(false);
|
||||
this.props.onUpdate && this.props.onUpdate();
|
||||
this.onClose();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.createStore.setBusy(false);
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
|
||||
onCreate = () => {
|
||||
return this.createStore
|
||||
.createAccount(this.vaultStore)
|
||||
@ -341,6 +402,7 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.createStore.clearPhrase();
|
||||
this.props.onClose && this.props.onClose();
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,20 @@ export default {
|
||||
/>
|
||||
),
|
||||
|
||||
noMatchBackupPhrase: (
|
||||
<FormattedMessage
|
||||
id='errors.noMatchBackupPhrase'
|
||||
defaultMessage='the supplied recovery phrase does not match'
|
||||
/>
|
||||
),
|
||||
|
||||
noMatchPhraseBackedUp: (
|
||||
<FormattedMessage
|
||||
id='errors.noMatchPhraseBackedUp'
|
||||
defaultMessage='type "I have written down the phrase"'
|
||||
/>
|
||||
),
|
||||
|
||||
noName: (
|
||||
<FormattedMessage
|
||||
id='errors.noName'
|
||||
@ -59,4 +73,5 @@ export default {
|
||||
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"'
|
||||
/>
|
||||
)
|
||||
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ const FAKEPATH = 'C:\\fakepath\\';
|
||||
const STAGE_SELECT_TYPE = 0;
|
||||
const STAGE_CREATE = 1;
|
||||
const STAGE_INFO = 2;
|
||||
const STAGE_CONFIRM_BACKUP = 3;
|
||||
|
||||
export default class Store {
|
||||
@observable accounts = null;
|
||||
@ -41,6 +42,8 @@ export default class Store {
|
||||
@observable passwordHint = '';
|
||||
@observable passwordRepeat = '';
|
||||
@observable phrase = '';
|
||||
@observable backupPhraseAddress = null;
|
||||
@observable phraseBackedUp = '';
|
||||
@observable qrAddress = null;
|
||||
@observable rawKey = '';
|
||||
@observable rawKeyError = ERRORS.nokey;
|
||||
@ -104,17 +107,39 @@ export default class Store {
|
||||
: ERRORS.noMatchPassword;
|
||||
}
|
||||
|
||||
@computed get backupPhraseError () {
|
||||
return !this.backupPhraseAddress || this.address === this.backupPhraseAddress
|
||||
? null
|
||||
: ERRORS.noMatchBackupPhrase;
|
||||
}
|
||||
|
||||
@computed get phraseBackedUpError () {
|
||||
return this.phraseBackedUp === 'I have written down the phrase'
|
||||
? null
|
||||
: ERRORS.noMatchPhraseBackedUp;
|
||||
}
|
||||
|
||||
@computed get qrAddressValid () {
|
||||
console.log('qrValid', this.qrAddress, this._api.util.isAddressValid(this.qrAddress));
|
||||
return this._api.util.isAddressValid(this.qrAddress);
|
||||
}
|
||||
|
||||
@action clearPhrase = () => {
|
||||
transaction(() => {
|
||||
this.phrase = '';
|
||||
this.phraseBackedUp = '';
|
||||
});
|
||||
}
|
||||
|
||||
@action clearErrors = () => {
|
||||
transaction(() => {
|
||||
this.address = '';
|
||||
this.description = '';
|
||||
this.password = '';
|
||||
this.passwordRepeat = '';
|
||||
this.phrase = '';
|
||||
this.backupPhraseAddress = null;
|
||||
this.phraseBackedUp = '';
|
||||
this.name = '';
|
||||
this.nameError = ERRORS.noName;
|
||||
this.qrAddress = null;
|
||||
@ -192,6 +217,26 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
@action setBackupPhraseAddress = (address) => {
|
||||
this.backupPhraseAddress = address;
|
||||
}
|
||||
|
||||
@action computeBackupPhraseAddress = () => {
|
||||
return this._api.parity.phraseToAddress(this.phrase)
|
||||
.then(address => {
|
||||
this.setBackupPhraseAddress(address);
|
||||
return address !== this.address;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('createAccount', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@action setPhraseBackedUp = (backedUp) => {
|
||||
this.phraseBackedUp = backedUp;
|
||||
}
|
||||
|
||||
@action setPassword = (password) => {
|
||||
this.password = password;
|
||||
}
|
||||
@ -217,6 +262,7 @@ export default class Store {
|
||||
.filter((part) => part.length);
|
||||
|
||||
this.phrase = phraseParts.join(' ');
|
||||
this.backupPhraseAddress = null;
|
||||
}
|
||||
|
||||
@action setRawKey = (rawKey) => {
|
||||
@ -460,7 +506,8 @@ export default class Store {
|
||||
}
|
||||
|
||||
export {
|
||||
STAGE_CREATE,
|
||||
STAGE_INFO,
|
||||
STAGE_CONFIRM_BACKUP,
|
||||
STAGE_CREATE,
|
||||
STAGE_SELECT_TYPE
|
||||
};
|
||||
|
@ -52,6 +52,10 @@ const STAGE_NAMES = [
|
||||
id='firstRun.title.recovery'
|
||||
defaultMessage='recovery'
|
||||
/>,
|
||||
<FormattedMessage
|
||||
id='firstRun.title.confirmation'
|
||||
defaultMessage='confirmation'
|
||||
/>,
|
||||
<FormattedMessage
|
||||
id='firstRun.title.completed'
|
||||
defaultMessage='completed'
|
||||
@ -129,9 +133,19 @@ class FirstRun extends Component {
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<AccountDetails createStore={ this.createStore } />
|
||||
<AccountDetails
|
||||
createStore={ this.createStore }
|
||||
withRequiredBackup
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<AccountDetails
|
||||
createStore={ this.createStore }
|
||||
isConfirming
|
||||
/>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<Completed />
|
||||
);
|
||||
@ -141,7 +155,7 @@ class FirstRun extends Component {
|
||||
renderDialogActions () {
|
||||
const { hasAccounts } = this.props;
|
||||
const { stage, hasAcceptedTnc } = this.state;
|
||||
const { canCreate } = this.createStore;
|
||||
const { canCreate, phraseBackedUpError } = this.createStore;
|
||||
|
||||
switch (stage) {
|
||||
case 0:
|
||||
@ -169,15 +183,10 @@ class FirstRun extends Component {
|
||||
const buttons = [
|
||||
<Button
|
||||
disabled={ !canCreate }
|
||||
icon={ <CheckIcon /> }
|
||||
key='create'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='firstRun.button.create'
|
||||
defaultMessage='Create'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onCreate }
|
||||
icon={ <NextIcon /> }
|
||||
key='next'
|
||||
label={ BUTTON_LABEL_NEXT }
|
||||
onClick={ this.onNext }
|
||||
/>
|
||||
];
|
||||
|
||||
@ -212,14 +221,30 @@ class FirstRun extends Component {
|
||||
onClick={ this.printPhrase }
|
||||
/>,
|
||||
<Button
|
||||
disabled={ !!phraseBackedUpError }
|
||||
icon={ <NextIcon /> }
|
||||
key='next'
|
||||
label={ BUTTON_LABEL_NEXT }
|
||||
onClick={ this.onNext }
|
||||
onClick={ this.onConfirmPhraseBackup }
|
||||
/>
|
||||
];
|
||||
|
||||
case 4:
|
||||
return (
|
||||
<Button
|
||||
icon={ <CheckIcon /> }
|
||||
key='create'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='firstRun.button.create'
|
||||
defaultMessage='Create'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onCreate }
|
||||
/>
|
||||
);
|
||||
|
||||
case 5:
|
||||
return (
|
||||
<Button
|
||||
icon={ <DoneIcon /> }
|
||||
@ -244,6 +269,11 @@ class FirstRun extends Component {
|
||||
}, onClose);
|
||||
}
|
||||
|
||||
onConfirmPhraseBackup = () => {
|
||||
this.createStore.clearPhrase();
|
||||
this.onNext();
|
||||
}
|
||||
|
||||
onNext = () => {
|
||||
const { stage } = this.state;
|
||||
|
||||
@ -261,11 +291,19 @@ class FirstRun extends Component {
|
||||
onCreate = () => {
|
||||
this.createStore.setBusy(true);
|
||||
|
||||
return this.createStore
|
||||
.createAccount()
|
||||
.then(() => {
|
||||
this.onNext();
|
||||
this.createStore.setBusy(false);
|
||||
this.createStore.computeBackupPhraseAddress()
|
||||
.then(err => {
|
||||
if (err) {
|
||||
this.createStore.setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.createStore.createAccount()
|
||||
.then(() => {
|
||||
this.createStore.clearPhrase();
|
||||
this.createStore.setBusy(false);
|
||||
this.onNext();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.createStore.setBusy(false);
|
||||
|
@ -57,6 +57,7 @@ export default class Input extends Component {
|
||||
PropTypes.string,
|
||||
PropTypes.bool
|
||||
]),
|
||||
allowPaste: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
@ -97,6 +98,7 @@ export default class Input extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
allowCopy: false,
|
||||
allowPaste: true,
|
||||
escape: 'initial',
|
||||
hideUnderline: false,
|
||||
onBlur: noop,
|
||||
@ -221,6 +223,12 @@ export default class Input extends Component {
|
||||
}
|
||||
|
||||
onChange = (event, value) => {
|
||||
if (!this.props.allowPaste) {
|
||||
if (value.length - this.state.value.length > 8) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event.persist();
|
||||
|
||||
this.setValue(value, () => {
|
||||
|
Loading…
Reference in New Issue
Block a user