import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { DEFAULT_WITHDRAW_CONFIG, QUICK_FIAT_AMOUNTS, WITHDRAW_EMAIL_PATTERN, WITHDRAW_PHONE_PATTERN, } from '@/constants' import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config' import { useAuthStore } from '@/store' import type { DepositWithdrawConfig } from '@/type' function formatNumber(locale: string, value: number) { return new Intl.NumberFormat(locale).format(value) } function getInitialWithdrawAmount( selectedExchangeRate: number, maxWithdrawAmount: number, ) { if (maxWithdrawAmount <= 0) { return 0 } return Math.min( maxWithdrawAmount, Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)), ) } function getCurrencyExchangeRate( config: DepositWithdrawConfig, currencyCode: string, fallbackRate: number, ) { const configuredRate = config.rates.find( (rate) => rate.currency === currencyCode, )?.diamondsPerFiatUnitValue return configuredRate && configuredRate > 0 ? configuredRate : fallbackRate } function toOptionalNumber(value: string) { if (value.trim().length === 0) { return null } const numericValue = Number(value) return Number.isFinite(numericValue) ? numericValue : null } function getChannelCurrencies( config: DepositWithdrawConfig, channel: DepositWithdrawConfig['withdraw']['payChannels'][number] | null, ) { if (!channel || channel.currencyCodes.length === 0) { return [] } const currencyMap = new Map( config.currencies.map((currency) => [currency.code, currency]), ) return channel.currencyCodes .map((currencyCode) => currencyMap.get(currencyCode)) .filter( (currency): currency is DepositWithdrawConfig['currencies'][number] => Boolean(currency), ) } function getNormalizedConfig( config: DepositWithdrawConfig | undefined, fallback: DepositWithdrawConfig, ) { return config ?? fallback } function isValidEmail(value: string) { if (value.trim().length === 0) { return false } return WITHDRAW_EMAIL_PATTERN.test(value.trim()) } function isValidPhone(value: string) { const normalized = value.replace(/[^\d+]/g, '') if (normalized.length === 0) { return false } return WITHDRAW_PHONE_PATTERN.test(normalized) } export function useWithdrawVm() { const { i18n, t } = useTranslation() const currentUser = useAuthStore((state) => state.currentUser) const withdrawConfigQuery = useDepositWithdrawConfig() const config = useMemo(() => { const baseConfig = getNormalizedConfig( withdrawConfigQuery.data, DEFAULT_WITHDRAW_CONFIG, ) return { ...baseConfig, currencies: baseConfig.currencies.length > 0 ? baseConfig.currencies : DEFAULT_WITHDRAW_CONFIG.currencies, payChannels: baseConfig.payChannels, withdraw: { ...baseConfig.withdraw, banks: baseConfig.withdraw.banks, payChannels: baseConfig.withdraw.payChannels, }, } }, [withdrawConfigQuery.data]) const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US' const [amount, setAmountState] = useState(0) const [hasInitializedAmount, setHasInitializedAmount] = useState(false) const [currencyCode, setCurrencyCode] = useState('') const [paymentChannelCode, setPaymentChannelCode] = useState('') const [paymentType, setPaymentType] = useState('') const [bankCode, setBankCode] = useState('') const [holderName, setHolderName] = useState('') const [bankAccount, setBankAccount] = useState('') const [receiverEmail, setReceiverEmail] = useState('') const [receiverPhone, setReceiverPhone] = useState('') const sortedPayChannels = useMemo( () => [...config.withdraw.payChannels] .filter((channel) => channel.status === 1) .sort((left, right) => left.sort - right.sort), [config.withdraw.payChannels], ) const availableBalance = Number(currentUser?.coin ?? 0) const maxWithdrawAmount = Math.max(0, Math.floor(availableBalance)) const selectedPaymentChannel = sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ?? null const availablePaymentMethods = useMemo( () => selectedPaymentChannel?.methods ?? [], [selectedPaymentChannel], ) const selectedPaymentMethod = availablePaymentMethods.find((method) => method.code === paymentType) ?? null const availableCurrencies = useMemo( () => getChannelCurrencies(config, selectedPaymentChannel), [config, selectedPaymentChannel], ) const selectedCurrency = availableCurrencies.find((item) => item.code === currencyCode) ?? null const selectedCurrencyCode = selectedCurrency?.code ?? '' const selectedRate = selectedCurrency?.withdrawCoinsPerFiatValue ?? 0 const selectedExchangeRate = selectedCurrency ? getCurrencyExchangeRate(config, selectedCurrency.code, selectedRate || 1) : 0 const convertedFiatAmount = selectedExchangeRate > 0 ? amount * selectedExchangeRate : 0 const selectedPaymentMinimumAmount = selectedPaymentMethod ? toOptionalNumber(selectedPaymentMethod.minimumAmount) : null const selectedPaymentMaximumAmount = selectedPaymentMethod ? toOptionalNumber(selectedPaymentMethod.maximumAmount) : null const amountBelowPaymentMinimum = amount > 0 && selectedPaymentMinimumAmount !== null && convertedFiatAmount < selectedPaymentMinimumAmount const amountAbovePaymentMaximum = amount > 0 && selectedPaymentMaximumAmount !== null && convertedFiatAmount > selectedPaymentMaximumAmount const sortedBanks = useMemo( () => [...(selectedPaymentChannel?.banks ?? [])] .filter( (bank) => bank.status === 1 && selectedCurrencyCode.length > 0 && (!bank.currencyCode || bank.currencyCode === selectedCurrencyCode), ) .sort((left, right) => left.sort - right.sort), [selectedPaymentChannel, selectedCurrencyCode], ) const setAmount = useCallback( (nextAmount: number) => { setAmountState( Math.min(maxWithdrawAmount, Math.max(0, Math.floor(nextAmount))), ) }, [maxWithdrawAmount], ) useEffect(() => { const firstAvailablePayChannel = sortedPayChannels[0] if (!firstAvailablePayChannel) { if (paymentChannelCode) { setPaymentChannelCode('') } return } const hasSelectedAvailablePayChannel = sortedPayChannels.some( (channel) => channel.code === paymentChannelCode, ) if (!hasSelectedAvailablePayChannel) { setPaymentChannelCode(firstAvailablePayChannel.code) } }, [paymentChannelCode, sortedPayChannels]) useEffect(() => { if (availablePaymentMethods.length === 0) { if (paymentType) { setPaymentType('') } return } const hasSelectedAvailablePaymentMethod = availablePaymentMethods.some( (method) => method.code === paymentType, ) if (!hasSelectedAvailablePaymentMethod) { setPaymentType(availablePaymentMethods[0].code) } }, [availablePaymentMethods, paymentType]) useEffect(() => { if (availableCurrencies.length === 0) { if (currencyCode) { setCurrencyCode('') } return } const hasSelectedAvailableCurrency = availableCurrencies.some( (currency) => currency.code === currencyCode, ) if (!hasSelectedAvailableCurrency) { setCurrencyCode(availableCurrencies[0].code) } }, [availableCurrencies, currencyCode]) useEffect(() => { if (sortedBanks.length === 0) { if (bankCode) { setBankCode('') } return } const hasSelectedAvailableBank = sortedBanks.some( (bank) => bank.code === bankCode, ) if (!hasSelectedAvailableBank) { setBankCode('') } }, [bankCode, sortedBanks]) useEffect(() => { if (!hasInitializedAmount && selectedExchangeRate > 0) { setAmount( getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount), ) setHasInitializedAmount(true) } }, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount]) useEffect(() => { if (amount > maxWithdrawAmount) { setAmount(maxWithdrawAmount) } }, [amount, maxWithdrawAmount, setAmount]) const quickAmounts = useMemo(() => { if (!selectedCurrency || selectedExchangeRate <= 0) { return [] } return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({ diamonds: Math.min( maxWithdrawAmount, Math.max(1, Math.round(fiatAmount / selectedExchangeRate)), ), id: `quick-${selectedCurrency.code}-${fiatAmount}`, preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`, })) }, [locale, maxWithdrawAmount, selectedCurrency, selectedExchangeRate]) const resetForm = useCallback(() => { const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? '' const nextPaymentChannel = sortedPayChannels.find( (channel) => channel.code === nextPaymentChannelCode, ) ?? null const nextCurrency = getChannelCurrencies(config, nextPaymentChannel)[0] ?? null const nextExchangeRate = nextCurrency ? getCurrencyExchangeRate( config, nextCurrency.code, nextCurrency.withdrawCoinsPerFiatValue || 1, ) : 0 setAmountState( nextExchangeRate > 0 ? getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount) : 0, ) setHasInitializedAmount(true) setCurrencyCode(nextCurrency?.code ?? '') setPaymentChannelCode(nextPaymentChannelCode) setPaymentType(nextPaymentChannel?.methods[0]?.code ?? '') setBankCode('') setHolderName('') setBankAccount('') setReceiverEmail('') setReceiverPhone('') }, [config, maxWithdrawAmount, sortedPayChannels]) const selectedCurrencyPreview = useMemo(() => { const previewCurrencyCode = selectedCurrency?.code ?? '--' return { currencyCode: selectedCurrency?.code ?? '', currencyLabel: selectedCurrency?.label ?? '', exchangeRateLabel: t('gameDesktop.withdraw.preview.exchangeRate', { currency: previewCurrencyCode, }), exchangeRateValue: selectedCurrency && selectedExchangeRate > 0 ? `1:${formatNumber(locale, selectedExchangeRate)}` : '--', convertibleLabel: t('gameDesktop.withdraw.preview.convertible', { currency: previewCurrencyCode, }), convertibleValue: selectedCurrency && selectedExchangeRate > 0 ? `${formatNumber( locale, amount * selectedExchangeRate, )} ${selectedCurrency.code}` : '--', } }, [amount, locale, selectedCurrency, selectedExchangeRate, t]) return { amount, amountAbovePaymentMaximum, amountBelowPaymentMinimum, amountExceedsBalance: amount > maxWithdrawAmount, amountRequiredError: amount <= 0, availableBalance, availableCurrencies, availablePaymentMethods, bankAccount, bankAccountError: bankAccount.trim().length === 0, bankCode, bankCodeError: bankCode.trim().length === 0, config, currencyCode, holderName, holderNameError: holderName.trim().length === 0, isLoading: withdrawConfigQuery.isLoading, isRefetching: withdrawConfigQuery.isFetching, maxWithdrawAmount, paymentChannelCode, paymentChannelCodeError: paymentChannelCode.trim().length === 0, paymentMaximumAmountLabel: selectedPaymentMaximumAmount !== null && selectedCurrency ? `${formatNumber(locale, selectedPaymentMaximumAmount)} ${ selectedCurrency.code }` : '', paymentMinimumAmountLabel: selectedPaymentMinimumAmount !== null && selectedCurrency ? `${formatNumber(locale, selectedPaymentMinimumAmount)} ${ selectedCurrency.code }` : '', paymentType, paymentTypeError: paymentType.trim().length === 0, quickAmounts, receiverEmail, receiverEmailError: !isValidEmail(receiverEmail), receiverPhone, receiverPhoneError: !isValidPhone(receiverPhone), selectedCurrency, selectedCurrencyPreview, selectedPaymentChannel, selectedPaymentMethod, selectedRate, resetForm, setAmount, setBankAccount, setBankCode, setCurrencyCode, setHolderName, setPaymentChannelCode, setPaymentType, setReceiverEmail, setReceiverPhone, sortedBanks, sortedPayChannels, withdrawCopy: { bankLabel: t('gameDesktop.withdraw.bank'), eWalletLabel: t('gameDesktop.withdraw.eWallet'), feeNote: config.withdraw.feeNote, noticeLabel: t('gameDesktop.withdraw.notice'), processingLabel: t('gameDesktop.withdraw.processingTime'), processingValue: config.withdraw.processingNote, rateHint: config.withdraw.rateHint, }, } }