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 {
|
||||
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%;
|
||||
|
|
|
@ -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
|
||||
`;
|
||||
|
||||
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(
|
||||
{
|
||||
disabled,
|
||||
showedMinMaxError: true
|
||||
},
|
||||
() => {
|
||||
this.props.showNotification('danger', notificationMessage, 10000);
|
||||
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,50 +248,64 @@ 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">
|
||||
{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)
|
||||
className={`CurrencySwap-input form-control ${
|
||||
String(originAmount) !== '' &&
|
||||
this.isMinMaxValid(originAmount, originKind)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
: '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}`}
|
||||
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)
|
||||
className={`CurrencySwap-input form-control ${
|
||||
String(destinationAmount) !== '' &&
|
||||
this.isMinMaxValid(originAmount, originKind)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
: 'is-invalid'
|
||||
}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={
|
||||
|
@ -213,9 +315,11 @@ export default class CurrencySwap extends Component<
|
|||
}
|
||||
onChange={this.onChangeDestinationAmount}
|
||||
/>
|
||||
|
||||
<div className="CurrencySwap-dropdown">
|
||||
<DestinationKindDropDown
|
||||
ariaLabel={`change destination kind. current destination kind ${destinationKind}`}
|
||||
ariaLabel={`change destination kind. current destination kind ${
|
||||
destinationKind
|
||||
}`}
|
||||
options={destinationKindOptions}
|
||||
value={destinationKind}
|
||||
onChange={this.props.destinationKindSwap}
|
||||
|
@ -223,6 +327,11 @@ export default class CurrencySwap extends Component<
|
|||
color="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
|
||||
<div className="CurrencySwap-submit">
|
||||
<SimpleButton
|
||||
|
|
|
@ -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')}
|
||||
|
|
Loading…
Reference in New Issue