MyCrypto/common/containers/Tabs/Swap/components/CurrencySwap.tsx

401 lines
13 KiB
TypeScript

import { TChangeStepSwap, TInitSwap, TChangeSwapProvider, ProviderName } from 'actions/swap';
import {
NormalizedBityRates,
NormalizedShapeshiftRates,
NormalizedOptions,
SwapInput
} from 'reducers/swap/types';
import SimpleButton from 'components/ui/SimpleButton';
import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from 'config/bity';
import React, { Component } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import { SwapDropdown } from 'components/ui';
import Spinner from 'components/ui/Spinner';
import { merge, reject, debounce } from 'lodash';
import './CurrencySwap.scss';
export interface StateProps {
bityRates: NormalizedBityRates;
shapeshiftRates: NormalizedShapeshiftRates;
provider: ProviderName;
options: NormalizedOptions;
}
export interface ActionProps {
changeStepSwap: TChangeStepSwap;
initSwap: TInitSwap;
swapProvider: TChangeSwapProvider;
}
interface State {
disabled: boolean;
origin: SwapInput;
destination: SwapInput;
originKindOptions: any[];
destinationKindOptions: any[];
originErr: string;
destinationErr: string;
timeout: boolean;
}
type Props = StateProps & ActionProps;
export default class CurrencySwap extends Component<Props, State> {
public state = {
disabled: true,
origin: {
id: 'BTC',
status: 'available',
image: 'https://shapeshift.io/images/coins/bitcoin.png',
amount: NaN
} as SwapInput,
destination: {
id: 'ETH',
status: 'available',
image: 'https://shapeshift.io/images/coins/ether.png',
amount: NaN
} as SwapInput,
originKindOptions: [],
destinationKindOptions: [],
originErr: '',
destinationErr: '',
timeout: false
};
public debouncedCreateErrString = debounce((origin, destination, showError) => {
const createErrString = (
originKind: WhitelistedCoins,
amount: number,
destKind: WhitelistedCoins
) => {
const rate = this.getMinMax(originKind, destKind);
let errString;
if (amount > rate.max) {
errString = `Maximum ${rate.max} ${originKind}`;
} else {
errString = `Minimum ${rate.min} ${originKind}`;
}
return errString;
};
const originErr = showError ? createErrString(origin.id, origin.amount, destination.id) : '';
const destinationErr = showError
? createErrString(destination.id, destination.amount, origin.id)
: '';
this.setErrorMessages(originErr, destinationErr);
}, 1000);
public componentDidMount() {
setTimeout(() => {
this.setState({
timeout: true
});
}, 10000);
const { origin } = this.state;
const { options } = this.props;
if (options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
);
this.setState({
originKindOptions,
destinationKindOptions
});
}
}
public componentDidUpdate(prevProps: Props, prevState: State) {
const { origin, destination } = this.state;
const { options, bityRates, shapeshiftRates } = this.props;
if (origin !== prevState.origin) {
this.setDisabled(origin, destination);
}
const originCap = origin.id.toUpperCase();
const destCap = destination.id.toUpperCase();
const { provider } = this.props;
const ensureCorrectProvider =
(originCap === 'BTC' && destCap === 'ETH') || (destCap === 'BTC' && originCap === 'ETH');
const ensureBityRatesLoaded =
bityRates.allIds.includes('ETHBTC') && bityRates.allIds.includes('BTCETH');
const ensureShapeshiftRatesLoaded = shapeshiftRates.allIds.length > 0;
if (ensureBityRatesLoaded && ensureCorrectProvider) {
if (provider === 'shapeshift') {
this.props.swapProvider('bity');
}
} else if (ensureShapeshiftRatesLoaded) {
if (provider !== 'shapeshift') {
this.props.swapProvider('shapeshift');
}
}
if (options.allIds !== prevProps.options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
);
this.setState({
originKindOptions,
destinationKindOptions
});
}
}
public rateMixer = () => {
const { shapeshiftRates, bityRates } = this.props;
return merge(shapeshiftRates, bityRates);
};
public getMinMax = (originKind: WhitelistedCoins, destinationKind) => {
let min;
let max;
const { provider, bityRates } = this.props;
if (provider === 'bity' && bityRates.allIds.length > 2) {
if (originKind !== 'BTC') {
const pairRate = this.rateMixer().byId['BTC' + originKind].rate;
min = generateKindMin(pairRate, originKind);
max = generateKindMax(pairRate, originKind);
} else {
min = bityConfig.BTCMin;
max = bityConfig.BTCMax;
}
} else {
const pair = (this.rateMixer() as NormalizedShapeshiftRates).byId[
originKind + destinationKind
];
min = pair.min;
max = pair.limit;
}
return { min, max };
};
public isMinMaxValid = (originAmount: number, originKind: WhitelistedCoins, destinationKind) => {
const rate = this.getMinMax(originKind, destinationKind);
const higherThanMin = originAmount >= rate.min;
const lowerThanMax = originAmount <= rate.max;
return higherThanMin && lowerThanMax;
};
public setDisabled(origin: SwapInput, destination: SwapInput) {
this.clearErrMessages();
const amountsValid = origin.amount && destination.amount;
const minMaxValid = this.isMinMaxValid(origin.amount as number, origin.id, destination.id);
const disabled = !(amountsValid && minMaxValid);
const showError = disabled && amountsValid;
this.setState({
disabled
});
this.debouncedCreateErrString(origin, destination, showError);
}
public setErrorMessages = (originErr, destinationErr) => {
this.setState({
originErr,
destinationErr
});
};
public clearErrMessages = () => {
this.setState({
originErr: '',
destinationErr: ''
});
};
public onClickStartSwap = () => {
const { origin, destination } = this.state;
const { changeStepSwap, initSwap } = this.props;
initSwap({ origin, destination });
changeStepSwap(2);
};
public setOriginAndDestinationToInitialVal = () => {
this.setState({
origin: { ...this.state.origin, amount: NaN },
destination: { ...this.state.destination, amount: NaN }
});
};
public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairName = combineAndUpper(origin.id, destination.id);
const rate = this.rateMixer().byId[pairName].rate;
const destinationAmount = amount * rate;
this.setState({
origin: { ...this.state.origin, amount },
destination: { ...this.state.destination, amount: destinationAmount }
});
} else {
this.setOriginAndDestinationToInitialVal();
}
};
public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairNameReversed = combineAndUpper(destination.id, origin.id);
const rate = this.rateMixer().byId[pairNameReversed].rate;
const originAmount = amount * rate;
this.setState({
origin: { ...this.state.origin, amount: originAmount },
destination: {
...this.state.destination,
amount
}
});
} else {
this.setOriginAndDestinationToInitialVal();
}
};
public onChangeAmount = (event: React.FormEvent<HTMLInputElement>) => {
const type = event.currentTarget.id;
const { origin, destination } = this.state;
const amount = parseFloat(event.currentTarget.value);
type === 'origin-swap-input'
? this.updateOriginAmount(origin, destination, amount)
: this.updateDestinationAmount(origin, destination, amount);
};
public onChangeOriginKind = (newOption: WhitelistedCoins) => {
const { origin, destination, destinationKindOptions } = this.state;
const { options, initSwap } = this.props;
const newOrigin = { ...origin, id: newOption, amount: '' };
const newDest = {
id: newOption === destination.id ? origin.id : destination.id,
amount: ''
};
this.setState({
origin: newOrigin,
destination: newDest,
destinationKindOptions: reject(
[...destinationKindOptions, options.byId[origin.id]],
o => o.id === newOption
)
});
initSwap({ origin: newOrigin, destination: newDest });
};
public onChangeDestinationKind = (newOption: WhitelistedCoins) => {
const { initSwap } = this.props;
const { origin, destination } = this.state;
const newOrigin = {
...origin,
amount: ''
};
const newDest = { ...destination, id: newOption, amount: '' };
this.setState({
origin: newOrigin,
destination: newDest
});
initSwap({ origin: newOrigin, destination: newDest });
};
public render() {
const { bityRates, shapeshiftRates, provider } = this.props;
const {
origin,
destination,
originKindOptions,
destinationKindOptions,
originErr,
destinationErr,
timeout
} = this.state;
const OriginKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const DestinationKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const pairName = combineAndUpper(origin.id, destination.id);
const bityLoaded = bityRates.byId && bityRates.byId[pairName] ? true : false;
const shapeshiftLoaded = shapeshiftRates.byId && shapeshiftRates.byId[pairName] ? true : false;
// This ensures both are loaded
const loaded = provider === 'shapeshift' ? shapeshiftLoaded : bityLoaded && shapeshiftLoaded;
const timeoutLoaded = (bityLoaded && timeout) || (shapeshiftLoaded && timeout);
return (
<article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
{loaded || timeoutLoaded ? (
<div className="form-inline CurrencySwap-inner-wrap">
<div className="CurrencySwap-input-group">
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
<input
id="origin-swap-input"
className={`CurrencySwap-input form-control ${
String(origin.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(origin.amount as number) ? '' : origin.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${origin.id}`}
options={originKindOptions}
value={origin.id}
onChange={this.onChangeOriginKind}
/>
</div>
</div>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<div className="CurrencySwap-input-group">
{destinationErr && (
<span className="CurrencySwap-error-message">{destinationErr}</span>
)}
<input
id="destination-swap-input"
className={`CurrencySwap-input form-control ${
String(destination.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(destination.amount as number) ? '' : destination.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${destination.id}`}
options={destinationKindOptions}
value={destination.id}
onChange={this.onChangeDestinationKind}
/>
</div>
</div>
</div>
) : (
<Spinner />
)}
<div className="CurrencySwap-submit">
<SimpleButton
onClick={this.onClickStartSwap}
text={translate('SWAP_init_CTA')}
disabled={this.state.disabled}
type="primary"
/>
</div>
</article>
);
}
}