import { useState, useEffect, useRef, type ChangeEvent } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminPaymentMethodsApi } from '../api/adminPaymentMethods'; import { adminOverpayCertificateApi, OVERPAY_CERT_MAX_SIZE } from '../api/adminOverpayCertificate'; import { METHOD_LABELS } from '../constants/paymentMethods'; import type { PromoGroupSimple } from '../types'; import { usePlatform } from '../platform/hooks/usePlatform'; import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { localeMap } from '../utils/withdrawalUtils'; import { BackIcon, CheckIcon, SaveIcon } from '@/components/icons'; function extractErrorDetail(err: unknown): string | null { const error = err as { response?: { data?: { detail?: unknown } } }; const detail = error.response?.data?.detail; return typeof detail === 'string' ? detail : null; } function OverpayCertificateSection() { const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); const haptic = useHapticFeedback(); const confirm = useDestructiveConfirm(); const fileInputRef = useRef(null); const [certFile, setCertFile] = useState(null); const [passphrase, setPassphrase] = useState(''); const [certError, setCertError] = useState(null); const [certWarning, setCertWarning] = useState(null); const { data: certStatus, isLoading } = useQuery({ queryKey: ['admin', 'overpay-certificate'], queryFn: adminOverpayCertificateApi.getStatus, }); const uploadMutation = useMutation({ mutationFn: (payload: { file: File; passphrase: string }) => adminOverpayCertificateApi.upload(payload.file, payload.passphrase), onSuccess: (resp) => { haptic.success(); setCertError(null); setCertWarning(resp.warning ?? null); setCertFile(null); setPassphrase(''); if (fileInputRef.current) fileInputRef.current.value = ''; queryClient.invalidateQueries({ queryKey: ['admin', 'overpay-certificate'] }); }, onError: (err) => { haptic.error(); setCertWarning(null); setCertError(extractErrorDetail(err) ?? t('admin.paymentMethods.overpayCertUploadError')); }, }); const deleteMutation = useMutation({ mutationFn: adminOverpayCertificateApi.remove, onSuccess: () => { haptic.success(); setCertError(null); setCertWarning(null); queryClient.invalidateQueries({ queryKey: ['admin', 'overpay-certificate'] }); }, onError: (err) => { haptic.error(); setCertError(extractErrorDetail(err) ?? t('admin.paymentMethods.overpayCertDeleteError')); }, }); const handleFileChange = (e: ChangeEvent) => { setCertError(null); setCertWarning(null); const selected = e.target.files?.[0] ?? null; if (selected && selected.size > OVERPAY_CERT_MAX_SIZE) { setCertFile(null); e.target.value = ''; setCertError(t('admin.paymentMethods.overpayCertFileTooLarge')); return; } setCertFile(selected); }; const handleDelete = async () => { const confirmed = await confirm(t('admin.paymentMethods.overpayCertConfirmDelete')); if (confirmed) deleteMutation.mutate(); }; const expiryDate = certStatus?.not_valid_after ? new Date(certStatus.not_valid_after).toLocaleDateString(localeMap[i18n.language] || 'ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', }) : null; return (

{t('admin.paymentMethods.overpayCertTitle')}

{isLoading ? (
) : certStatus ? (
{certStatus.valid ? ( <>

{t('admin.paymentMethods.overpayCertValid', { date: expiryDate })}

{certStatus.subject && (

{certStatus.subject}

)} ) : certStatus.uploaded ? (

{t('admin.paymentMethods.overpayCertUnreadable')}

) : (

{t('admin.paymentMethods.overpayCertMissing')}

)} {certStatus.env_locked_path && (

{t('admin.paymentMethods.overpayCertEnvLockedPath')}

)} {certStatus.env_locked_passphrase && (

{t('admin.paymentMethods.overpayCertEnvLockedPassphrase')}

)}
) : null}
setPassphrase(e.target.value)} placeholder={t('admin.paymentMethods.overpayCertPassphrasePlaceholder')} className="input" />
{certError &&

{certError}

} {certWarning &&

{certWarning}

}
{certStatus?.uploaded && ( )}
); } export default function AdminPaymentMethodEdit() { const { t } = useTranslation(); const { methodId } = useParams<{ methodId: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); // Fetch payment methods const { data: methods, isLoading } = useQuery({ queryKey: ['admin-payment-methods'], queryFn: adminPaymentMethodsApi.getAll, }); // Fetch promo groups const { data: promoGroups = [] } = useQuery({ queryKey: ['admin-payment-methods-promo-groups'], queryFn: adminPaymentMethodsApi.getPromoGroups, }); const config = methods?.find((m) => m.method_id === methodId); // Local state for editing const [isEnabled, setIsEnabled] = useState(false); const [customName, setCustomName] = useState(''); const [subOptions, setSubOptions] = useState>({}); const [minAmount, setMinAmount] = useState(''); const [maxAmount, setMaxAmount] = useState(''); const [userTypeFilter, setUserTypeFilter] = useState<'all' | 'telegram' | 'email'>('all'); const [firstTopupFilter, setFirstTopupFilter] = useState<'any' | 'yes' | 'no'>('any'); const [promoGroupFilterMode, setPromoGroupFilterMode] = useState<'all' | 'selected'>('all'); const [selectedPromoGroupIds, setSelectedPromoGroupIds] = useState([]); const [openUrlDirect, setOpenUrlDirect] = useState(false); const [quickAmounts, setQuickAmounts] = useState([]); const [quickAmountInput, setQuickAmountInput] = useState(''); const [quickAmountsError, setQuickAmountsError] = useState(null); // Initialize state when config loads useEffect(() => { if (config) { setIsEnabled(config.is_enabled); setCustomName(config.display_name || ''); setSubOptions(config.sub_options || {}); setMinAmount(config.min_amount_kopeks ?? ''); setMaxAmount(config.max_amount_kopeks ?? ''); setUserTypeFilter(config.user_type_filter); setFirstTopupFilter(config.first_topup_filter); setPromoGroupFilterMode(config.promo_group_filter_mode); setSelectedPromoGroupIds(config.allowed_promo_group_ids); // ?? false — защита от stale-config (backend ещё не пришёл с миграцией) setOpenUrlDirect(config.open_url_direct ?? false); setQuickAmounts((config.quick_amounts ?? []).map((kopeks) => kopeks / 100)); } }, [config]); // Update method mutation const updateMethodMutation = useMutation({ mutationFn: (data: Record) => adminPaymentMethodsApi.update(methodId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-payment-methods'] }); navigate('/admin/payment-methods'); }, }); const handleSave = () => { if (!config) return; const data: Record = { is_enabled: isEnabled, user_type_filter: userTypeFilter, first_topup_filter: firstTopupFilter, promo_group_filter_mode: promoGroupFilterMode, allowed_promo_group_ids: promoGroupFilterMode === 'selected' ? selectedPromoGroupIds : [], open_url_direct: openUrlDirect, }; // Display name if (customName.trim()) { data.display_name = customName.trim(); } else { data.reset_display_name = true; } // Sub-options if (config.available_sub_options) { data.sub_options = subOptions; } // Amounts if (minAmount !== '') { data.min_amount_kopeks = toNumber(minAmount) || null; } else { data.reset_min_amount = true; } if (maxAmount !== '') { data.max_amount_kopeks = toNumber(maxAmount) || null; } else { data.reset_max_amount = true; } if (quickAmounts.length > 0) { data.quick_amounts = [...quickAmounts] .sort((a, b) => a - b) .map((rubles) => Math.round(rubles * 100)); } else { data.reset_quick_amounts = true; } updateMethodMutation.mutate(data); }; const togglePromoGroup = (id: number) => { setSelectedPromoGroupIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); }; const addQuickAmount = () => { setQuickAmountsError(null); const parsed = parseFloat(quickAmountInput.replace(',', '.')); const value = Math.round(parsed); if (isNaN(value) || value <= 0) { setQuickAmountsError(t('admin.paymentMethods.quickAmountsInvalid')); return; } if (quickAmounts.includes(value)) { setQuickAmountInput(''); return; } if (quickAmounts.length >= 10) { setQuickAmountsError(t('admin.paymentMethods.quickAmountsLimit')); return; } setQuickAmounts((prev) => [...prev, value].sort((a, b) => a - b)); setQuickAmountInput(''); }; const removeQuickAmount = (value: number) => { setQuickAmounts((prev) => prev.filter((amount) => amount !== value)); }; if (isLoading) { return (
); } if (!config) { return (
{/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( )}

{t('admin.paymentMethods.notFound', 'Payment method not found')}

); } const displayName = config.display_name || config.default_display_name; return (
{/* Header */}
{/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( )}

{displayName}

{METHOD_LABELS[config.method_id] || config.method_id}

{/* Form */}
{/* Enable toggle */}
{t('admin.paymentMethods.methodEnabled')}
{!config.is_provider_configured && (
{t('admin.paymentMethods.providerNotConfigured')}
)}
{/* Open URL directly toggle */}
{t('admin.paymentMethods.openUrlDirect', 'Открывать страницу оплаты сразу')}
{t( 'admin.paymentMethods.openUrlDirectHint', 'Без панели со ссылкой — провайдер открывается внутри MiniApp/вкладки сразу после клика. После оплаты юзер возвращается на /balance/result.', )}
{/* Display name */}
setCustomName(e.target.value)} placeholder={config.default_display_name} className="input" />

