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:
parent
8fe664c171
commit
a9f78011cb
|
@ -1,4 +1,4 @@
|
||||||
@import "common/sass/variables";
|
@import 'common/sass/variables';
|
||||||
|
|
||||||
.CurrencySwap {
|
.CurrencySwap {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -13,6 +13,30 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
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 {
|
&-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import translate from 'translations';
|
||||||
import { combineAndUpper, toFixedIfLarger } from 'utils/formatters';
|
import { combineAndUpper, toFixedIfLarger } from 'utils/formatters';
|
||||||
import './CurrencySwap.scss';
|
import './CurrencySwap.scss';
|
||||||
import { Dropdown } from 'components/ui';
|
import { Dropdown } from 'components/ui';
|
||||||
|
import Spinner from 'components/ui/Spinner';
|
||||||
|
|
||||||
export interface StateProps {
|
export interface StateProps {
|
||||||
bityRates: any;
|
bityRates: any;
|
||||||
|
@ -36,6 +37,8 @@ export interface ActionProps {
|
||||||
interface State {
|
interface State {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
showedMinMaxError: boolean;
|
showedMinMaxError: boolean;
|
||||||
|
originErr: string;
|
||||||
|
destinationErr: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CurrencySwap extends Component<
|
export default class CurrencySwap extends Component<
|
||||||
|
@ -44,9 +47,31 @@ export default class CurrencySwap extends Component<
|
||||||
> {
|
> {
|
||||||
public state = {
|
public state = {
|
||||||
disabled: true,
|
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) => {
|
public isMinMaxValid = (amount, kind) => {
|
||||||
let bityMin;
|
let bityMin;
|
||||||
let bityMax;
|
let bityMax;
|
||||||
|
@ -70,38 +95,86 @@ export default class CurrencySwap extends Component<
|
||||||
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
|
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
|
||||||
};
|
};
|
||||||
|
|
||||||
public setDisabled(originAmount, originKind, destinationAmount) {
|
public setDisabled(
|
||||||
|
originAmount,
|
||||||
|
originKind,
|
||||||
|
destinationKind,
|
||||||
|
destinationAmount
|
||||||
|
) {
|
||||||
const disabled = this.isDisabled(
|
const disabled = this.isDisabled(
|
||||||
originAmount,
|
originAmount,
|
||||||
originKind,
|
originKind,
|
||||||
destinationAmount
|
destinationAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
if (disabled && originAmount && !this.state.showedMinMaxError) {
|
if (disabled && originAmount) {
|
||||||
const { bityRates } = this.props;
|
const { bityRates } = this.props;
|
||||||
const ETHMin = generateKindMin(bityRates.BTCETH, 'ETH');
|
const ETHMin = generateKindMin(bityRates.BTCETH, 'ETH');
|
||||||
const ETHMax = generateKindMax(bityRates.BTCETH, 'ETH');
|
const ETHMax = generateKindMax(bityRates.BTCETH, 'ETH');
|
||||||
const REPMin = generateKindMin(bityRates.BTCREP, 'REP');
|
const REPMin = generateKindMin(bityRates.BTCREP, 'REP');
|
||||||
|
|
||||||
const notificationMessage = `
|
const getRates = kind => {
|
||||||
Minimum amount ${bityConfig.BTCMin} BTC,
|
let minAmount;
|
||||||
${toFixedIfLarger(ETHMin, 3)} ETH.
|
let maxAmount;
|
||||||
Max amount ${bityConfig.BTCMax} BTC,
|
switch (kind) {
|
||||||
${toFixedIfLarger(ETHMax, 3)} ETH, or
|
case 'BTC':
|
||||||
${toFixedIfLarger(REPMin, 3)} REP
|
minAmount = toFixedIfLarger(bityConfig.BTCMin, 3);
|
||||||
`;
|
maxAmount = toFixedIfLarger(bityConfig.BTCMax, 3);
|
||||||
|
break;
|
||||||
this.setState(
|
case 'ETH':
|
||||||
{
|
minAmount = toFixedIfLarger(ETHMin, 3);
|
||||||
disabled,
|
maxAmount = toFixedIfLarger(ETHMax, 3);
|
||||||
showedMinMaxError: true
|
break;
|
||||||
},
|
case 'REP':
|
||||||
() => {
|
minAmount = toFixedIfLarger(REPMin, 3);
|
||||||
this.props.showNotification('danger', notificationMessage, 10000);
|
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 {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
originErr: '',
|
||||||
|
destinationErr: '',
|
||||||
disabled
|
disabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -114,7 +187,12 @@ export default class CurrencySwap extends Component<
|
||||||
public setOriginAndDestinationToNull = () => {
|
public setOriginAndDestinationToNull = () => {
|
||||||
this.props.originAmountSwap(null);
|
this.props.originAmountSwap(null);
|
||||||
this.props.destinationAmountSwap(null);
|
this.props.destinationAmountSwap(null);
|
||||||
this.setDisabled(null, this.props.originKind, null);
|
this.setDisabled(
|
||||||
|
null,
|
||||||
|
this.props.originKind,
|
||||||
|
this.props.destinationKind,
|
||||||
|
null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onChangeOriginAmount = (
|
public onChangeOriginAmount = (
|
||||||
|
@ -129,7 +207,12 @@ export default class CurrencySwap extends Component<
|
||||||
this.props.originAmountSwap(originAmountAsNumber);
|
this.props.originAmountSwap(originAmountAsNumber);
|
||||||
const destinationAmount = originAmountAsNumber * bityRate;
|
const destinationAmount = originAmountAsNumber * bityRate;
|
||||||
this.props.destinationAmountSwap(destinationAmount);
|
this.props.destinationAmountSwap(destinationAmount);
|
||||||
this.setDisabled(originAmountAsNumber, originKind, destinationAmount);
|
this.setDisabled(
|
||||||
|
originAmountAsNumber,
|
||||||
|
originKind,
|
||||||
|
destinationKind,
|
||||||
|
destinationAmount
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.setOriginAndDestinationToNull();
|
this.setOriginAndDestinationToNull();
|
||||||
}
|
}
|
||||||
|
@ -147,7 +230,12 @@ export default class CurrencySwap extends Component<
|
||||||
const bityRate = this.props.bityRates[pairNameReversed];
|
const bityRate = this.props.bityRates[pairNameReversed];
|
||||||
const originAmount = destinationAmountAsNumber * bityRate;
|
const originAmount = destinationAmountAsNumber * bityRate;
|
||||||
this.props.originAmountSwap(originAmount);
|
this.props.originAmountSwap(originAmount);
|
||||||
this.setDisabled(originAmount, originKind, destinationAmountAsNumber);
|
this.setDisabled(
|
||||||
|
originAmount,
|
||||||
|
originKind,
|
||||||
|
destinationKind,
|
||||||
|
destinationAmountAsNumber
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.setOriginAndDestinationToNull();
|
this.setOriginAndDestinationToNull();
|
||||||
}
|
}
|
||||||
|
@ -160,69 +248,90 @@ export default class CurrencySwap extends Component<
|
||||||
originKind,
|
originKind,
|
||||||
destinationKind,
|
destinationKind,
|
||||||
destinationKindOptions,
|
destinationKindOptions,
|
||||||
originKindOptions
|
originKindOptions,
|
||||||
|
bityRates
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { originErr, destinationErr } = this.state;
|
||||||
|
|
||||||
const OriginKindDropDown = Dropdown as new () => Dropdown<
|
const OriginKindDropDown = Dropdown as new () => Dropdown<
|
||||||
typeof originKind
|
typeof originKind
|
||||||
>;
|
>;
|
||||||
const DestinationKindDropDown = Dropdown as new () => Dropdown<
|
const DestinationKindDropDown = Dropdown as new () => Dropdown<
|
||||||
typeof destinationKind
|
typeof destinationKind
|
||||||
>;
|
>;
|
||||||
|
const pairName = combineAndUpper(originKind, destinationKind);
|
||||||
|
const bityLoaded = bityRates[pairName];
|
||||||
return (
|
return (
|
||||||
<article className="CurrencySwap">
|
<article className="CurrencySwap">
|
||||||
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
|
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
|
||||||
|
{bityLoaded ? (
|
||||||
<div className="form-inline">
|
<div className="form-inline CurrencySwap-inner-wrap">
|
||||||
<input
|
<div className="CurrencySwap-input-group">
|
||||||
className={`CurrencySwap-input form-control ${String(
|
<span className="CurrencySwap-error-message">{originErr}</span>
|
||||||
originAmount
|
<input
|
||||||
) !== '' && this.isMinMaxValid(originAmount, originKind)
|
className={`CurrencySwap-input form-control ${
|
||||||
? 'is-valid'
|
String(originAmount) !== '' &&
|
||||||
: 'is-invalid'}`}
|
this.isMinMaxValid(originAmount, originKind)
|
||||||
type="number"
|
? 'is-valid'
|
||||||
placeholder="Amount"
|
: 'is-invalid'
|
||||||
value={originAmount || originAmount === 0 ? originAmount : ''}
|
}`}
|
||||||
onChange={this.onChangeOriginAmount}
|
type="number"
|
||||||
/>
|
placeholder="Amount"
|
||||||
|
value={originAmount || originAmount === 0 ? originAmount : ''}
|
||||||
<OriginKindDropDown
|
onChange={this.onChangeOriginAmount}
|
||||||
ariaLabel={`change origin kind. current origin kind ${originKind}`}
|
/>
|
||||||
options={originKindOptions}
|
<div className="CurrencySwap-dropdown">
|
||||||
value={originKind}
|
<OriginKindDropDown
|
||||||
onChange={this.props.originKindSwap}
|
ariaLabel={`change origin kind. current origin kind ${
|
||||||
size="smr"
|
originKind
|
||||||
color="default"
|
}`}
|
||||||
/>
|
options={originKindOptions}
|
||||||
|
value={originKind}
|
||||||
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
|
onChange={this.props.originKindSwap}
|
||||||
|
size="smr"
|
||||||
<input
|
color="default"
|
||||||
className={`CurrencySwap-input form-control ${String(
|
/>
|
||||||
destinationAmount
|
</div>
|
||||||
) !== '' && this.isMinMaxValid(originAmount, originKind)
|
</div>
|
||||||
? 'is-valid'
|
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
|
||||||
: 'is-invalid'}`}
|
<div className="CurrencySwap-input-group">
|
||||||
type="number"
|
<span className="CurrencySwap-error-message">
|
||||||
placeholder="Amount"
|
{destinationErr}
|
||||||
value={
|
</span>
|
||||||
destinationAmount || destinationAmount === 0
|
<input
|
||||||
? destinationAmount
|
className={`CurrencySwap-input form-control ${
|
||||||
: ''
|
String(destinationAmount) !== '' &&
|
||||||
}
|
this.isMinMaxValid(originAmount, originKind)
|
||||||
onChange={this.onChangeDestinationAmount}
|
? 'is-valid'
|
||||||
/>
|
: 'is-invalid'
|
||||||
|
}`}
|
||||||
<DestinationKindDropDown
|
type="number"
|
||||||
ariaLabel={`change destination kind. current destination kind ${destinationKind}`}
|
placeholder="Amount"
|
||||||
options={destinationKindOptions}
|
value={
|
||||||
value={destinationKind}
|
destinationAmount || destinationAmount === 0
|
||||||
onChange={this.props.destinationKindSwap}
|
? destinationAmount
|
||||||
size="smr"
|
: ''
|
||||||
color="default"
|
}
|
||||||
/>
|
onChange={this.onChangeDestinationAmount}
|
||||||
</div>
|
/>
|
||||||
|
<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">
|
<div className="CurrencySwap-submit">
|
||||||
<SimpleButton
|
<SimpleButton
|
||||||
|
|
|
@ -66,9 +66,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
||||||
{/*Amount to send*/}
|
{/*Amount to send*/}
|
||||||
{!this.isExpanded() && (
|
{!this.isExpanded() && (
|
||||||
<div className={this.computedClass()}>
|
<div className={this.computedClass()}>
|
||||||
<h3 className="SwapInfo-details-block-value">
|
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${originKind}`}</h3>
|
||||||
{` ${originAmount} ${originKind}`}
|
|
||||||
</h3>
|
|
||||||
<p className="SwapInfo-details-block-label">
|
<p className="SwapInfo-details-block-label">
|
||||||
{translate('SEND_amount')}
|
{translate('SEND_amount')}
|
||||||
</p>
|
</p>
|
||||||
|
@ -113,7 +111,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
||||||
{`${computedOriginDestinationRatio &&
|
{`${computedOriginDestinationRatio &&
|
||||||
toFixedIfLarger(
|
toFixedIfLarger(
|
||||||
computedOriginDestinationRatio
|
computedOriginDestinationRatio
|
||||||
)} ${originKind}/${destinationKind}`}
|
)} ${destinationKind}/${originKind}`}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="SwapInfo-details-block-label">
|
<p className="SwapInfo-details-block-label">
|
||||||
{translate('SWAP_your_rate')}
|
{translate('SWAP_your_rate')}
|
||||||
|
|
Loading…
Reference in New Issue