Swap UX Cleanup (#339)

Fixes #226
Fixes #383

Added a simple check to ensure that swap rates exist so we don't get a NaN error, which would be confusing to users.

Instead, the form is hidden and a spinner is shown until the bityRates are ready for the user.

* adds spinner while fetching

* added error on top of input

* removed classnames prop and added cn

* added simple err mssge

* css fixes and disabled button

* better err mssge generation and fixed swap details

* minor typo

* added redux notification on unreachable error

* minheight for err message and swap update on redux change

* fixed formatting and removed className prop
This commit is contained in:
Eddie Wang 2017-11-14 13:37:56 -05:00 committed by Daniel Ternyak
parent 8fe664c171
commit a9f78011cb
3 changed files with 209 additions and 78 deletions

View File

@ -1,4 +1,4 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.CurrencySwap {
text-align: center;
@ -13,6 +13,30 @@
display: inline-block;
vertical-align: top;
}
&-input-group {
display: inline-block;
}
&-error-message {
display: block;
min-height: 25px;
color: $brand-danger;
text-align: left;
}
&-inner-wrap {
display: block;
}
@media (min-width: $screen-xs-min) {
&-inner-wrap {
display: flex;
align-items: center;
justify-content: center;
}
}
&-dropdown {
display: inline-block;
margin-top: 0.6rem;
margin-bottom: 0.6rem;
}
&-input {
width: 100%;

View File

@ -13,6 +13,7 @@ import translate from 'translations';
import { combineAndUpper, toFixedIfLarger } from 'utils/formatters';
import './CurrencySwap.scss';
import { Dropdown } from 'components/ui';
import Spinner from 'components/ui/Spinner';
export interface StateProps {
bityRates: any;
@ -36,6 +37,8 @@ export interface ActionProps {
interface State {
disabled: boolean;
showedMinMaxError: boolean;
originErr: string;
destinationErr: string;
}
export default class CurrencySwap extends Component<
@ -44,9 +47,31 @@ export default class CurrencySwap extends Component<
> {
public state = {
disabled: true,
showedMinMaxError: false
showedMinMaxError: false,
originErr: '',
destinationErr: ''
};
public componentWillReceiveProps(newProps) {
const {
originAmount,
originKind,
destinationKind,
destinationAmount
} = newProps;
if (
originKind !== this.props.originKind ||
destinationKind !== this.props.destinationKind
) {
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmount
);
}
}
public isMinMaxValid = (amount, kind) => {
let bityMin;
let bityMax;
@ -70,38 +95,86 @@ export default class CurrencySwap extends Component<
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
};
public setDisabled(originAmount, originKind, destinationAmount) {
public setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmount
) {
const disabled = this.isDisabled(
originAmount,
originKind,
destinationAmount
);
if (disabled && originAmount && !this.state.showedMinMaxError) {
if (disabled && originAmount) {
const { bityRates } = this.props;
const ETHMin = generateKindMin(bityRates.BTCETH, 'ETH');
const ETHMax = generateKindMax(bityRates.BTCETH, 'ETH');
const REPMin = generateKindMin(bityRates.BTCREP, 'REP');
const notificationMessage = `
Minimum amount ${bityConfig.BTCMin} BTC,
${toFixedIfLarger(ETHMin, 3)} ETH.
Max amount ${bityConfig.BTCMax} BTC,
${toFixedIfLarger(ETHMax, 3)} ETH, or
${toFixedIfLarger(REPMin, 3)} REP
`;
this.setState(
{
disabled,
showedMinMaxError: true
},
() => {
this.props.showNotification('danger', notificationMessage, 10000);
const getRates = kind => {
let minAmount;
let maxAmount;
switch (kind) {
case 'BTC':
minAmount = toFixedIfLarger(bityConfig.BTCMin, 3);
maxAmount = toFixedIfLarger(bityConfig.BTCMax, 3);
break;
case 'ETH':
minAmount = toFixedIfLarger(ETHMin, 3);
maxAmount = toFixedIfLarger(ETHMax, 3);
break;
case 'REP':
minAmount = toFixedIfLarger(REPMin, 3);
break;
default:
if (this.state.showedMinMaxError) {
this.setState(
{
showedMinMaxError: true
},
() => {
this.props.showNotification(
'danger',
"Couldn't get match currency kind. Something went terribly wrong",
10000
);
}
);
}
}
return { minAmount, maxAmount };
};
const createErrString = (kind, amount, rate) => {
let errString;
if (amount > rate.maxAmount) {
errString = `Maximum ${kind} is ${rate.maxAmount} ${kind}`;
} else {
errString = `Minimum ${kind} is ${rate.minAmount} ${kind}`;
}
return errString;
};
const originRate = getRates(originKind);
const destinationRate = getRates(destinationKind);
const originErr = createErrString(originKind, originAmount, originRate);
const destinationErr = createErrString(
destinationKind,
destinationAmount,
destinationRate
);
this.setState({
originErr,
destinationErr,
disabled: true
});
} else {
this.setState({
originErr: '',
destinationErr: '',
disabled
});
}
@ -114,7 +187,12 @@ export default class CurrencySwap extends Component<
public setOriginAndDestinationToNull = () => {
this.props.originAmountSwap(null);
this.props.destinationAmountSwap(null);
this.setDisabled(null, this.props.originKind, null);
this.setDisabled(
null,
this.props.originKind,
this.props.destinationKind,
null
);
};
public onChangeOriginAmount = (
@ -129,7 +207,12 @@ export default class CurrencySwap extends Component<
this.props.originAmountSwap(originAmountAsNumber);
const destinationAmount = originAmountAsNumber * bityRate;
this.props.destinationAmountSwap(destinationAmount);
this.setDisabled(originAmountAsNumber, originKind, destinationAmount);
this.setDisabled(
originAmountAsNumber,
originKind,
destinationKind,
destinationAmount
);
} else {
this.setOriginAndDestinationToNull();
}
@ -147,7 +230,12 @@ export default class CurrencySwap extends Component<
const bityRate = this.props.bityRates[pairNameReversed];
const originAmount = destinationAmountAsNumber * bityRate;
this.props.originAmountSwap(originAmount);
this.setDisabled(originAmount, originKind, destinationAmountAsNumber);
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmountAsNumber
);
} else {
this.setOriginAndDestinationToNull();
}
@ -160,69 +248,90 @@ export default class CurrencySwap extends Component<
originKind,
destinationKind,
destinationKindOptions,
originKindOptions
originKindOptions,
bityRates
} = this.props;
const { originErr, destinationErr } = this.state;
const OriginKindDropDown = Dropdown as new () => Dropdown<
typeof originKind
>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<
typeof destinationKind
>;
const pairName = combineAndUpper(originKind, destinationKind);
const bityLoaded = bityRates[pairName];
return (
<article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
<div className="form-inline">
<input
className={`CurrencySwap-input form-control ${String(
originAmount
) !== '' && this.isMinMaxValid(originAmount, originKind)
? 'is-valid'
: 'is-invalid'}`}
type="number"
placeholder="Amount"
value={originAmount || originAmount === 0 ? originAmount : ''}
onChange={this.onChangeOriginAmount}
/>
<OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${originKind}`}
options={originKindOptions}
value={originKind}
onChange={this.props.originKindSwap}
size="smr"
color="default"
/>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<input
className={`CurrencySwap-input form-control ${String(
destinationAmount
) !== '' && this.isMinMaxValid(originAmount, originKind)
? 'is-valid'
: 'is-invalid'}`}
type="number"
placeholder="Amount"
value={
destinationAmount || destinationAmount === 0
? destinationAmount
: ''
}
onChange={this.onChangeDestinationAmount}
/>
<DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${destinationKind}`}
options={destinationKindOptions}
value={destinationKind}
onChange={this.props.destinationKindSwap}
size="smr"
color="default"
/>
</div>
{bityLoaded ? (
<div className="form-inline CurrencySwap-inner-wrap">
<div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message">{originErr}</span>
<input
className={`CurrencySwap-input form-control ${
String(originAmount) !== '' &&
this.isMinMaxValid(originAmount, originKind)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={originAmount || originAmount === 0 ? originAmount : ''}
onChange={this.onChangeOriginAmount}
/>
<div className="CurrencySwap-dropdown">
<OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${
originKind
}`}
options={originKindOptions}
value={originKind}
onChange={this.props.originKindSwap}
size="smr"
color="default"
/>
</div>
</div>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message">
{destinationErr}
</span>
<input
className={`CurrencySwap-input form-control ${
String(destinationAmount) !== '' &&
this.isMinMaxValid(originAmount, originKind)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={
destinationAmount || destinationAmount === 0
? destinationAmount
: ''
}
onChange={this.onChangeDestinationAmount}
/>
<div className="CurrencySwap-dropdown">
<DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${
destinationKind
}`}
options={destinationKindOptions}
value={destinationKind}
onChange={this.props.destinationKindSwap}
size="smr"
color="default"
/>
</div>
</div>
</div>
) : (
<Spinner />
)}
<div className="CurrencySwap-submit">
<SimpleButton

View File

@ -66,9 +66,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Amount to send*/}
{!this.isExpanded() && (
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{` ${originAmount} ${originKind}`}
</h3>
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${originKind}`}</h3>
<p className="SwapInfo-details-block-label">
{translate('SEND_amount')}
</p>
@ -113,7 +111,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{`${computedOriginDestinationRatio &&
toFixedIfLarger(
computedOriginDestinationRatio
)} ${originKind}/${destinationKind}`}
)} ${destinationKind}/${originKind}`}
</h3>
<p className="SwapInfo-details-block-label">
{translate('SWAP_your_rate')}