more permissive verification process (#4317)
* style fixes ✨ * verification: find last request * verification: don't request again if same input * didRequestWithSameValues -> shallRequestAgain * bugfixes 🐛, update SMS verification ABI * verification: hasRequested -> accountHasRequested * verification: hasIsVerified -> accountIsVerified * verification: shallRequestAgain -> shallSkipRequest * verification: show if unable to send req * email verification: check if email already used * address style grumbles 🎨
This commit is contained in:
parent
8e82b2f631
commit
1547af191b
@ -20,23 +20,23 @@ export const checkIfVerified = (contract, account) => {
|
||||
return contract.instance.certified.call({}, [account]);
|
||||
};
|
||||
|
||||
export const checkIfRequested = (contract, account) => {
|
||||
export const findLastRequested = (contract, account) => {
|
||||
let subId = null;
|
||||
let resolved = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
contract
|
||||
.subscribe('Requested', {
|
||||
fromBlock: 0, toBlock: 'pending'
|
||||
fromBlock: 0,
|
||||
toBlock: 'pending',
|
||||
limit: 1,
|
||||
topics: [account]
|
||||
}, (err, logs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const e = logs.find((l) => {
|
||||
return l.type === 'mined' && l.params.who && l.params.who.value === account;
|
||||
});
|
||||
|
||||
resolve(e ? e.transactionHash : false);
|
||||
resolve(logs[0] || null);
|
||||
resolved = true;
|
||||
|
||||
if (subId) {
|
||||
|
@ -30,6 +30,10 @@
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.terms {
|
||||
line-height: 1.3;
|
||||
opacity: .7;
|
||||
|
@ -31,19 +31,22 @@ import emailTermsOfService from '~/3rdparty/email-verification/terms-of-service'
|
||||
import { howSMSVerificationWorks, howEmailVerificationWorks } from '../how-it-works';
|
||||
import styles from './gatherData.css';
|
||||
|
||||
const boolOfError = PropTypes.oneOfType([ PropTypes.bool, PropTypes.instanceOf(Error) ]);
|
||||
|
||||
export default class GatherData extends Component {
|
||||
static propTypes = {
|
||||
fee: React.PropTypes.instanceOf(BigNumber),
|
||||
fields: PropTypes.array.isRequired,
|
||||
hasRequested: nullableProptype(PropTypes.bool.isRequired),
|
||||
accountHasRequested: nullableProptype(PropTypes.bool.isRequired),
|
||||
isServerRunning: nullableProptype(PropTypes.bool.isRequired),
|
||||
isVerified: nullableProptype(PropTypes.bool.isRequired),
|
||||
isAbleToRequest: nullableProptype(boolOfError.isRequired),
|
||||
accountIsVerified: nullableProptype(PropTypes.bool.isRequired),
|
||||
method: PropTypes.string.isRequired,
|
||||
setConsentGiven: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { method, isVerified } = this.props;
|
||||
const { method, accountIsVerified } = this.props;
|
||||
const termsOfService = method === 'email' ? emailTermsOfService : smsTermsOfService;
|
||||
const howItWorks = method === 'email' ? howEmailVerificationWorks : howSMSVerificationWorks;
|
||||
|
||||
@ -55,6 +58,7 @@ export default class GatherData extends Component {
|
||||
{ this.renderCertified() }
|
||||
{ this.renderRequested() }
|
||||
{ this.renderFields() }
|
||||
{ this.renderIfAbleToRequest() }
|
||||
<Checkbox
|
||||
className={ styles.spacing }
|
||||
label={
|
||||
@ -63,7 +67,7 @@ export default class GatherData extends Component {
|
||||
defaultMessage='I agree to the terms and conditions below.'
|
||||
/>
|
||||
}
|
||||
disabled={ isVerified }
|
||||
disabled={ accountIsVerified }
|
||||
onCheck={ this.consentOnChange }
|
||||
/>
|
||||
<div className={ styles.terms }>{ termsOfService }</div>
|
||||
@ -145,27 +149,27 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderCertified () {
|
||||
const { isVerified } = this.props;
|
||||
const { accountIsVerified } = this.props;
|
||||
|
||||
if (isVerified) {
|
||||
if (accountIsVerified) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<ErrorIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.true'
|
||||
id='ui.verification.gatherData.accountIsVerified.true'
|
||||
defaultMessage='Your account is already verified.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (isVerified === false) {
|
||||
} else if (accountIsVerified === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.false'
|
||||
id='ui.verification.gatherData.accountIsVerified.false'
|
||||
defaultMessage='Your account is not verified yet.'
|
||||
/>
|
||||
</p>
|
||||
@ -175,7 +179,7 @@ export default class GatherData extends Component {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.pending'
|
||||
id='ui.verification.gatherData.accountIsVerified.pending'
|
||||
defaultMessage='Checking if your account is verified…'
|
||||
/>
|
||||
</p>
|
||||
@ -183,33 +187,33 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderRequested () {
|
||||
const { isVerified, hasRequested } = this.props;
|
||||
const { accountIsVerified, accountHasRequested } = this.props;
|
||||
|
||||
// If the account is verified, don't show that it has requested verification.
|
||||
if (isVerified) {
|
||||
if (accountIsVerified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasRequested) {
|
||||
if (accountHasRequested) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<InfoIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.true'
|
||||
defaultMessage='You already requested verification.'
|
||||
id='ui.verification.gatherData.accountHasRequested.true'
|
||||
defaultMessage='You already requested verification from this account.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (hasRequested === false) {
|
||||
} else if (accountHasRequested === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.false'
|
||||
defaultMessage='You did not request verification yet.'
|
||||
id='ui.verification.gatherData.accountHasRequested.false'
|
||||
defaultMessage='You did not request verification from this account yet.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
@ -218,7 +222,7 @@ export default class GatherData extends Component {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.pending'
|
||||
id='ui.verification.gatherData.accountHasRequested.pending'
|
||||
defaultMessage='Checking if you requested verification…'
|
||||
/>
|
||||
</p>
|
||||
@ -226,7 +230,7 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderFields () {
|
||||
const { isVerified, fields } = this.props;
|
||||
const { accountIsVerified, fields } = this.props;
|
||||
|
||||
const rendered = fields.map((field) => {
|
||||
const onChange = (_, v) => {
|
||||
@ -236,11 +240,12 @@ export default class GatherData extends Component {
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ styles.field }
|
||||
key={ field.key }
|
||||
label={ field.label }
|
||||
hint={ field.hint }
|
||||
error={ field.error }
|
||||
disabled={ isVerified }
|
||||
disabled={ accountIsVerified }
|
||||
onChange={ onChange }
|
||||
onSubmit={ onSubmit }
|
||||
/>
|
||||
@ -250,6 +255,36 @@ export default class GatherData extends Component {
|
||||
return (<div>{rendered}</div>);
|
||||
}
|
||||
|
||||
renderIfAbleToRequest () {
|
||||
const { accountIsVerified, isAbleToRequest } = this.props;
|
||||
|
||||
// If the account is verified, don't show a warning.
|
||||
// If the client is able to send the request, don't show a warning
|
||||
if (accountIsVerified || isAbleToRequest === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAbleToRequest === null) {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isAbleToRequest.pending'
|
||||
defaultMessage='Validating your input…'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (isAbleToRequest) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<ErrorIcon />
|
||||
<p className={ styles.message }>
|
||||
{ isAbleToRequest.message }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
consentOnChange = (_, consentGiven) => {
|
||||
this.props.setConsentGiven(consentGiven);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { observable, computed, action } from 'mobx';
|
||||
import { sha3 } from '~/api/util/sha3';
|
||||
import { bytesToHex } from '~/api/util/format';
|
||||
|
||||
import EmailVerificationABI from '~/contracts/abi/email-verification.json';
|
||||
import VerificationStore, {
|
||||
@ -23,6 +24,8 @@ import VerificationStore, {
|
||||
} from './store';
|
||||
import { isServerRunning, hasReceivedCode, postToServer } from '~/3rdparty/email-verification';
|
||||
|
||||
const ZERO20 = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
// name in the `BadgeReg.sol` contract
|
||||
const EMAIL_VERIFICATION = 'emailverification';
|
||||
|
||||
@ -44,9 +47,9 @@ export default class EmailVerificationStore extends VerificationStore {
|
||||
|
||||
switch (this.step) {
|
||||
case LOADING:
|
||||
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
|
||||
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
|
||||
case QUERY_DATA:
|
||||
return this.isEmailValid && this.consentGiven;
|
||||
return this.isEmailValid && this.consentGiven && this.isAbleToRequest === true;
|
||||
case QUERY_CODE:
|
||||
return this.requestTx && this.isCodeValid === true;
|
||||
case POSTED_CONFIRMATION:
|
||||
@ -68,8 +71,53 @@ export default class EmailVerificationStore extends VerificationStore {
|
||||
return hasReceivedCode(this.email, this.account, this.isTestnet);
|
||||
}
|
||||
|
||||
// If the email has already been used for verification of another account,
|
||||
// we prevent the user from wasting ETH to request another verification.
|
||||
@action setIfAbleToRequest = () => {
|
||||
const { isEmailValid } = this;
|
||||
|
||||
if (!isEmailValid) {
|
||||
this.isAbleToRequest = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const { contract, email } = this;
|
||||
const emailHash = sha3.text(email);
|
||||
|
||||
this.isAbleToRequest = null;
|
||||
contract
|
||||
.instance.reverse
|
||||
.call({}, [ emailHash ])
|
||||
.then((address) => {
|
||||
if (address === ZERO20) {
|
||||
this.isAbleToRequest = true;
|
||||
} else {
|
||||
this.isAbleToRequest = new Error('Another account has been verified using this e-mail.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if able to send request: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the values relevant for checking if the last request contains
|
||||
// the same data as the current one.
|
||||
requestValues = () => [ sha3.text(this.email) ]
|
||||
|
||||
shallSkipRequest = (currentValues) => {
|
||||
const { accountHasRequested } = this;
|
||||
const lastRequest = this.lastRequestValues;
|
||||
|
||||
if (!accountHasRequested) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// If the last email verification `request` for the selected address contains
|
||||
// the same email as the current one, don't send another request to save ETH.
|
||||
const skip = currentValues[0] === bytesToHex(lastRequest.emailHash.value);
|
||||
|
||||
return Promise.resolve(skip);
|
||||
}
|
||||
|
||||
@action setEmail = (email) => {
|
||||
this.email = email;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ export default class SMSVerificationStore extends VerificationStore {
|
||||
|
||||
switch (this.step) {
|
||||
case LOADING:
|
||||
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
|
||||
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
|
||||
case QUERY_DATA:
|
||||
return this.isNumberValid && this.consentGiven;
|
||||
case QUERY_CODE:
|
||||
@ -67,6 +67,18 @@ export default class SMSVerificationStore extends VerificationStore {
|
||||
return hasReceivedCode(this.number, this.account, this.isTestnet);
|
||||
}
|
||||
|
||||
// SMS verification events don't contain the phone number, so we will have to
|
||||
// send a new request every single time. See below.
|
||||
@action setIfAbleToRequest = () => {
|
||||
this.isAbleToRequest = true;
|
||||
}
|
||||
|
||||
// SMS verification `request` & `confirm` transactions and events don't contain the
|
||||
// phone number, so we will have to send a new request every single time. This may
|
||||
// cost the user more money, but given that it fails otherwise, it seems like a
|
||||
// reasonable tradeoff.
|
||||
shallSkipRequest = () => Promise.resolve(false)
|
||||
|
||||
@action setNumber = (number) => {
|
||||
this.number = number;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { sha3 } from '~/api/util/sha3';
|
||||
import Contract from '~/api/contract';
|
||||
import Contracts from '~/contracts';
|
||||
|
||||
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/verification';
|
||||
import { checkIfVerified, findLastRequested, awaitPuzzle } from '~/contracts/verification';
|
||||
import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
|
||||
|
||||
export const LOADING = 'fetching-contract';
|
||||
@ -38,8 +38,10 @@ export default class VerificationStore {
|
||||
|
||||
@observable contract = null;
|
||||
@observable fee = null;
|
||||
@observable isVerified = null;
|
||||
@observable hasRequested = null;
|
||||
@observable accountIsVerified = null;
|
||||
@observable accountHasRequested = null;
|
||||
@observable isAbleToRequest = null;
|
||||
@observable lastRequestValues = null;
|
||||
@observable isServerRunning = null;
|
||||
@observable consentGiven = false;
|
||||
@observable requestTx = null;
|
||||
@ -68,6 +70,14 @@ export default class VerificationStore {
|
||||
console.error('verification: ' + this.error);
|
||||
}
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
if (this.step !== QUERY_DATA) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setIfAbleToRequest();
|
||||
});
|
||||
}
|
||||
|
||||
@action load = () => {
|
||||
@ -91,19 +101,20 @@ export default class VerificationStore {
|
||||
this.error = 'Failed to fetch the fee: ' + err.message;
|
||||
});
|
||||
|
||||
const isVerified = checkIfVerified(contract, account)
|
||||
.then((isVerified) => {
|
||||
this.isVerified = isVerified;
|
||||
const accountIsVerified = checkIfVerified(contract, account)
|
||||
.then((accountIsVerified) => {
|
||||
this.accountIsVerified = accountIsVerified;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if verified: ' + err.message;
|
||||
});
|
||||
|
||||
const hasRequested = checkIfRequested(contract, account)
|
||||
.then((txHash) => {
|
||||
this.hasRequested = !!txHash;
|
||||
if (txHash) {
|
||||
this.requestTx = txHash;
|
||||
const accountHasRequested = findLastRequested(contract, account)
|
||||
.then((log) => {
|
||||
this.accountHasRequested = !!log;
|
||||
if (log) {
|
||||
this.lastRequestValues = log.params;
|
||||
this.requestTx = log.transactionHash;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -111,7 +122,7 @@ export default class VerificationStore {
|
||||
});
|
||||
|
||||
Promise
|
||||
.all([ isServerRunning, fee, isVerified, hasRequested ])
|
||||
.all([ isServerRunning, fee, accountIsVerified, accountHasRequested ])
|
||||
.then(() => {
|
||||
this.step = QUERY_DATA;
|
||||
});
|
||||
@ -150,40 +161,41 @@ export default class VerificationStore {
|
||||
requestValues = () => []
|
||||
|
||||
@action sendRequest = () => {
|
||||
const { api, account, contract, fee, hasRequested } = this;
|
||||
const { api, account, contract, fee } = this;
|
||||
|
||||
const request = contract.functions.find((fn) => fn.name === 'request');
|
||||
const options = { from: account, value: fee.toString() };
|
||||
const values = this.requestValues();
|
||||
|
||||
let chain = Promise.resolve();
|
||||
this.shallSkipRequest(values)
|
||||
.then((skipRequest) => {
|
||||
if (skipRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasRequested) {
|
||||
this.step = POSTING_REQUEST;
|
||||
chain = request.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return request.postTransaction(options, values);
|
||||
})
|
||||
.then((handle) => {
|
||||
// TODO: The "request rejected" error doesn't have any property to
|
||||
// distinguish it from other errors, so we can't give a meaningful error here.
|
||||
return api.pollMethod('parity_checkRequest', handle);
|
||||
})
|
||||
.then((txHash) => {
|
||||
this.requestTx = txHash;
|
||||
return checkIfTxFailed(api, txHash, options.gas)
|
||||
.then((hasFailed) => {
|
||||
if (hasFailed) {
|
||||
throw new Error('Transaction failed, all gas used up.');
|
||||
}
|
||||
this.step = POSTED_REQUEST;
|
||||
return waitForConfirmations(api, txHash, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chain
|
||||
this.step = POSTING_REQUEST;
|
||||
return request.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return request.postTransaction(options, values);
|
||||
})
|
||||
.then((handle) => {
|
||||
// The "request rejected" error doesn't have any property to distinguish
|
||||
// it from other errors, so we can't give a meaningful error here.
|
||||
return api.pollMethod('parity_checkRequest', handle);
|
||||
})
|
||||
.then((txHash) => {
|
||||
this.requestTx = txHash;
|
||||
return checkIfTxFailed(api, txHash, options.gas)
|
||||
.then((hasFailed) => {
|
||||
if (hasFailed) {
|
||||
throw new Error('Transaction failed, all gas used up.');
|
||||
}
|
||||
this.step = POSTED_REQUEST;
|
||||
return waitForConfirmations(api, txHash, 1);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => this.checkIfReceivedCode())
|
||||
.then((hasReceived) => {
|
||||
if (hasReceived) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
@ -206,7 +207,7 @@ class Verification extends Component {
|
||||
|
||||
const {
|
||||
step,
|
||||
isServerRunning, fee, isVerified, hasRequested,
|
||||
isServerRunning, isAbleToRequest, fee, accountIsVerified, accountHasRequested,
|
||||
requestTx, isCodeValid, confirmationTx,
|
||||
setCode
|
||||
} = this.store;
|
||||
@ -223,17 +224,37 @@ class Verification extends Component {
|
||||
if (method === 'sms') {
|
||||
fields.push({
|
||||
key: 'number',
|
||||
label: 'phone number in international format',
|
||||
hint: 'the SMS will be sent to this number',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.phoneNumber.label'
|
||||
defaultMessage='phone number in international format'
|
||||
/>
|
||||
),
|
||||
hint: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.phoneNumber.hint'
|
||||
defaultMessage='the SMS will be sent to this number'
|
||||
/>
|
||||
),
|
||||
error: this.store.isNumberValid ? null : 'invalid number',
|
||||
onChange: this.store.setNumber
|
||||
});
|
||||
} else if (method === 'email') {
|
||||
fields.push({
|
||||
key: 'email',
|
||||
label: 'email address',
|
||||
hint: 'the code will be sent to this address',
|
||||
error: this.store.isEmailValid ? null : 'invalid email',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.email.label'
|
||||
defaultMessage='e-mail address'
|
||||
/>
|
||||
),
|
||||
hint: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.email.hint'
|
||||
defaultMessage='the code will be sent to this address'
|
||||
/>
|
||||
),
|
||||
error: this.store.isEmailValid ? null : 'invalid e-mail',
|
||||
onChange: this.store.setEmail
|
||||
});
|
||||
}
|
||||
@ -241,10 +262,12 @@ class Verification extends Component {
|
||||
return (
|
||||
<GatherData
|
||||
fee={ fee }
|
||||
hasRequested={ hasRequested }
|
||||
accountHasRequested={ accountHasRequested }
|
||||
isServerRunning={ isServerRunning }
|
||||
isVerified={ isVerified }
|
||||
method={ method } fields={ fields }
|
||||
isAbleToRequest={ isAbleToRequest }
|
||||
accountIsVerified={ accountIsVerified }
|
||||
method={ method }
|
||||
fields={ fields }
|
||||
setConsentGiven={ setConsentGiven }
|
||||
/>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user