{t('admin.paymentMethods.displayNameHint')}: {config.default_display_name}

{/* Sub-options */} {config.available_sub_options && config.available_sub_options.length > 0 && (
{config.available_sub_options.map((opt) => { const enabled = subOptions[opt.id] !== false; return ( ); })}
)} {/* Min/Max amounts */}
{quickAmounts.length > 0 && (
{quickAmounts.map((value) => ( ))}
)}
setQuickAmountInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addQuickAmount(); } }} placeholder={t('admin.paymentMethods.quickAmountsPlaceholder')} className="input flex-1" />
{quickAmountsError &&

{quickAmountsError}

}

{t('admin.paymentMethods.quickAmountsHint', { defaults: (config.default_quick_amounts ?? []) .map((kopeks) => kopeks / 100) .join(', '), })}

{/* Display conditions */}

{t('admin.paymentMethods.conditions')}

{/* User type filter */}
{(['all', 'telegram', 'email'] as const).map((val) => ( ))}
{/* First topup filter */}
{(['any', 'yes', 'no'] as const).map((val) => ( ))}
{/* Promo groups filter */}
{(['all', 'selected'] as const).map((val) => ( ))}
{promoGroupFilterMode === 'selected' && (
{promoGroups.length === 0 ? (

{t('admin.paymentMethods.noPromoGroups')}

) : ( promoGroups.map((group) => { const selected = selectedPromoGroupIds.includes(group.id); return ( ); }) )}
)}
{config.method_id === 'overpay' && } {/* Actions */}
); }