Commit 6c168635 authored by Kenny Tran's avatar Kenny Tran Committed by Chi Kei Chan

Create error state for currency input (#43)

* Create error state for currency input

* Create error functionality for redux

* Add proptypes and convert to float before compare

* Use big number for balance comparison
parent c691cfab
...@@ -7,9 +7,13 @@ ...@@ -7,9 +7,13 @@
position: relative; position: relative;
z-index: 200; z-index: 200;
border-radius: 1.25rem; border-radius: 1.25rem;
border: 1px solid $mercury-gray; border: 0.5px solid $mercury-gray;
background-color: $white; background-color: $white;
box-shadow: 0px 4px 4px 2px rgba($royal-blue, 0.05); box-shadow: 0px 4px 4px 2px rgba($royal-blue, 0.05);
&--error {
border: 0.5px solid $salmon-red;
}
} }
&__label-row { &__label-row {
...@@ -42,6 +46,15 @@ ...@@ -42,6 +46,15 @@
&__input { &__input {
@extend %borderless-input; @extend %borderless-input;
&--error {
color: $salmon-red;
}
}
&__extra-text {
&--error {
color: $salmon-red;
}
} }
&__currency-select { &__currency-select {
......
...@@ -6,6 +6,7 @@ import classnames from 'classnames'; ...@@ -6,6 +6,7 @@ import classnames from 'classnames';
import {BigNumber as BN} from 'bignumber.js'; import {BigNumber as BN} from 'bignumber.js';
import Fuse from '../../helpers/fuse'; import Fuse from '../../helpers/fuse';
import { updateField } from '../../ducks/swap'; import { updateField } from '../../ducks/swap';
import { INSUFFICIENT_BALANCE } from '../../constants/currencyInputErrorTypes';
import Modal from '../Modal'; import Modal from '../Modal';
import TokenLogo from '../TokenLogo'; import TokenLogo from '../TokenLogo';
import SearchIcon from '../../assets/images/magnifying-glass.svg'; import SearchIcon from '../../assets/images/magnifying-glass.svg';
...@@ -41,11 +42,17 @@ class CurrencyInputPanel extends Component { ...@@ -41,11 +42,17 @@ class CurrencyInputPanel extends Component {
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired,
}).isRequired, }).isRequired,
errors: PropTypes.arrayOf(PropTypes.string),
addError: PropTypes.func,
removeError: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
onCurrencySelected() {}, onCurrencySelected() {},
onValueChange() {}, onValueChange() {},
addError() {},
removeError() {},
errors: [],
}; };
static contextTypes = { static contextTypes = {
...@@ -99,24 +106,24 @@ class CurrencyInputPanel extends Component { ...@@ -99,24 +106,24 @@ class CurrencyInputPanel extends Component {
const { web3 } = drizzle; const { web3 } = drizzle;
if (!selectedTokenAddress || !initialized || !web3 || !balance) { if (!selectedTokenAddress || !initialized || !web3 || !balance) {
return ''; return null;
} }
if (selectedTokenAddress === 'ETH') { if (selectedTokenAddress === 'ETH') {
return `Balance: ${BN(web3.utils.fromWei(balance, 'ether')).toFixed(2)}`; return BN(web3.utils.fromWei(balance, 'ether')).toFixed(2);
} }
const tokenData = this.getTokenData(selectedTokenAddress); const tokenData = this.getTokenData(selectedTokenAddress);
if (!tokenData) { if (!tokenData) {
return ''; return null;
} }
const tokenBalance = BN(tokenData.balance); const tokenBalance = BN(tokenData.balance);
const denomination = Math.pow(10, tokenData.decimals); const denomination = Math.pow(10, tokenData.decimals);
const adjustedBalance = tokenBalance.dividedBy(denomination); const adjustedBalance = tokenBalance.dividedBy(denomination);
return `Balance: ${adjustedBalance.toFixed(2)}`; return adjustedBalance.toFixed(2);
} }
createTokenList = () => { createTokenList = () => {
...@@ -135,6 +142,18 @@ class CurrencyInputPanel extends Component { ...@@ -135,6 +142,18 @@ class CurrencyInputPanel extends Component {
return tokenList; return tokenList;
}; };
validate = (balance) => {
const { value, addError, removeError, errors } = this.props;
const hasInsufficientBalance = errors.indexOf(INSUFFICIENT_BALANCE) > -1;
const balanceIsLess = BN(value).isGreaterThan(BN(balance));
if (balanceIsLess && !hasInsufficientBalance) {
addError(INSUFFICIENT_BALANCE);
} else if (!balanceIsLess && hasInsufficientBalance) {
removeError(INSUFFICIENT_BALANCE);
}
};
onTokenSelect = (address) => { onTokenSelect = (address) => {
this.setState({ this.setState({
selectedTokenAddress: address || 'ETH', selectedTokenAddress: address || 'ETH',
...@@ -178,6 +197,14 @@ class CurrencyInputPanel extends Component { ...@@ -178,6 +197,14 @@ class CurrencyInputPanel extends Component {
} }
}; };
renderBalance(balance) {
if (balance === null) {
return null;
}
return `Balance: ${balance}`;
}
renderTokenList() { renderTokenList() {
const tokens = this.createTokenList(); const tokens = this.createTokenList();
const { searchQuery } = this.state; const { searchQuery } = this.state;
...@@ -250,26 +277,43 @@ class CurrencyInputPanel extends Component { ...@@ -250,26 +277,43 @@ class CurrencyInputPanel extends Component {
const { const {
title, title,
description, description,
value,
errors
} = this.props; } = this.props;
const { selectedTokenAddress } = this.state; const { selectedTokenAddress } = this.state;
const balance = this.getBalance();
this.validate(balance);
const hasInsufficientBalance = errors.indexOf(INSUFFICIENT_BALANCE) > -1;
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div className="currency-input-panel__container"> <div className={
classnames('currency-input-panel__container',
{ 'currency-input-panel__container--error': hasInsufficientBalance }
)
}>
<div className="currency-input-panel__label-row"> <div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title}</span> <span className="currency-input-panel__label">{title}</span>
<span className="currency-input-panel__label-description">{description}</span> <span className="currency-input-panel__label-description">{description}</span>
</div> </div>
<span className="currency-input-panel__extra-text"> <span className={
{this.getBalance()} classnames('currency-input-panel__extra-text',
{ 'currency-input-panel__extra-text--error': hasInsufficientBalance }
)
}>
{this.renderBalance(balance)}
</span> </span>
</div> </div>
<div className="currency-input-panel__input-row"> <div className="currency-input-panel__input-row">
<input <input
type="number" type="number"
className="currency-input-panel__input" className={
classnames('currency-input-panel__input',
{ 'currency-input-panel__input--error': hasInsufficientBalance }
)
}
placeholder="0.0" placeholder="0.0"
onChange={e => this.props.onValueChange(e.target.value)} onChange={e => this.props.onValueChange(e.target.value)}
value={this.props.value} value={this.props.value}
......
export const INSUFFICIENT_BALANCE = 'Insufficient balance';
import { import {
EXCHANGE_CONTRACT_READY EXCHANGE_CONTRACT_READY
} from '../constants' } from '../constants/actionTypes';
// definitely needs to be redux thunk // definitely needs to be redux thunk
export const exchangeContractReady = (symbol, exchangeContract) => ({ export const exchangeContractReady = (symbol, exchangeContract) => ({
......
...@@ -24,7 +24,7 @@ import { ...@@ -24,7 +24,7 @@ import {
SET_INVEST_ETH_REQUIRED, SET_INVEST_ETH_REQUIRED,
SET_INVEST_TOKENS_REQUIRED, SET_INVEST_TOKENS_REQUIRED,
SET_INVEST_CHECKED SET_INVEST_CHECKED
} from '../constants'; } from '../constants/actionTypes';
export const setInputBalance = (inputBalance) => ({ export const setInputBalance = (inputBalance) => ({
......
const UPDATE_FIELD = 'app/swap/updateField'; const UPDATE_FIELD = 'app/swap/updateField';
const ADD_ERROR = 'app/swap/addError';
const REMOVE_ERROR = 'app/swap/removeError';
const initialState = { const initialState = {
input: '', input: '',
...@@ -6,12 +8,46 @@ const initialState = { ...@@ -6,12 +8,46 @@ const initialState = {
inputCurrency: '', inputCurrency: '',
outputCurrency: '', outputCurrency: '',
lastEditedField: '', lastEditedField: '',
inputErrors: [],
outputErrors: [],
}; };
export const updateField = ({ name, value }) => ({ export const updateField = ({ name, value }) => ({
type: UPDATE_FIELD, type: UPDATE_FIELD,
payload: { name, value }, payload: { name, value },
}) });
export const addError = ({ name, value }) => ({
type: ADD_ERROR,
payload: { name, value },
});
export const removeError = ({ name, value }) => ({
type: REMOVE_ERROR,
payload: { name, value },
});
function reduceAddError(state, payload) {
const { name, value } = payload;
let nextErrors = state[name];
if (nextErrors.indexOf(value) === -1) {
nextErrors = [...nextErrors, value];
}
return {
...state,
[name]: nextErrors,
};
}
function reduceRemoveError(state, payload) {
const { name, value } = payload;
return {
...state,
[name]: state[name].filter(error => error !== value),
};
}
export default function swapReducer(state = initialState, { type, payload }) { export default function swapReducer(state = initialState, { type, payload }) {
switch (type) { switch (type) {
...@@ -20,6 +56,10 @@ export default function swapReducer(state = initialState, { type, payload }) { ...@@ -20,6 +56,10 @@ export default function swapReducer(state = initialState, { type, payload }) {
...state, ...state,
[payload.name]: payload.value, [payload.name]: payload.value,
}; };
case ADD_ERROR:
return reduceAddError(state, payload);
case REMOVE_ERROR:
return reduceRemoveError(state, payload);
default: default:
return state; return state;
} }
......
import { import {
TOKEN_CONTRACT_READY TOKEN_CONTRACT_READY
} from '../constants'; } from '../constants/actionTypes';
// again, needs to be redux thunk // again, needs to be redux thunk
export const tokenContractReady = (symbol, tokenContract) => ({ export const tokenContractReady = (symbol, tokenContract) => ({
......
...@@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom'; ...@@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import {BigNumber as BN} from "bignumber.js"; import {BigNumber as BN} from "bignumber.js";
import { updateField } from '../../ducks/swap'; import { updateField, addError, removeError } from '../../ducks/swap';
import Header from '../../components/Header'; import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel'; import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel'; import OversizedPanel from '../../components/OversizedPanel';
...@@ -32,6 +32,8 @@ class Swap extends Component { ...@@ -32,6 +32,8 @@ class Swap extends Component {
inputCurrency: PropTypes.string, inputCurrency: PropTypes.string,
outputCurrency: PropTypes.string, outputCurrency: PropTypes.string,
lastEditedField: PropTypes.string, lastEditedField: PropTypes.string,
inputErrors: PropTypes.arrayOf(PropTypes.string),
outputErrors: PropTypes.arrayOf(PropTypes.string),
}; };
static contextTypes = { static contextTypes = {
...@@ -192,7 +194,7 @@ class Swap extends Component { ...@@ -192,7 +194,7 @@ class Swap extends Component {
}; };
render() { render() {
const { lastEditedField, inputCurrency, outputCurrency, input, output } = this.props; const { lastEditedField, inputCurrency, outputCurrency, input, output, outputErrors, inputErrors } = this.props;
const { exchangeRate } = this.state; const { exchangeRate } = this.state;
const inputLabel = this.getTokenLabel(inputCurrency); const inputLabel = this.getTokenLabel(inputCurrency);
const outputLabel = this.getTokenLabel(outputCurrency); const outputLabel = this.getTokenLabel(outputCurrency);
...@@ -212,6 +214,9 @@ class Swap extends Component { ...@@ -212,6 +214,9 @@ class Swap extends Component {
onCurrencySelected={d => this.props.updateField('inputCurrency', d)} onCurrencySelected={d => this.props.updateField('inputCurrency', d)}
onValueChange={d => this.updateInput(d)} onValueChange={d => this.updateInput(d)}
selectedTokens={[inputCurrency, outputCurrency]} selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('inputErrors', error)}
removeError={error => this.props.removeError('inputErrors', error)}
errors={inputErrors}
value={input} value={input}
/> />
<OversizedPanel> <OversizedPanel>
...@@ -225,6 +230,9 @@ class Swap extends Component { ...@@ -225,6 +230,9 @@ class Swap extends Component {
onCurrencySelected={d => this.props.updateField('outputCurrency', d)} onCurrencySelected={d => this.props.updateField('outputCurrency', d)}
onValueChange={d => this.updateOutput(d)} onValueChange={d => this.updateOutput(d)}
selectedTokens={[inputCurrency, outputCurrency]} selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('outputErrors', error)}
removeError={error => this.props.removeError('outputErrors', error)}
errors={outputErrors}
value={output} value={output}
/> />
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
...@@ -283,9 +291,13 @@ export default withRouter( ...@@ -283,9 +291,13 @@ export default withRouter(
outputCurrency: state.swap.outputCurrency, outputCurrency: state.swap.outputCurrency,
lastEditedField: state.swap.lastEditedField, lastEditedField: state.swap.lastEditedField,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses,
inputErrors: state.swap.inputErrors,
outputErrors: state.swap.outputErrors,
}), }),
dispatch => ({ dispatch => ({
updateField: (name, value) => dispatch(updateField({ name, value })), updateField: (name, value) => dispatch(updateField({ name, value })),
addError: (name, value) => dispatch(addError({ name, value })),
removeError: (name, value) => dispatch(removeError({ name, value }))
}) })
), ),
); );
...@@ -16,6 +16,9 @@ $royal-blue: #2F80ED; ...@@ -16,6 +16,9 @@ $royal-blue: #2F80ED;
// Purple // Purple
$wisteria-purple: #AE60B9; $wisteria-purple: #AE60B9;
// Red
$salmon-red: #FF8368;
%col-nowrap { %col-nowrap {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment