19.05.2026 okkk

This commit is contained in:
2026-05-19 21:03:21 +03:00
parent fad50a1b1b
commit 29fbd71d8f
7 changed files with 301 additions and 111 deletions

View File

@@ -62,6 +62,10 @@ export function getPaymentQuote(usdtAmount: number): Promise<PaymentQuote> {
return doPaymentRequest(`/payment/quote?usdt_amount=${usdtAmount}`, {}, true) return doPaymentRequest(`/payment/quote?usdt_amount=${usdtAmount}`, {}, true)
} }
export function getPaymentQuoteByRub(rubAmount: number): Promise<PaymentQuote> {
return doPaymentRequest(`/payment/quote/rub?total_rub=${rubAmount}`, {}, true)
}
export interface CreateOrderPayload { export interface CreateOrderPayload {
usdt_amount: number usdt_amount: number
usdt_exchange_rate: number usdt_exchange_rate: number

View File

@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query'
import { getPaymentQuoteByRub } from '../api/paymentApi'
import type { PaymentQuote } from '../api/paymentApi'
export function usePaymentQuoteByRub(rubAmount: number) {
return useQuery<PaymentQuote>({
queryKey: ['payment', 'quote', 'rub', rubAmount],
queryFn: () => getPaymentQuoteByRub(rubAmount),
enabled: rubAmount > 0,
staleTime: 30_000,
retry: false,
})
}

View File

@@ -1,4 +1,5 @@
export { usePaymentConfig } from './hooks/usePaymentConfig' export { usePaymentConfig } from './hooks/usePaymentConfig'
export { usePaymentQuote } from './hooks/usePaymentQuote' export { usePaymentQuote } from './hooks/usePaymentQuote'
export { usePaymentQuoteByRub } from './hooks/usePaymentQuoteByRub'
export { useCreateOrder } from './hooks/useCreateOrder' export { useCreateOrder } from './hooks/useCreateOrder'
export type { PaymentConfig, PaymentQuote, CreateOrderPayload, OrderResult } from './api/paymentApi' export type { PaymentConfig, PaymentQuote, CreateOrderPayload, OrderResult } from './api/paymentApi'

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useConverter, progressPercent } from '@widgets/currency-converter' import { useConverter, progressPercent } from '@widgets/currency-converter'
import { useDebounce } from '@shared/lib/hooks/useDebounce' import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { usePaymentQuote, usePaymentConfig, useCreateOrder } from '@features/payment' import { usePaymentQuote, usePaymentQuoteByRub, usePaymentConfig, useCreateOrder } from '@features/payment'
import { CommissionPanel } from './CommissionPanel' import { CommissionPanel } from './CommissionPanel'
import { AgreementCheck } from './AgreementCheck' import { AgreementCheck } from './AgreementCheck'
import styles from './ConverterSection.module.css' import styles from './ConverterSection.module.css'
@@ -8,31 +9,69 @@ import { GAS_PRICE } from '@shared/config/constants'
export function ConverterSection() { export function ConverterSection() {
const c = useConverter({ usdtRate: 0 }) const c = useConverter({ usdtRate: 0 })
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
const [rubInputVal, setRubInputVal] = useState('1000')
const debouncedUsdt = useDebounce(c.numRub, 400)
const { data: quote, isError: quoteError } = usePaymentQuote(debouncedUsdt)
const { data: config } = usePaymentConfig() const { data: config } = usePaymentConfig()
const configUsdtRate = Number(config?.usdt_exchange_rate) || 0 const configUsdtRate = Number(config?.usdt_exchange_rate) || 0
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
const rubTotal = quote?.total_price ?? '' const isUsdtToRub = direction === 'usdt_to_rub'
const rubTotalNum = Number(rubTotal) || 0
const commission = Number(quote?.service_fee) || 0 const debouncedUsdt = useDebounce(c.numRub, 400)
const effectiveRate = c.numRub > 0 ? rubTotalNum / c.numRub : 0 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)
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() const { mutate: submitOrder, isPending } = useCreateOrder()
function handlePay() { function handlePay() {
submitOrder({ if (isUsdtToRub) {
usdt_amount: c.numRub, submitOrder({
usdt_exchange_rate: 1, usdt_amount: c.numRub,
gas_fee: 1, usdt_exchange_rate: 1,
total_price: Number(rubTotal) || 0, 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)
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.header}> <div className={styles.header}>
@@ -63,62 +102,120 @@ export function ConverterSection() {
</button> </button>
</div> </div>
<div className={styles.field}> {isUsdtToRub ? (
<div className={styles.fieldLabel}>Конвертируете</div> <>
<div className={styles.fieldInput}> <div className={styles.field}>
<input <div className={styles.fieldLabel}>Конвертируете</div>
type="text" <div className={styles.fieldInput}>
value={c.rubVal} <input
onChange={(e) => c.updateRub(e.target.value)} type="text"
placeholder="0" value={c.rubVal}
inputMode="decimal" onChange={(e) => c.updateRub(e.target.value)}
/> placeholder="0"
<div className={styles.currency}> inputMode="decimal"
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span> />
USDT <div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span>
USDT
</div>
</div>
{quoteError && (
<div className={styles.fieldError}>
Сумма слишком большая и превышает 600 000
</div>
)}
</div> </div>
</div>
{quoteError && (
<div className={styles.fieldError}>
Сумма слишком большая и превышает 600 000
</div>
)}
</div>
<div className={styles.swapWrap}> <div className={styles.swapWrap}>
<button <button
type="button" type="button"
className={styles.swapBtn} className={styles.swapBtn}
onClick={c.toggleMode} onClick={handleSwap}
aria-label="Поменять направление" aria-label="Поменять направление"
> >
<svg width={16} height={16} viewBox="0 0 16 16" fill="none"> <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
<path <path
d="M8 2v12M4 10l4 4 4-4" d="M8 2v12M4 10l4 4 4-4"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5} strokeWidth={1.5}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
</button> </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> <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>
{quoteRubError && (
<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> </div>
<CommissionPanel <CommissionPanel
amount={rubTotalNum} amount={displayRubAmount}
progress={progressPercent(rubTotalNum)} progress={progressPercent(displayRubAmount)}
commission={commission} commission={commission}
effectiveRate={effectiveRate} effectiveRate={effectiveRate}
/> />
@@ -132,7 +229,7 @@ export function ConverterSection() {
type="button" type="button"
className={styles.payBtn} className={styles.payBtn}
onClick={handlePay} onClick={handlePay}
disabled={!rubTotal || isPending || !c.agreed} disabled={isPayDisabled}
> >
{isPending ? 'Обработка...' : 'Оплатить'} {isPending ? 'Обработка...' : 'Оплатить'}
</button> </button>

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { GAS_PRICE, USDT_RATE } from '@shared/config/constants' import { GAS_PRICE, USDT_RATE } from '@shared/config/constants'
import { useConverter } from '../model/useConverter' import { useConverter } from '../model/useConverter'
import { progressPercent } from '../model/tiers' import { progressPercent } from '../model/tiers'
import { usePaymentConfig, usePaymentQuote } from '@features/payment' import { usePaymentConfig, usePaymentQuote, usePaymentQuoteByRub } from '@features/payment'
import { useDebounce } from '@shared/lib/hooks/useDebounce' import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { CommissionTable } from './CommissionTable' import { CommissionTable } from './CommissionTable'
import styles from './Converter.module.css' import styles from './Converter.module.css'
@@ -11,20 +12,44 @@ import { ROUTES } from '@shared/config/routes'
export function Converter() { export function Converter() {
const { data: config } = usePaymentConfig() const { data: config } = usePaymentConfig()
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
const [rubInputVal, setRubInputVal] = useState('1000')
const configUsdtRate = Number(config?.usdt_exchange_rate) || USDT_RATE const configUsdtRate = Number(config?.usdt_exchange_rate) || USDT_RATE
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
const c = useConverter({ usdtRate: configUsdtRate }) const c = useConverter({ usdtRate: configUsdtRate })
const isUsdtToRub = direction === 'usdt_to_rub'
const debouncedUsdt = useDebounce(c.numRub, 400) const debouncedUsdt = useDebounce(c.numRub, 400)
const { data: quote } = usePaymentQuote(debouncedUsdt) const { data: quoteUsdtToRub } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
const rubTotal = quote?.total_price ?? '' const numRubInput = Number.parseFloat(rubInputVal) || 0
const debouncedRub = useDebounce(numRubInput, 400)
const { data: quoteRubToUsdt } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
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 rubTotalNum = Number(rubTotal) || 0
const usdtFromRub = quoteRubToUsdt?.usdt_amount ?? ''
const usdtFromRubNum = Number(usdtFromRub) || 0
const commission = Number(quote?.service_fee) || 0 const commission = isUsdtToRub
const effectiveRate = c.numRub > 0 ? rubTotalNum / c.numRub : 0 ? 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)
return ( return (
<section className={styles.section} id="converter"> <section className={styles.section} id="converter">
@@ -57,52 +82,102 @@ export function Converter() {
</button> </button>
</div> </div>
<div className={styles.field}> {isUsdtToRub ? (
<div className={styles.fieldInput}> <>
<input <div className={styles.field}>
type="text" <div className={styles.fieldInput}>
value={c.rubVal} <input
onChange={(e) => c.updateRub(e.target.value)} type="text"
placeholder="0" value={c.rubVal}
inputMode="decimal" onChange={(e) => c.updateRub(e.target.value)}
/> placeholder="0"
<div className={styles.currency}> inputMode="decimal"
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span> USDT />
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span> USDT
</div>
</div>
</div> </div>
</div>
</div>
<div className={styles.swapWrap}> <div className={styles.swapWrap}>
<button <button
type="button" type="button"
className={styles.swapBtn} className={styles.swapBtn}
aria-label="Поменять направление" onClick={handleSwap}
> aria-label="Поменять направление"
<svg width={16} height={16} viewBox="0 0 16 16" fill="none"> >
<path <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
d="M8 2v12M4 10l4 4 4-4" <path
stroke="currentColor" d="M8 2v12M4 10l4 4 4-4"
strokeWidth={1.5} stroke="currentColor"
strokeLinecap="round" strokeWidth={1.5}
strokeLinejoin="round" strokeLinecap="round"
/> strokeLinejoin="round"
</svg> />
</button> </svg>
</div> </button>
<div className={styles.field}>
<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> <div className={styles.field}>
<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.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>
</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.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> </div>
<CommissionTable <CommissionTable
amount={rubTotalNum} amount={displayRubAmount}
progress={progressPercent(rubTotalNum)} progress={progressPercent(displayRubAmount)}
commission={commission} commission={commission}
effectiveRate={effectiveRate} effectiveRate={effectiveRate}
/> />

View File

@@ -106,7 +106,7 @@ export function buildTokensFromBalance(data: WalletBalanceData): Token[] {
const RATE = 82.2578 const RATE = 82.2578
export function useSwapForm() { export function useSwapForm() {
const [fromAmount, setFromAmountRaw] = useState('0.25') const [fromAmount, setFromAmountRaw] = useState('0')
const [fromToken, setFromToken] = useState<Token>(TOKENS.SOL) const [fromToken, setFromToken] = useState<Token>(TOKENS.SOL)
const [toToken, setToToken] = useState<Token>(TOKENS.USDC) const [toToken, setToToken] = useState<Token>(TOKENS.USDC)
const [isRefreshing, setIsRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false)

File diff suppressed because one or more lines are too long