Unverified Commit 739f1eff authored by Amaury Martiny's avatar Amaury Martiny Committed by GitHub
Browse files

Merge pull request #135 from paritytech/am-form-validation

Form validation
parents 254ecab9 dba232c0
...@@ -38,8 +38,10 @@ ...@@ -38,8 +38,10 @@
"@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94", "@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94",
"@parity/shared": "^3.0.2", "@parity/shared": "^3.0.2",
"bignumber.js": "^4.1.0", "bignumber.js": "^4.1.0",
"debounce-promise": "^3.1.0",
"debug": "^3.1.0", "debug": "^3.1.0",
"fether-ui": "^0.1.0", "fether-ui": "^0.1.0",
"final-form": "^4.8.3",
"is-electron": "^2.1.0", "is-electron": "^2.1.0",
"light-hoc": "^0.1.0", "light-hoc": "^0.1.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
...@@ -48,10 +50,12 @@ ...@@ -48,10 +50,12 @@
"react": "^16.3.2", "react": "^16.3.2",
"react-blockies": "^1.3.0", "react-blockies": "^1.3.0",
"react-dom": "^16.3.2", "react-dom": "^16.3.2",
"react-final-form": "^3.6.4",
"react-markdown": "^3.3.4", "react-markdown": "^3.3.4",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-scripts": "1.1.4", "react-scripts": "1.1.4",
"react-tooltip": "^3.6.1", "react-tooltip": "^3.6.1",
"recompose": "^0.27.1",
"rxjs": "^6.2.0" "rxjs": "^6.2.0"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -29,6 +29,13 @@ const Router = ...@@ -29,6 +29,13 @@ const Router =
@inject('healthStore', 'onboardingStore') @inject('healthStore', 'onboardingStore')
@observer @observer
class App extends Component { class App extends Component {
componentDidCatch () {
if (process.env.NODE_ENV !== 'development') {
// Redirect to '/' on errors
window.location.href = '/';
}
}
render () { render () {
return ( return (
<Router> <Router>
......
...@@ -4,32 +4,20 @@ ...@@ -4,32 +4,20 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react'; import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import { Route, Redirect, Switch } from 'react-router-dom'; import { Route, Redirect, Switch } from 'react-router-dom';
import Sent from './Sent'; import Sent from './Sent';
import Signer from './Signer'; import Signer from './Signer';
import TxForm from './TxForm'; import TxForm from './TxForm';
@inject('sendStore')
@observer
class Send extends Component { class Send extends Component {
render () { render () {
const {
sendStore: { tokenAddress }
} = this.props;
// We only show then Send components if we have already selected a token to
// send.
if (!tokenAddress) {
return <Redirect to='/' />;
}
return ( return (
<Switch> <Switch>
<Route exact path='/send' component={TxForm} /> <Route exact path='/send/:tokenAddress' component={TxForm} />
<Route path='/send/signer' component={Signer} /> <Route path='/send/:tokenAddress/signer' component={Signer} />
<Route path='/send/sent' component={Sent} /> <Route path='/send/:tokenAddress/sent' component={Sent} />
<Redirect to='/' />
</Switch> </Switch>
); );
} }
......
...@@ -4,65 +4,41 @@ ...@@ -4,65 +4,41 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react'; import React, { Component } from 'react';
import { findDOMNode } from 'react-dom'; import { Field, Form } from 'react-final-form';
import { FormField, Header } from 'fether-ui'; import { Form as FetherForm, Header } from 'fether-ui';
import { inject, observer } from 'mobx-react'; import { inject, observer } from 'mobx-react';
import { Link } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import ReactTooltip from 'react-tooltip'; import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance'; import TokenBalance from '../../Tokens/TokensList/TokenBalance';
@inject('sendStore', 'tokensStore') @inject('sendStore', 'tokensStore')
@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
token: tokensStore.tokens[tokenAddress]
}))
@observer @observer
class Signer extends Component { class Signer extends Component {
state = { handleAccept = values => {
error: null, const { history, sendStore, token } = this.props;
isSending: false,
password: '' return sendStore
}; .send(token, values.password)
.then(() => history.push(`/send/${token.address}/sent`))
handleAccept = event => { .catch(error => ({
const { history, sendStore } = this.props; password: error.text
const { password } = this.state; }));
event.preventDefault();
this.setState({ isSending: true }, () => {
sendStore
.send(password)
.then(() => history.push('/send/sent'))
.catch(error => {
this.setState({ error, isSending: false }, () =>
ReactTooltip.show(findDOMNode(this.tooltip))
);
});
});
};
handleCancel = () => {
const { history } = this.props;
history.goBack();
};
handleChangePassword = ({ target: { value } }) => {
this.setState({ error: null, password: value });
};
/**
* TODO All this tooltips refs etc should go inside a React validation
* library.
*/
handleTooltipRef = ref => {
this.tooltip = ref;
}; };
render () { render () {
const { const {
sendStore: { tokenAddress, tx }, history,
tokensStore sendStore: { tx },
token
} = this.props; } = this.props;
const { error, isSending, password } = this.state;
const token = tokensStore.tokens[tokenAddress]; if (!tx || !token) {
return <Redirect to='/' />;
}
return ( return (
<div> <div>
...@@ -72,7 +48,7 @@ class Signer extends Component { ...@@ -72,7 +48,7 @@ class Signer extends Component {
Close Close
</Link> </Link>
} }
title={<h1>Send {token.name}</h1>} title={token && <h1>Send {token.name}</h1>}
/> />
<div className='window_content'> <div className='window_content'>
...@@ -80,64 +56,63 @@ class Signer extends Component { ...@@ -80,64 +56,63 @@ class Signer extends Component {
<TokenBalance <TokenBalance
drawers={[ drawers={[
<div key='txForm'> <div key='txForm'>
<div className='form_field'> <FetherForm.Field
<label>Amount</label> className='form_field_value'
<div className='form_field_value'> disabled
{tx.amount} {token.symbol} defaultValue={`${tx.amount} ${token.symbol}`}
</div> label='Amount'
</div> />
<div className='form_field'>
<label>To</label> <FetherForm.Field
<div className='form_field_value'>{tx.to}</div> as='textarea'
</div> className='form_field_value'
disabled
defaultValue={tx.to}
label='To'
/>
</div>, </div>,
<form key='signerForm' onSubmit={this.handleAccept}> <Form
<div className='text'> key='signerForm'
<p>Enter your password to confirm this transaction.</p> onSubmit={this.handleAccept}
</div> render={({ handleSubmit, pristine, submitting }) => (
<form onSubmit={handleSubmit}>
<div <div className='text'>
data-tip={error ? error.text : ''} <p>Enter your password to confirm this transaction.</p>
ref={this.handleTooltipRef} </div>
>
<FormField <Field
label='Password' label='Password'
onChange={this.handleChangePassword} name='password'
required render={FetherForm.Field}
type='password' required
value={password} type='password'
/> />
</div>
<nav className='form-nav -binary'>
<nav className='form-nav -binary'> <button
<button className='button -cancel'
className='button -cancel' onClick={history.goBack}
onClick={this.handleCancel} type='button'
type='button' >
> Cancel
Cancel </button>
</button>
<button
<button className='button -submit'
className='button -submit' disabled={pristine || submitting}
disabled={!password.length || isSending || error} >
> Confirm transaction
Confirm transaction </button>
</button> </nav>
</nav> </form>
</form> )}
/>
]} ]}
onClick={null} onClick={null}
token={token} token={token}
/> />
</div> </div>
</div> </div>
<ReactTooltip
effect='solid'
event='mouseover'
eventOff='keydown mouseout'
place='top'
/>
</div> </div>
); );
} }
......
...@@ -4,13 +4,15 @@ ...@@ -4,13 +4,15 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react'; import React, { Component } from 'react';
import debounce from 'lodash/debounce'; import debounce from 'debounce-promise';
import { FormField, Header } from 'fether-ui'; import { estimateGas } from '../../utils/estimateGas';
import { Field, Form } from 'react-final-form';
import { Form as FetherForm, Header } from 'fether-ui';
import { fromWei, toWei } from '@parity/api/lib/util/wei'; import { fromWei, toWei } from '@parity/api/lib/util/wei';
import { inject, observer } from 'mobx-react'; import { inject, observer } from 'mobx-react';
import { isAddress } from '@parity/api/lib/util/address'; import { isAddress } from '@parity/api/lib/util/address';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip'; import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance'; import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withBalance from '../../utils/withBalance'; import withBalance from '../../utils/withBalance';
...@@ -18,120 +20,24 @@ import withBalance from '../../utils/withBalance'; ...@@ -18,120 +20,24 @@ import withBalance from '../../utils/withBalance';
const MAX_GAS_PRICE = 40; // In Gwei const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
@inject('sendStore', 'tokensStore') @inject('parityStore', 'sendStore', 'tokensStore')
@withBalance( @withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
({ sendStore: { tokenAddress }, tokensStore }) => token: tokensStore.tokens[tokenAddress]
tokensStore.tokens[tokenAddress] }))
) @withBalance
@observer @observer
class Send extends Component { class Send extends Component {
state = { handleSubmit = values => {
amount: '', // In Ether or in token const { history, sendStore, token } = this.props;
gasPrice: 4, // in Gwei sendStore.setTx(values);
to: '', history.push(`/send/${token.address}/signer`);
estimating: false, // Currently estimating gasPrice
...this.props.sendStore.tx
};
static getDerivedStateFromProps (nextProps, prevState) {
const {
balance,
sendStore: { estimated }
} = nextProps;
// Calculate the maxAount
return {
maxAmount:
balance && estimated
? +fromWei(
toWei(balance).minus(
estimated.mul(toWei(prevState.gasPrice, 'shannon'))
)
)
: 0.01
};
}
componentDidMount () {
this.handleEstimateGasPrice();
}
estimateGas = debounce(
() =>
this.props.sendStore
.estimateGas()
.then(() => this.setState({ estimating: false }))
.catch(() => this.setState({ estimating: false })),
1000
);
handleChangeAmount = ({ target: { value } }) =>
this.setState({ amount: value }, this.handleEstimateGasPrice);
handleChangeGasPrice = ({ target: { value } }) =>
this.setState({ gasPrice: value }, this.handleEstimateGasPrice);
handleChangeTo = ({ target: { value } }) => {
this.setState({ to: value }, this.handleEstimateGasPrice);
};
handleEstimateGasPrice = () => {
if (this.hasError()) {
return;
}
const { amount, gasPrice, to } = this.state;
this.props.sendStore.setTx({ amount, gasPrice, to });
this.setState({ estimating: true }, this.estimateGas);
};
handleMax = () =>
this.setState(
{ amount: this.state.maxAmount },
this.handleEstimateGasPrice
);
handleSubmit = event => {
event.preventDefault();
const { history } = this.props;
history.push('/send/signer');
};
/**
* Get form errors.
*
* TODO Use a React form library to do this?
*/
hasError = () => {
const { amount, maxAmount, to } = this.state;
if (!amount || isNaN(amount)) {
return 'Please enter a valid amount';
}
if (amount < 0) {
return 'Please enter a positive amount ';
}
if (amount > maxAmount) {
return "You don't have enough balance";
}
if (!isAddress(to)) {
return 'Please enter a valid Ethereum address';
}
return null;
}; };
render () { render () {
const { const {
sendStore: { tokenAddress }, sendStore: { tx },
tokensStore token
} = this.props; } = this.props;
const { amount, estimating, gasPrice, maxAmount, to } = this.state;
const token = tokensStore.tokens[tokenAddress];
const error = this.hasError();
return ( return (
<div> <div>
...@@ -141,7 +47,7 @@ class Send extends Component { ...@@ -141,7 +47,7 @@ class Send extends Component {
Close Close
</Link> </Link>
} }
title={<h1>Send {token.name}</h1>} title={token && <h1>Send {token.name}</h1>}
/> />
<div className='window_content'> <div className='window_content'>
...@@ -149,104 +55,122 @@ class Send extends Component { ...@@ -149,104 +55,122 @@ class Send extends Component {
<TokenBalance <TokenBalance
decimals={6} decimals={6}
drawers={[ drawers={[
<form <Form
className='send-form'
key='txForm' key='txForm'
initialValues={{ gasPrice: 4, ...tx }}
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
> validate={this.validateForm}
<fieldset className='form_fields'> render={({ handleSubmit, valid, validating, values }) => (
<FormField <form className='send-form' onSubmit={handleSubmit}>
input={ <fieldset className='form_fields'>
<div> <Field
<input className='form_field_amount'
className='form_field_amount' formNoValidate
formNoValidate label='Amount'
max={maxAmount} name='amount'
min={0} placeholder='1.00'
onChange={this.handleChangeAmount} render={FetherForm.Field}
placeholder='1.00' required
required type='number' // In ETH or coin
step={+fromWei(1)} />
type='number'
value={amount} <Field
/> as='textarea'
<nav className='form-field_nav'>
<button
className='button -utility'
onClick={this.handleMax}
tabIndex={-1}
type='button'
>
Max
</button>
</nav>
</div>
}
label='Amount'
/>
<FormField
input={
<textarea
className='-sm' className='-sm'
onChange={this.handleChangeTo} label='To'
name='to'
placeholder='0x...'