241 lines
8.6 KiB
TypeScript
241 lines
8.6 KiB
TypeScript
import { useState } from 'react'
|
||
import { useConverter, progressPercent } from '@widgets/currency-converter'
|
||
import { useDebounce } from '@shared/lib/hooks/useDebounce'
|
||
import { usePaymentQuote, usePaymentQuoteByRub, usePaymentConfig, useCreateOrder } from '@features/payment'
|
||
import { CommissionPanel } from './CommissionPanel'
|
||
import { AgreementCheck } from './AgreementCheck'
|
||
import styles from './ConverterSection.module.css'
|
||
import { GAS_PRICE, MIN_RUB_AMOUNT } from '@shared/config/constants'
|
||
|
||
export function ConverterSection() {
|
||
const c = useConverter({ usdtRate: 0 })
|
||
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
|
||
const [rubInputVal, setRubInputVal] = useState(String(MIN_RUB_AMOUNT))
|
||
|
||
const { data: config } = usePaymentConfig()
|
||
|
||
const configUsdtRate = Number(config?.usdt_exchange_rate) || 0
|
||
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
|
||
|
||
const isUsdtToRub = direction === 'usdt_to_rub'
|
||
|
||
const debouncedUsdt = useDebounce(c.numRub, 400)
|
||
const { data: quoteUsdtToRub, isError: quoteError } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
|
||
|
||
const numRubInput = Number.parseFloat(rubInputVal) || 0
|
||
const debouncedRub = useDebounce(numRubInput, 400)
|
||
const { data: quoteRubToUsdt, isError: quoteRubError } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
|
||
|
||
const rubBelowMin = !isUsdtToRub && numRubInput > 0 && numRubInput < MIN_RUB_AMOUNT
|
||
|
||
function updateRubInput(raw: string) {
|
||
setRubInputVal(raw.replace(/[^0-9.]/g, ''))
|
||
}
|
||
|
||
function handleSwap() {
|
||
setDirection(d => d === 'usdt_to_rub' ? 'rub_to_usdt' : 'usdt_to_rub')
|
||
}
|
||
|
||
const rubTotal = quoteUsdtToRub?.total_price ?? ''
|
||
const rubTotalNum = Number(rubTotal) || 0
|
||
const usdtFromRub = quoteRubToUsdt?.usdt_amount ?? ''
|
||
const usdtFromRubNum = Number(usdtFromRub) || 0
|
||
|
||
const commission = isUsdtToRub
|
||
? Number(quoteUsdtToRub?.service_fee) || 0
|
||
: Number(quoteRubToUsdt?.service_fee) || 0
|
||
|
||
const displayRubAmount = isUsdtToRub ? rubTotalNum : numRubInput
|
||
const effectiveRate = isUsdtToRub
|
||
? (c.numRub > 0 ? rubTotalNum / c.numRub : 0)
|
||
: (usdtFromRubNum > 0 ? numRubInput / usdtFromRubNum : 0)
|
||
|
||
const { mutate: submitOrder, isPending } = useCreateOrder()
|
||
|
||
function handlePay() {
|
||
if (isUsdtToRub) {
|
||
submitOrder({
|
||
usdt_amount: c.numRub,
|
||
usdt_exchange_rate: 1,
|
||
gas_fee: 1,
|
||
total_price: Number(rubTotal) || 0,
|
||
})
|
||
} else {
|
||
submitOrder({
|
||
usdt_amount: usdtFromRubNum,
|
||
usdt_exchange_rate: 1,
|
||
gas_fee: 1,
|
||
total_price: numRubInput,
|
||
})
|
||
}
|
||
}
|
||
|
||
const isPayDisabled = isUsdtToRub
|
||
? (!rubTotal || isPending || !c.agreed)
|
||
: (!usdtFromRub || isPending || !c.agreed || rubBelowMin)
|
||
|
||
return (
|
||
<div className={styles.wrap}>
|
||
<div className={styles.header}>
|
||
<div>
|
||
<h1 className={styles.title}>Конвертация</h1>
|
||
<div className={styles.subtitle}>Данные обновляются в реальном времени</div>
|
||
</div>
|
||
<div className={styles.pills}>
|
||
<div className={styles.pill}>
|
||
Цена газа в RUB <span className={styles.pillValue}>{gasPriceRub.toFixed(2)} RUB</span>
|
||
</div>
|
||
<div className={styles.pill}>
|
||
USDT/RUB <span className={styles.pillValue}>{configUsdtRate.toFixed(2)} ₽</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.body}>
|
||
<div>
|
||
<div className={styles.tabs}>
|
||
<div className={styles.tab} data-active>
|
||
КУПИТЬ
|
||
</div>
|
||
</div>
|
||
|
||
{isUsdtToRub ? (
|
||
<>
|
||
<div className={styles.field}>
|
||
<div className={styles.fieldLabel}>Конвертируете</div>
|
||
<div className={styles.fieldInput}>
|
||
<input
|
||
type="text"
|
||
value={c.rubVal}
|
||
onChange={(e) => c.updateRub(e.target.value)}
|
||
placeholder="0"
|
||
inputMode="decimal"
|
||
/>
|
||
<div className={styles.currency}>
|
||
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}>₮</span>
|
||
USDT
|
||
</div>
|
||
</div>
|
||
{quoteError && (
|
||
<div className={styles.fieldError}>
|
||
Сумма слишком большая и превышает 600 000 ₽
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className={styles.swapWrap}>
|
||
<button
|
||
type="button"
|
||
className={styles.swapBtn}
|
||
onClick={handleSwap}
|
||
aria-label="Поменять направление"
|
||
>
|
||
<svg width={16} height={16} viewBox="0 0 16 16" fill="none">
|
||
<path
|
||
d="M8 2v12M4 10l4 4 4-4"
|
||
stroke="currentColor"
|
||
strokeWidth={1.5}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className={styles.field}>
|
||
<div className={styles.fieldLabel}>Платите</div>
|
||
<div className={styles.fieldInput}>
|
||
<input type="text" value={rubTotal} readOnly placeholder="0" />
|
||
<div className={styles.currency}>
|
||
<span className={`${styles.currencyIcon} ${styles.currencyRub}`}>₽</span>
|
||
RUB
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className={styles.field}>
|
||
<div className={styles.fieldLabel}>Конвертируете</div>
|
||
<div className={styles.fieldInput}>
|
||
<input
|
||
type="text"
|
||
value={rubInputVal}
|
||
onChange={(e) => updateRubInput(e.target.value)}
|
||
placeholder="0"
|
||
inputMode="decimal"
|
||
/>
|
||
<div className={styles.currency}>
|
||
<span className={`${styles.currencyIcon} ${styles.currencyRub}`}>₽</span>
|
||
RUB
|
||
</div>
|
||
</div>
|
||
{rubBelowMin && (
|
||
<div className={styles.fieldError}>
|
||
Минимальная сумма: {MIN_RUB_AMOUNT.toLocaleString('ru-RU')} ₽
|
||
</div>
|
||
)}
|
||
{quoteRubError && !rubBelowMin && (
|
||
<div className={styles.fieldError}>
|
||
Сумма слишком большая и превышает 600 000 ₽
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className={styles.swapWrap}>
|
||
<button
|
||
type="button"
|
||
className={styles.swapBtn}
|
||
onClick={handleSwap}
|
||
aria-label="Поменять направление"
|
||
>
|
||
<svg width={16} height={16} viewBox="0 0 16 16" fill="none">
|
||
<path
|
||
d="M8 2v12M4 10l4 4 4-4"
|
||
stroke="currentColor"
|
||
strokeWidth={1.5}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className={styles.field}>
|
||
<div className={styles.fieldLabel}>Платите</div>
|
||
<div className={styles.fieldInput}>
|
||
<input type="text" value={usdtFromRub} readOnly placeholder="0" />
|
||
<div className={styles.currency}>
|
||
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}>₮</span>
|
||
USDT
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<CommissionPanel
|
||
amount={displayRubAmount}
|
||
progress={progressPercent(displayRubAmount)}
|
||
commission={commission}
|
||
effectiveRate={effectiveRate}
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.bottom}>
|
||
<AgreementCheck checked={c.agreed} onToggle={() => c.setAgreed(!c.agreed)} />
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
className={styles.payBtn}
|
||
onClick={handlePay}
|
||
disabled={isPayDisabled}
|
||
>
|
||
{isPending ? 'Обработка...' : 'Оплатить'}
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|