Files
frontend/src/features/payment/model/useCurrencyConversion.ts
rassadin11 9b1d6ffb5d refactor(converter): shared page layout + reusable conversion logic/UI
Pages:
- add WalletLayout route (WalletHeader + main + Footer via <Outlet/>),
  wrap converter/swap/bridge/transactions; thin pages, drop duplicated shell CSS
- extract SwapBridgeTabs shared between swap/bridge pages

Converter reuse (FSD layers, no widget->widget imports):
- move commission tiers to entities/commission (+ CommissionTable ui)
- shared calc hook features/payment/model/useCurrencyConversion;
  useConverterSection becomes thin wrapper; HomePage Converter reuses it
- move ConvertField/DirectionSwapButton to shared/ui; delete dead useConverter

Tooling:
- add eslint.config.js (ESLint 9 flat config); fix no-explicit-any in WalletPage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:39:53 +03:00

103 lines
3.5 KiB
TypeScript

import { useState } from 'react'
import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { progressPercent } from '@entities/commission'
import { GAS_PRICE, MIN_RUB_AMOUNT } from '@shared/config/constants'
import type { ConvertFieldData } from '@shared/ui'
import { usePaymentConfig } from '../hooks/usePaymentConfig'
import { usePaymentQuote } from '../hooks/usePaymentQuote'
import { usePaymentQuoteByRub } from '../hooks/usePaymentQuoteByRub'
const TOO_LARGE_ERROR = 'Сумма слишком большая и превышает 600 000 ₽'
const sanitize = (raw: string) => raw.replace(/[^0-9.]/g, '')
interface Options {
/** Значение курса USDT/RUB до загрузки конфига (пилюля). */
rateFallback?: number
}
export function useCurrencyConversion({ rateFallback = 0 }: Options = {}) {
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
const [usdtInput, setUsdtInput] = useState('1000')
const [rubInput, setRubInput] = useState(String(MIN_RUB_AMOUNT))
const { data: config } = usePaymentConfig()
const configUsdtRate = Number(config?.usdt_exchange_rate) || rateFallback
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
const isUsdtToRub = direction === 'usdt_to_rub'
const numUsdt = Number.parseFloat(usdtInput) || 0
const debouncedUsdt = useDebounce(numUsdt, 400)
const { data: quoteUsdtToRub, isError: quoteError } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
const numRubInput = Number.parseFloat(rubInput) || 0
const debouncedRub = useDebounce(numRubInput, 400)
const { data: quoteRubToUsdt, isError: quoteRubError } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
const rubBelowMin = !isUsdtToRub && numRubInput > 0 && numRubInput < MIN_RUB_AMOUNT
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
? (numUsdt > 0 ? rubTotalNum / numUsdt : 0)
: (usdtFromRubNum > 0 ? numRubInput / usdtFromRubNum : 0)
function onSwap() {
setDirection(d => (d === 'usdt_to_rub' ? 'rub_to_usdt' : 'usdt_to_rub'))
}
const convert: ConvertFieldData = isUsdtToRub
? {
value: usdtInput,
currency: 'USDT',
onChange: (raw) => setUsdtInput(sanitize(raw)),
error: quoteError ? TOO_LARGE_ERROR : undefined,
}
: {
value: rubInput,
currency: 'RUB',
onChange: (raw) => setRubInput(sanitize(raw)),
error: rubBelowMin
? `Минимальная сумма: ${MIN_RUB_AMOUNT.toLocaleString('ru-RU')}`
: quoteRubError
? TOO_LARGE_ERROR
: undefined,
}
const pay: ConvertFieldData = isUsdtToRub
? { value: rubTotal, currency: 'RUB' }
: { value: usdtFromRub, currency: 'USDT' }
return {
isUsdtToRub,
gasPriceRub,
configUsdtRate,
convert,
pay,
onSwap,
commission: {
amount: displayRubAmount,
progress: progressPercent(displayRubAmount),
commission,
effectiveRate,
},
// сырые значения для создания ордера и валидации в обёртках
numUsdt,
usdtFromRubNum,
rubTotal,
rubTotalNum,
numRubInput,
usdtFromRub,
rubBelowMin,
}
}