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

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

Form validation
parents 254ecab9 dba232c0
......@@ -38,8 +38,10 @@
"@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94",
"@parity/shared": "^3.0.2",
"bignumber.js": "^4.1.0",
"debounce-promise": "^3.1.0",
"debug": "^3.1.0",
"fether-ui": "^0.1.0",
"final-form": "^4.8.3",
"is-electron": "^2.1.0",
"light-hoc": "^0.1.0",
"lodash": "^4.17.10",
......@@ -48,10 +50,12 @@
"react": "^16.3.2",
"react-blockies": "^1.3.0",
"react-dom": "^16.3.2",
"react-final-form": "^3.6.4",
"react-markdown": "^3.3.4",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.4",
"react-tooltip": "^3.6.1",
"recompose": "^0.27.1",
"rxjs": "^6.2.0"
},
"devDependencies": {
......
......@@ -29,6 +29,13 @@ const Router =
@inject('healthStore', 'onboardingStore')
@observer
class App extends Component {
componentDidCatch () {
if (process.env.NODE_ENV !== 'development') {
// Redirect to '/' on errors
window.location.href = '/';
}
}
render () {
return (
<Router>
......
......@@ -4,32 +4,20 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import { Route, Redirect, Switch } from 'react-router-dom';
import Sent from './Sent';
import Signer from './Signer';
import TxForm from './TxForm';
@inject('sendStore')
@observer
class Send extends Component {
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 (
<Switch>
<Route exact path='/send' component={TxForm} />
<Route path='/send/signer' component={Signer} />
<Route path='/send/sent' component={Sent} />
<Route exact path='/send/:tokenAddress' component={TxForm} />
<Route path='/send/:tokenAddress/signer' component={Signer} />
<Route path='/send/:tokenAddress/sent' component={Sent} />
<Redirect to='/' />
</Switch>
);
}
......
......@@ -4,65 +4,41 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { FormField, Header } from 'fether-ui';
import { Field, Form } from 'react-final-form';
import { Form as FetherForm, Header } from 'fether-ui';
import { inject, observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { Link, Redirect } from 'react-router-dom';
import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
@inject('sendStore', 'tokensStore')
@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
token: tokensStore.tokens[tokenAddress]
}))
@observer
class Signer extends Component {
state = {
error: null,
isSending: false,
password: ''
};
handleAccept = event => {
const { history, sendStore } = this.props;
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;
handleAccept = values => {
const { history, sendStore, token } = this.props;
return sendStore
.send(token, values.password)
.then(() => history.push(`/send/${token.address}/sent`))
.catch(error => ({
password: error.text
}));
};
render () {
const {
sendStore: { tokenAddress, tx },
tokensStore
history,
sendStore: { tx },
token
} = this.props;
const { error, isSending, password } = this.state;
const token = tokensStore.tokens[tokenAddress];
if (!tx || !token) {
return <Redirect to='/' />;
}
return (
<div>
......@@ -72,7 +48,7 @@ class Signer extends Component {
Close
</Link>
}
title={<h1>Send {token.name}</h1>}
title={token && <h1>Send {token.name}</h1>}
/>
<div className='window_content'>
......@@ -80,64 +56,63 @@ class Signer extends Component {
<TokenBalance
drawers={[
<div key='txForm'>
<div className='form_field'>
<label>Amount</label>
<div className='form_field_value'>
{tx.amount} {token.symbol}
</div>
</div>
<div className='form_field'>
<label>To</label>
<div className='form_field_value'>{tx.to}</div>
</div>
<FetherForm.Field
className='form_field_value'
disabled
defaultValue={`${tx.amount} ${token.symbol}`}
label='Amount'
/>
<FetherForm.Field
as='textarea'
className='form_field_value'
disabled
defaultValue={tx.to}
label='To'
/>
</div>,
<form key='signerForm' onSubmit={this.handleAccept}>
<div className='text'>
<p>Enter your password to confirm this transaction.</p>
</div>
<div
data-tip={error ? error.text : ''}
ref={this.handleTooltipRef}
>
<FormField
label='Password'
onChange={this.handleChangePassword}
required
type='password'
value={password}
/>
</div>
<nav className='form-nav -binary'>
<button
className='button -cancel'
onClick={this.handleCancel}
type='button'
>
Cancel
</button>
<button
className='button -submit'
disabled={!password.length || isSending || error}
>
Confirm transaction
</button>
</nav>
</form>
<Form
key='signerForm'
onSubmit={this.handleAccept}
render={({ handleSubmit, pristine, submitting }) => (
<form onSubmit={handleSubmit}>
<div className='text'>
<p>Enter your password to confirm this transaction.</p>
</div>
<Field
label='Password'
name='password'
render={FetherForm.Field}
required
type='password'
/>
<nav className='form-nav -binary'>
<button
className='button -cancel'
onClick={history.goBack}
type='button'
>
Cancel
</button>
<button
className='button -submit'
disabled={pristine || submitting}
>
Confirm transaction
</button>
</nav>
</form>
)}
/>
]}
onClick={null}
token={token}
/>
</div>
</div>
<ReactTooltip
effect='solid'
event='mouseover'
eventOff='keydown mouseout'
place='top'
/>
</div>
);
}
......
......@@ -4,13 +4,15 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import debounce from 'lodash/debounce';
import { FormField, Header } from 'fether-ui';
import debounce from 'debounce-promise';
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 { inject, observer } from 'mobx-react';
import { isAddress } from '@parity/api/lib/util/address';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withBalance from '../../utils/withBalance';
......@@ -18,120 +20,24 @@ import withBalance from '../../utils/withBalance';
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
@inject('sendStore', 'tokensStore')
@withBalance(
({ sendStore: { tokenAddress }, tokensStore }) =>
tokensStore.tokens[tokenAddress]
)
@inject('parityStore', 'sendStore', 'tokensStore')
@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
token: tokensStore.tokens[tokenAddress]
}))
@withBalance
@observer
class Send extends Component {
state = {
amount: '', // In Ether or in token
gasPrice: 4, // in Gwei
to: '',
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;
handleSubmit = values => {
const { history, sendStore, token } = this.props;
sendStore.setTx(values);
history.push(`/send/${token.address}/signer`);
};
render () {
const {
sendStore: { tokenAddress },
tokensStore
sendStore: { tx },
token
} = this.props;
const { amount, estimating, gasPrice, maxAmount, to } = this.state;
const token = tokensStore.tokens[tokenAddress];
const error = this.hasError();
return (
<div>
......@@ -141,7 +47,7 @@ class Send extends Component {
Close
</Link>
}
title={<h1>Send {token.name}</h1>}
title={token && <h1>Send {token.name}</h1>}
/>
<div className='window_content'>
......@@ -149,104 +55,122 @@ class Send extends Component {
<TokenBalance
decimals={6}
drawers={[
<form
className='send-form'
<Form
key='txForm'
initialValues={{ gasPrice: 4, ...tx }}
onSubmit={this.handleSubmit}
>
<fieldset className='form_fields'>
<FormField
input={
<div>
<input
className='form_field_amount'
formNoValidate
max={maxAmount}
min={0}
onChange={this.handleChangeAmount}
placeholder='1.00'
required
step={+fromWei(1)}
type='number'
value={amount}
/>
<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
validate={this.validateForm}
render={({ handleSubmit, valid, validating, values }) => (
<form className='send-form' onSubmit={handleSubmit}>
<fieldset className='form_fields'>
<Field
className='form_field_amount'
formNoValidate
label='Amount'
name='amount'
placeholder='1.00'
render={FetherForm.Field}
required
type='number' // In ETH or coin
/>
<Field
as='textarea'
className='-sm'
onChange={this.handleChangeTo}
label='To'
name='to'
placeholder='0x...'
required
type='text'
value={to}
render={FetherForm.Field}
/>
}
label='To'
/>
<FormField
className='-range'
input={
<div>
<input
max={MAX_GAS_PRICE}
min={MIN_GAS_PRICE}
onChange={this.handleChangeGasPrice}
required
step={0.5}
type='range'
value={gasPrice}
/>
<nav className='range-nav'>
<span className='range-nav_label'>Cheap</span>
<span className='range-nav_value'>
{gasPrice} Gwei
</span>
<span className='range-nav_label'>Fast</span>
</nav>
</div>
}
label='Gas'
/>
</fieldset>
<nav className='form-nav'>
<span data-tip={error || ''}>
<button disabled={error || estimating} className='button'>
{estimating ? 'Checking...' : 'Send'}
</button>
</span>
</nav>
</form>
<Field
centerText={`${values.gasPrice} GWEI`}
className='-range'
label='Gas'
leftText='Cheap'
max={MAX_GAS_PRICE}
min={MIN_GAS_PRICE}
name='gasPrice'
render={FetherForm.Slider}
required
rightText='Fast'
step={0.5}
type='range' // In Gwei
/>
</fieldset>
<nav className='form-nav'>
<button
disabled={!valid || validating}
className='button'