import { useState, useEffect, useMemo, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import { AxiosError } from 'axios' import { subscriptionApi } from '../api/subscription' import { promoApi } from '../api/promo' import type { PurchaseSelection, PeriodOption, Tariff, TariffPeriod, ClassicPurchaseOptions } from '../types' import ConnectionModal from '../components/ConnectionModal' import { useCurrency } from '../hooks/useCurrency' // Helper to extract error message from axios/api errors const getErrorMessage = (error: unknown): string => { if (error instanceof AxiosError) { const detail = error.response?.data?.detail if (typeof detail === 'string') return detail if (typeof detail === 'object' && detail?.message) return detail.message } if (error instanceof Error) return error.message return 'Произошла ошибка' } // Icons const CopyIcon = () => ( ) const CheckIcon = () => ( ) // Convert country code to flag emoji const getFlagEmoji = (countryCode: string): string => { if (!countryCode || countryCode.length !== 2) return '' const codePoints = countryCode .toUpperCase() .split('') .map((char) => 127397 + char.charCodeAt(0)) return String.fromCodePoint(...codePoints) } type PurchaseStep = 'period' | 'traffic' | 'servers' | 'devices' | 'confirm' export default function Subscription() { const { t } = useTranslation() const queryClient = useQueryClient() const location = useLocation() const { formatAmount, currencySymbol } = useCurrency() const [copied, setCopied] = useState(false) const [showConnectionModal, setShowConnectionModal] = useState(false) // Helper to format price from kopeks const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}` // Helper to apply promo discount to a price const applyPromoDiscount = (priceKopeks: number, hasExistingDiscount: boolean = false): { price: number; original: number | null; percent: number | null } => { // Only apply promo discount if no existing discount (from promo group) and we have an active promo discount if (!activeDiscount?.is_active || !activeDiscount.discount_percent || hasExistingDiscount) { return { price: priceKopeks, original: null, percent: null } } const discountedPrice = Math.round(priceKopeks * (1 - activeDiscount.discount_percent / 100)) return { price: discountedPrice, original: priceKopeks, percent: activeDiscount.discount_percent } } // Purchase state (classic mode) const [currentStep, setCurrentStep] = useState('period') const [selectedPeriod, setSelectedPeriod] = useState(null) const [selectedTraffic, setSelectedTraffic] = useState(null) const [selectedServers, setSelectedServers] = useState([]) const [selectedDevices, setSelectedDevices] = useState(1) const [showPurchaseForm, setShowPurchaseForm] = useState(false) // Tariffs mode state const [selectedTariff, setSelectedTariff] = useState(null) const [selectedTariffPeriod, setSelectedTariffPeriod] = useState(null) const [showTariffPurchase, setShowTariffPurchase] = useState(false) // Custom days/traffic state const [customDays, setCustomDays] = useState(30) const [customTrafficGb, setCustomTrafficGb] = useState(50) const [useCustomDays, setUseCustomDays] = useState(false) const [useCustomTraffic, setUseCustomTraffic] = useState(false) // Device/traffic topup state const [showDeviceTopup, setShowDeviceTopup] = useState(false) const [devicesToAdd, setDevicesToAdd] = useState(1) const [showTrafficTopup, setShowTrafficTopup] = useState(false) const [selectedTrafficPackage, setSelectedTrafficPackage] = useState(null) const { data: subscription, isLoading } = useQuery({ queryKey: ['subscription'], queryFn: subscriptionApi.getSubscription, retry: false, staleTime: 0, // Always refetch to get latest data refetchOnMount: 'always', }) const { data: purchaseOptions, isLoading: optionsLoading } = useQuery({ queryKey: ['purchase-options'], queryFn: subscriptionApi.getPurchaseOptions, }) // Fetch active promo discount const { data: activeDiscount } = useQuery({ queryKey: ['active-discount'], queryFn: promoApi.getActiveDiscount, staleTime: 30000, }) // Check if in tariffs mode (moved up to be available for useEffect) const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs' const classicOptions = !isTariffsMode ? purchaseOptions as ClassicPurchaseOptions : null const tariffs = isTariffsMode && purchaseOptions && 'tariffs' in purchaseOptions ? purchaseOptions.tariffs : [] // Determine which steps are needed const steps = useMemo(() => { const result: PurchaseStep[] = ['period'] if (selectedPeriod?.traffic.selectable && (selectedPeriod.traffic.options?.length ?? 0) > 0) { result.push('traffic') } if (selectedPeriod && (selectedPeriod.servers.options?.length ?? 0) > 0) { result.push('servers') } if (selectedPeriod && selectedPeriod.devices.max > selectedPeriod.devices.min) { result.push('devices') } result.push('confirm') return result }, [selectedPeriod]) const currentStepIndex = steps.indexOf(currentStep) const isFirstStep = currentStepIndex === 0 const isLastStep = currentStepIndex === steps.length - 1 // Initialize selection from options (classic mode only) useEffect(() => { if (classicOptions && !selectedPeriod) { const defaultPeriod = classicOptions.periods.find(p => p.id === classicOptions.selection.period_id) || classicOptions.periods[0] setSelectedPeriod(defaultPeriod) setSelectedTraffic(classicOptions.selection.traffic_value) setSelectedServers(classicOptions.selection.servers) setSelectedDevices(classicOptions.selection.devices) } }, [classicOptions, selectedPeriod]) // Build selection object const currentSelection: PurchaseSelection = useMemo(() => ({ period_id: selectedPeriod?.id, period_days: selectedPeriod?.period_days, traffic_value: selectedTraffic ?? undefined, servers: selectedServers, devices: selectedDevices, }), [selectedPeriod, selectedTraffic, selectedServers, selectedDevices]) // Preview query const { data: preview, isLoading: previewLoading } = useQuery({ queryKey: ['purchase-preview', currentSelection], queryFn: () => subscriptionApi.previewPurchase(currentSelection), enabled: !!selectedPeriod && showPurchaseForm && currentStep === 'confirm', }) const purchaseMutation = useMutation({ mutationFn: () => subscriptionApi.submitPurchase(currentSelection), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) queryClient.invalidateQueries({ queryKey: ['purchase-options'] }) setShowPurchaseForm(false) setCurrentStep('period') }, }) const autopayMutation = useMutation({ mutationFn: (enabled: boolean) => subscriptionApi.updateAutopay(enabled), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) }, }) // Devices query const { data: devicesData, isLoading: devicesLoading } = useQuery({ queryKey: ['devices'], queryFn: subscriptionApi.getDevices, enabled: !!subscription, }) // Delete device mutation const deleteDeviceMutation = useMutation({ mutationFn: (hwid: string) => subscriptionApi.deleteDevice(hwid), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['devices'] }) }, }) // Delete all devices mutation const deleteAllDevicesMutation = useMutation({ mutationFn: () => subscriptionApi.deleteAllDevices(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['devices'] }) }, }) // Pause subscription mutation const pauseMutation = useMutation({ mutationFn: () => subscriptionApi.togglePause(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) }, }) // Refs for auto-scroll const switchModalRef = useRef(null) const tariffPurchaseRef = useRef(null) const tariffsCardRef = useRef(null) // Tariff switch preview const [switchTariffId, setSwitchTariffId] = useState(null) const { data: switchPreview, isLoading: switchPreviewLoading } = useQuery({ queryKey: ['tariff-switch-preview', switchTariffId], queryFn: () => subscriptionApi.previewTariffSwitch(switchTariffId!), enabled: !!switchTariffId, }) // Tariff switch mutation const switchTariffMutation = useMutation({ mutationFn: (tariffId: number) => subscriptionApi.switchTariff(tariffId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) queryClient.invalidateQueries({ queryKey: ['purchase-options'] }) setSwitchTariffId(null) }, }) // Tariff purchase mutation const tariffPurchaseMutation = useMutation({ mutationFn: () => { if (!selectedTariff) { throw new Error('Tariff not selected') } // For daily tariffs, always use 1 day const isDailyTariff = selectedTariff.is_daily || (selectedTariff.daily_price_kopeks && selectedTariff.daily_price_kopeks > 0) const days = isDailyTariff ? 1 : (useCustomDays ? customDays : (selectedTariffPeriod?.days || 30)) const trafficGb = useCustomTraffic && selectedTariff.custom_traffic_enabled ? customTrafficGb : undefined return subscriptionApi.purchaseTariff(selectedTariff.id, days, trafficGb) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) queryClient.invalidateQueries({ queryKey: ['purchase-options'] }) setShowTariffPurchase(false) setSelectedTariff(null) setSelectedTariffPeriod(null) setUseCustomDays(false) setUseCustomTraffic(false) }, }) // Device price query const { data: devicePriceData } = useQuery({ queryKey: ['device-price', devicesToAdd], queryFn: () => subscriptionApi.getDevicePrice(devicesToAdd), enabled: showDeviceTopup && !!subscription, }) // Device purchase mutation const devicePurchaseMutation = useMutation({ mutationFn: () => subscriptionApi.purchaseDevices(devicesToAdd), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) queryClient.invalidateQueries({ queryKey: ['devices'] }) setShowDeviceTopup(false) setDevicesToAdd(1) }, }) // Traffic packages query const { data: trafficPackages } = useQuery({ queryKey: ['traffic-packages'], queryFn: subscriptionApi.getTrafficPackages, enabled: showTrafficTopup && !!subscription, }) // Traffic purchase mutation const trafficPurchaseMutation = useMutation({ mutationFn: (gb: number) => subscriptionApi.purchaseTraffic(gb), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }) setShowTrafficTopup(false) setSelectedTrafficPackage(null) }, }) // Auto-scroll to switch tariff modal when it appears useEffect(() => { if (switchTariffId && switchModalRef.current) { const timer = setTimeout(() => { switchModalRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) }, 100) return () => clearTimeout(timer) } }, [switchTariffId]) // Auto-scroll to tariff purchase form when it appears useEffect(() => { if (showTariffPurchase && tariffPurchaseRef.current) { const timer = setTimeout(() => { tariffPurchaseRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) }, 100) return () => clearTimeout(timer) } }, [showTariffPurchase]) // Auto-scroll to tariffs section when coming from Dashboard "Продлить" button useEffect(() => { const state = location.state as { scrollToExtend?: boolean } | null if (state?.scrollToExtend && tariffsCardRef.current) { const timer = setTimeout(() => { tariffsCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) }, 300) // Clear the state to prevent re-scrolling on subsequent renders window.history.replaceState({}, document.title) return () => clearTimeout(timer) } }, [location.state]) const copyUrl = () => { if (subscription?.subscription_url) { navigator.clipboard.writeText(subscription.subscription_url) setCopied(true) setTimeout(() => setCopied(false), 2000) } } const getTrafficColor = (percent: number) => { if (percent > 90) return 'bg-error-500' if (percent > 70) return 'bg-warning-500' return 'bg-success-500' } const toggleServer = (uuid: string) => { if (selectedServers.includes(uuid)) { if (selectedServers.length > 1) { setSelectedServers(selectedServers.filter(s => s !== uuid)) } } else { setSelectedServers([...selectedServers, uuid]) } } const goToNextStep = () => { const nextIndex = currentStepIndex + 1 if (nextIndex < steps.length) { setCurrentStep(steps[nextIndex]) } } const goToPrevStep = () => { const prevIndex = currentStepIndex - 1 if (prevIndex >= 0) { setCurrentStep(steps[prevIndex]) } } const resetPurchase = () => { setShowPurchaseForm(false) setCurrentStep('period') } const getStepLabel = (step: PurchaseStep) => { switch (step) { case 'period': return t('subscription.stepPeriod') case 'traffic': return t('subscription.stepTraffic') case 'servers': return t('subscription.stepServers') case 'devices': return t('subscription.stepDevices') case 'confirm': return t('subscription.stepConfirm') } } if (isLoading || optionsLoading) { return (
) } return (

{t('subscription.title')}

{/* Current Subscription */} {subscription ? (

{t('subscription.currentPlan')}

{subscription.tariff_name && (
{subscription.tariff_name}
)}
{subscription.is_trial ? t('subscription.trialStatus') : subscription.is_active ? t('subscription.active') : t('subscription.expired')}
{/* Connection Data - Top Priority */} {subscription.subscription_url && (
{t('subscription.connectionInfo')}
{/* Get Config Button */} {/* Subscription URL */}
{subscription.subscription_url}
)}
{t('subscription.daysLeft')}
{subscription.days_left > 0 ? ( `${subscription.days_left} ${t('subscription.days')}` ) : subscription.hours_left > 0 ? ( `${subscription.hours_left}${t('subscription.hours')} ${subscription.minutes_left}${t('subscription.minutes')}` ) : subscription.minutes_left > 0 ? ( `${subscription.minutes_left}${t('subscription.minutes')}` ) : ( {t('subscription.expired')} )}
{t('subscription.expiresAt')}
{new Date(subscription.end_date).toLocaleDateString()}
{t('subscription.traffic')}
{subscription.traffic_used_gb.toFixed(1)} / {subscription.traffic_limit_gb || '∞'} GB
{t('subscription.devices')}
{subscription.device_limit}
{/* Servers */} {subscription.servers && subscription.servers.length > 0 && (
{t('subscription.servers')}
{subscription.servers.map((server) => ( {server.country_code && ( {getFlagEmoji(server.country_code)} )} {server.name} ))}
)} {/* Traffic Usage */} {subscription.traffic_limit_gb > 0 && (
{t('subscription.trafficUsed')} {subscription.traffic_used_percent.toFixed(1)}%
)} {/* Purchased Traffic Packages */} {subscription.traffic_purchases && subscription.traffic_purchases.length > 0 && (
{t('subscription.purchasedTraffic')}
{subscription.traffic_purchases.map((purchase) => (
{purchase.traffic_gb} ГБ
{purchase.days_remaining === 0 ? ( Истекает сегодня ) : purchase.days_remaining === 1 ? ( Остался 1 день ) : ( Осталось {purchase.days_remaining} {purchase.days_remaining >= 5 ? 'дней' : purchase.days_remaining === 1 ? 'день' : 'дня'} )}
{t('subscription.trafficResetAt', 'Сброс')}: {new Date(purchase.expires_at).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' })}
{new Date(purchase.created_at).toLocaleDateString()} {new Date(purchase.expires_at).toLocaleDateString()}
))}
)} {/* Autopay Toggle - hide for daily tariffs */} {!subscription.is_trial && !subscription.is_daily && (
{t('subscription.autoRenewal')}
{subscription.autopay_days_before} {t('subscription.daysBeforeExpiry')}
)}
) : (
{t('subscription.noSubscription')}
)} {/* Daily Subscription Pause */} {subscription && subscription.is_daily && !subscription.is_trial && (

{t('subscription.pause.title')}

{subscription.is_daily_paused ? t('subscription.pause.paused') : t('subscription.pause.active')}
{/* Pause mutation error */} {pauseMutation.isError && (
{getErrorMessage(pauseMutation.error)}
)} {/* Paused info or Next charge progress bar */} {subscription.is_daily_paused ? (
⏸️
{t('subscription.pause.pausedInfo')}
{t('subscription.pause.pausedDescription')} {new Date(subscription.end_date).toLocaleDateString()} ({subscription.days_left} { subscription.days_left === 1 ? t('subscription.pause.days_one') : subscription.days_left < 5 ? t('subscription.pause.days_few') : t('subscription.pause.days_many') })
) : subscription.next_daily_charge_at && (() => { const now = new Date() // Backend returns UTC datetime, ensure it's parsed as UTC const nextChargeStr = subscription.next_daily_charge_at.endsWith('Z') ? subscription.next_daily_charge_at : subscription.next_daily_charge_at + 'Z' const nextCharge = new Date(nextChargeStr) const totalMs = 24 * 60 * 60 * 1000 // 24 hours in ms const remainingMs = Math.max(0, nextCharge.getTime() - now.getTime()) const elapsedMs = totalMs - remainingMs const progress = Math.min(100, (elapsedMs / totalMs) * 100) const hours = Math.floor(remainingMs / (1000 * 60 * 60)) const minutes = Math.floor((remainingMs % (1000 * 60 * 60)) / (1000 * 60)) return (
{t('subscription.pause.nextCharge')} {hours > 0 ? `${hours}${t('subscription.pause.hours')} ${minutes}${t('subscription.pause.minutes')}` : `${minutes}${t('subscription.pause.minutes')}`}
{subscription.daily_price_kopeks && (
{t('subscription.pause.willBeCharged')}: {formatPrice(subscription.daily_price_kopeks)}
)}
) })()}
)} {/* Additional Options (Buy Devices) */} {subscription && subscription.is_active && !subscription.is_trial && (

Дополнительные опции

{/* Buy Devices */} {!showDeviceTopup ? ( ) : (

Докупить устройства

{devicePriceData?.available === false ? (
{devicePriceData.reason || 'Докупка устройств недоступна'}
) : (
{devicesToAdd}
устройств
{devicePriceData && (
{devicePriceData.price_per_device_label}/устройство (пропорционально {devicePriceData.days_left} дням)
{devicePriceData.total_price_label}
)} {devicePurchaseMutation.isError && (
{getErrorMessage(devicePurchaseMutation.error)}
)}
)}
)} {/* Buy Traffic */} {subscription.traffic_limit_gb > 0 && (
{!showTrafficTopup ? ( ) : (

Докупить трафик

⚠️ Докупленный трафик добавляется к текущему лимиту и не переносится на следующий период
{!trafficPackages || trafficPackages.length === 0 ? (
Докупка трафика недоступна для вашего тарифа
) : (
{trafficPackages.map((pkg) => ( ))}
{selectedTrafficPackage && ( )} {trafficPurchaseMutation.isError && (
{getErrorMessage(trafficPurchaseMutation.error)}
)}
)}
)}
)}
)} {/* My Devices Section */} {subscription && (

{t('subscription.myDevices')}

{devicesData && devicesData.devices.length > 0 && ( )}
{devicesLoading ? (
) : devicesData && devicesData.devices.length > 0 ? (
{devicesData.total} / {devicesData.device_limit} {t('subscription.devices')}
{devicesData.devices.map((device) => (
{device.device_model || device.platform}
{device.platform}
))}
) : (
{t('subscription.noDevices')}
)}
)} {/* Tariffs Section - Combined Purchase/Extend/Switch like MiniApp */} {isTariffsMode && tariffs.length > 0 && (

{subscription?.is_daily && !subscription?.is_trial ? t('subscription.switchTariff.title') : subscription && !subscription.is_trial ? t('subscription.extend') : t('subscription.getSubscription')}

{/* Legacy subscription notice - if user has subscription without tariff */} {subscription && !subscription.is_trial && !subscription.tariff_id && (
📦 Выберите тариф для продления
Ваша текущая подписка была создана до введения тарифов. Для продления необходимо выбрать один из доступных тарифов.
⚠️ Ваша текущая подписка продолжит действовать до окончания срока.
)} {/* Switch Tariff Preview Modal */} {switchTariffId && (

{t('subscription.switchTariff.title')}

{switchPreviewLoading ? (
) : switchPreview && (() => { // Find the target tariff to get daily price info const targetTariff = tariffs.find(t => t.id === switchTariffId) const dailyPrice = targetTariff?.daily_price_kopeks ?? targetTariff?.price_per_day_kopeks ?? 0 const isDailyTariff = dailyPrice > 0 return ( <>
{t('subscription.switchTariff.currentTariff')} {switchPreview.current_tariff_name || '-'}
{t('subscription.switchTariff.newTariff')} {switchPreview.new_tariff_name}
{t('subscription.switchTariff.remainingDays')} {switchPreview.remaining_days}
{/* Daily tariff info */} {isDailyTariff && (
Оплата за день
{formatPrice(dailyPrice)}
Списывается ежедневно с баланса
)}
{t('subscription.switchTariff.upgradeCost')} {switchPreview.upgrade_cost_kopeks > 0 ? switchPreview.upgrade_cost_label : t('subscription.switchTariff.free')}
{!switchPreview.has_enough_balance && switchPreview.upgrade_cost_kopeks > 0 && (
{t('subscription.switchTariff.notEnoughBalance')}: {switchPreview.missing_amount_label}
)} ) })()}
)} {!showTariffPurchase ? ( <> {/* Promo group discount banner */} {tariffs.some(t => t.promo_group_name) && (
Ваша группа: {tariffs.find(t => t.promo_group_name)?.promo_group_name}
Персональные скидки применены к ценам
)} {/* Tariff List - current tariff first */}
{[...tariffs] // Hide Trial tariffs for users who already have trial subscription .filter((tariff) => { if (subscription?.is_trial && tariff.name.toLowerCase().includes('trial')) { return false } return true }) .sort((a, b) => { const aIsCurrent = a.is_current || a.id === subscription?.tariff_id const bIsCurrent = b.is_current || b.id === subscription?.tariff_id if (aIsCurrent && !bIsCurrent) return -1 if (!aIsCurrent && bIsCurrent) return 1 return 0 }) .map((tariff) => { const isCurrentTariff = tariff.is_current || tariff.id === subscription?.tariff_id const canSwitch = subscription && subscription.tariff_id && !isCurrentTariff && !subscription.is_trial // Если есть подписка БЕЗ tariff_id (классическая) - разрешить выбрать тариф const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id return (
{tariff.name}
{tariff.description && (
{tariff.description}
)}
{isCurrentTariff && ( {t('subscription.currentTariff')} )}
{tariff.traffic_limit_label} {tariff.device_limit} {t('subscription.devices')} {tariff.servers_count} {t('subscription.servers')}
{/* Price info - daily or period-based */}
{(() => { // Daily tariff price (is_daily + daily_price_kopeks) const dailyPrice = tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0 const originalDailyPrice = tariff.original_daily_price_kopeks || 0 const hasExistingDailyDiscount = originalDailyPrice > dailyPrice if (dailyPrice > 0) { // Apply promo discount if no existing discount const promoDaily = applyPromoDiscount(dailyPrice, hasExistingDailyDiscount) return ( {formatPrice(promoDaily.price)} {/* Show original price from promo group or promo offer */} {(hasExistingDailyDiscount || promoDaily.original) && ( {formatPrice(hasExistingDailyDiscount ? originalDailyPrice : promoDaily.original!)} )} / день {/* Show discount badge */} {(tariff.daily_discount_percent && tariff.daily_discount_percent > 0) ? ( -{tariff.daily_discount_percent}% ) : promoDaily.percent && ( -{promoDaily.percent}% )} ) } // Period-based price if (tariff.periods.length > 0) { const firstPeriod = tariff.periods[0] const hasExistingDiscount = !!(firstPeriod?.original_price_kopeks && firstPeriod.original_price_kopeks > firstPeriod.price_kopeks) // Apply promo discount if no existing discount const promoPeriod = applyPromoDiscount(firstPeriod?.price_kopeks || 0, hasExistingDiscount) return ( {t('subscription.from')} {formatPrice(promoPeriod.price)} {/* Show original price from promo group or promo offer */} {(hasExistingDiscount || promoPeriod.original) && ( {formatPrice(hasExistingDiscount ? firstPeriod.original_price_kopeks! : promoPeriod.original!)} )} {/* Show discount badge */} {hasExistingDiscount && firstPeriod.discount_percent ? ( -{firstPeriod.discount_percent}% ) : promoPeriod.percent && ( -{promoPeriod.percent}% )} ) } // Fallback return Гибкая оплата })()}
{/* Action Buttons */}
{isCurrentTariff ? ( /* Current tariff - extend button (hide for daily tariffs) */ subscription?.is_daily ? (
{t('subscription.currentTariff')}
) : ( ) ) : isLegacySubscription ? ( /* Legacy subscription without tariff - allow selecting tariff for renewal */ ) : canSwitch ? ( /* Other tariffs with existing tariff - switch button */ ) : ( /* No subscription or trial - purchase button */ )}
) })}
) : selectedTariff && ( /* Tariff Purchase Form */

{selectedTariff.name}

{/* Tariff Info */}
{t('subscription.traffic')}: {selectedTariff.traffic_limit_label}
{t('subscription.devices')}: {selectedTariff.device_limit}
{t('subscription.servers')}: {selectedTariff.servers_count}
{selectedTariff.servers.length > 0 && (
{selectedTariff.servers.map((server) => ( {server.name} ))} {selectedTariff.servers_count > selectedTariff.servers.length && ( +{selectedTariff.servers_count - selectedTariff.servers.length} )}
)}
{/* Daily Tariff Purchase */} {(selectedTariff.is_daily || (selectedTariff.daily_price_kopeks && selectedTariff.daily_price_kopeks > 0)) ? (
{t('subscription.dailyPurchase.costPerDay')}
{formatPrice(selectedTariff.daily_price_kopeks || 0)}
{t('subscription.dailyPurchase.chargedDaily')}
{t('subscription.dailyPurchase.canPause')}
{t('subscription.dailyPurchase.pausedOnLowBalance')}
{/* Purchase button for daily tariff */} {(() => { const dailyPrice = selectedTariff.daily_price_kopeks || 0 const hasEnoughBalance = purchaseOptions && dailyPrice <= purchaseOptions.balance_kopeks return (
{purchaseOptions && !hasEnoughBalance && (
{t('subscription.insufficientBalance', { missing: ((dailyPrice - purchaseOptions.balance_kopeks) / 100).toFixed(2) })}
)} {tariffPurchaseMutation.isError && (
{getErrorMessage(tariffPurchaseMutation.error)}
)}
) })()}
) : ( <> {/* Period Selection for non-daily tariffs */}
{t('subscription.selectPeriod')}
{/* Fixed periods */} {selectedTariff.periods.length > 0 && !useCustomDays && (
{selectedTariff.periods.map((period) => { const hasExistingDiscount = !!(period.original_price_kopeks && period.original_price_kopeks > period.price_kopeks) const promoPeriod = applyPromoDiscount(period.price_kopeks, hasExistingDiscount) const displayDiscount = hasExistingDiscount ? period.discount_percent : promoPeriod.percent const displayOriginal = hasExistingDiscount ? period.original_price_kopeks : promoPeriod.original const displayPrice = promoPeriod.price const displayPerMonth = hasExistingDiscount ? period.price_per_month_kopeks : Math.round(promoPeriod.price / (period.days / 30)) return ( ) })}
)} {/* Custom days option */} {selectedTariff.custom_days_enabled && (selectedTariff.price_per_day_kopeks ?? 0) > 0 && (
Произвольное кол-во дней
{useCustomDays && (
setCustomDays(parseInt(e.target.value))} className="flex-1 accent-accent-500" /> setCustomDays(Math.max(selectedTariff.min_days ?? 1, Math.min(selectedTariff.max_days ?? 365, parseInt(e.target.value) || (selectedTariff.min_days ?? 1))))} className="w-20 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 text-center" />
{(() => { const basePrice = customDays * (selectedTariff.price_per_day_kopeks ?? 0) const hasExistingDiscount = !!(selectedTariff.original_price_per_day_kopeks && selectedTariff.original_price_per_day_kopeks > (selectedTariff.price_per_day_kopeks ?? 0)) const promoCustom = applyPromoDiscount(basePrice, hasExistingDiscount) return (
{customDays} дней × {formatPrice(selectedTariff.price_per_day_kopeks ?? 0)}/день
{formatPrice(promoCustom.price)} {promoCustom.original && ( <> {formatPrice(promoCustom.original)} -{promoCustom.percent}% )}
) })()}
)}
)}
{/* Custom traffic option */} {selectedTariff.custom_traffic_enabled && (selectedTariff.traffic_price_per_gb_kopeks ?? 0) > 0 && (
Трафик
Выбрать объём трафика
{!useCustomTraffic && (
По умолчанию: {selectedTariff.traffic_limit_label}
)} {useCustomTraffic && (
setCustomTrafficGb(parseInt(e.target.value))} className="flex-1 accent-accent-500" />
setCustomTrafficGb(Math.max(selectedTariff.min_traffic_gb ?? 1, Math.min(selectedTariff.max_traffic_gb ?? 1000, parseInt(e.target.value) || (selectedTariff.min_traffic_gb ?? 1))))} className="w-20 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 text-center" /> ГБ
{customTrafficGb} ГБ × {formatPrice(selectedTariff.traffic_price_per_gb_kopeks ?? 0)}/ГБ +{formatPrice(customTrafficGb * (selectedTariff.traffic_price_per_gb_kopeks ?? 0))}
)}
)} {/* Summary & Purchase */} {(selectedTariffPeriod || useCustomDays) && (
{(() => { // Calculate prices with promo discount const basePeriodPrice = useCustomDays ? customDays * (selectedTariff.price_per_day_kopeks ?? 0) : (selectedTariffPeriod?.price_kopeks || 0) const hasExistingPeriodDiscount = !useCustomDays && selectedTariffPeriod?.original_price_kopeks ? selectedTariffPeriod.original_price_kopeks > selectedTariffPeriod.price_kopeks : false const promoPeriod = applyPromoDiscount(basePeriodPrice, hasExistingPeriodDiscount) const trafficPrice = useCustomTraffic && selectedTariff.custom_traffic_enabled ? customTrafficGb * (selectedTariff.traffic_price_per_gb_kopeks ?? 0) : 0 const totalPrice = promoPeriod.price + trafficPrice const originalTotal = promoPeriod.original ? promoPeriod.original + trafficPrice : null const hasEnoughBalance = purchaseOptions && totalPrice <= purchaseOptions.balance_kopeks return ( <> {/* Price breakdown */}
{useCustomDays ? (
Период: {customDays} дней
{formatPrice(promoPeriod.price)} {promoPeriod.original && ( {formatPrice(promoPeriod.original)} )}
) : selectedTariffPeriod && (
Период: {selectedTariffPeriod.label}
{formatPrice(promoPeriod.price)} {(hasExistingPeriodDiscount || promoPeriod.original) && ( {formatPrice(hasExistingPeriodDiscount ? selectedTariffPeriod.original_price_kopeks! : promoPeriod.original!)} )}
)} {useCustomTraffic && selectedTariff.custom_traffic_enabled && (
Трафик: {customTrafficGb} ГБ +{formatPrice(trafficPrice)}
)}
{/* Promo discount info */} {promoPeriod.percent && (
{t('promo.discountApplied')} -{promoPeriod.percent}%
)}
{t('subscription.total')}
{formatPrice(totalPrice)} {originalTotal && (
{formatPrice(originalTotal)}
)}
{purchaseOptions && !hasEnoughBalance && (
{t('subscription.insufficientBalance', { missing: ((totalPrice - purchaseOptions.balance_kopeks) / 100).toFixed(2) })}
)} ) })()} {tariffPurchaseMutation.isError && (
{getErrorMessage(tariffPurchaseMutation.error)}
)}
)} )}
)}
)} {/* Purchase/Extend Section - Classic Mode */} {classicOptions && classicOptions.periods.length > 0 && (

{subscription && !subscription.is_trial ? t('subscription.extend') : t('subscription.getSubscription')}

{!showPurchaseForm && ( )}
{showPurchaseForm && (
{/* Step Indicator */}
{t('subscription.step', { current: currentStepIndex + 1, total: steps.length })}
{steps.map((step, idx) => (
))}
{getStepLabel(currentStep)}
{/* Step: Period Selection */} {currentStep === 'period' && classicOptions && (
{classicOptions.periods.map((period) => { const hasExistingDiscount = !!(period.discount_percent && period.discount_percent > 0) const promoPeriod = applyPromoDiscount(period.price_kopeks, hasExistingDiscount) const displayDiscount = hasExistingDiscount ? period.discount_percent : promoPeriod.percent const displayOriginal = hasExistingDiscount ? period.original_price_kopeks : promoPeriod.original return ( ) })}
)} {/* Step: Traffic Selection */} {currentStep === 'traffic' && selectedPeriod?.traffic.options && (
{selectedPeriod.traffic.options.map((option) => { const hasExistingDiscount = !!(option.discount_percent && option.discount_percent > 0) const promoTraffic = applyPromoDiscount(option.price_kopeks, hasExistingDiscount) return ( ) })}
)} {/* Step: Server Selection */} {currentStep === 'servers' && selectedPeriod?.servers.options && (
{selectedPeriod.servers.options // Hide Trial server for users who already have trial subscription .filter((server) => { if (subscription?.is_trial && server.name.toLowerCase().includes('trial')) { return false } return true }) .map((server) => { const hasExistingDiscount = !!(server.discount_percent && server.discount_percent > 0) const promoServer = applyPromoDiscount(server.price_kopeks, hasExistingDiscount) return ( ) })}
)} {/* Step: Device Selection */} {currentStep === 'devices' && selectedPeriod && (
{selectedDevices}
{t('subscription.devices')}
{selectedPeriod.devices.min} {t('subscription.devicesFree')}
{selectedPeriod.devices.max > selectedPeriod.devices.min && (
{formatPrice(selectedPeriod.devices.price_per_device_kopeks)} {t('subscription.perExtraDevice')}
)}
)} {/* Step: Confirm */} {currentStep === 'confirm' && (
{previewLoading ? (
) : preview ? (
{/* Active promo discount banner */} {activeDiscount?.is_active && activeDiscount.discount_percent && !preview.original_price_kopeks && (
{t('promo.discountApplied')} -{activeDiscount.discount_percent}%
)} {preview.breakdown.map((item, idx) => (
{item.label} {item.value}
))} {(() => { // Apply promo discount if not already applied by server const hasServerDiscount = !!preview.original_price_kopeks const promoTotal = applyPromoDiscount(preview.total_price_kopeks, hasServerDiscount) const displayTotal = promoTotal.price const displayOriginal = hasServerDiscount ? preview.original_price_kopeks : promoTotal.original return (
{t('subscription.total')}
{formatPrice(displayTotal)}
{displayOriginal && (
{formatPrice(displayOriginal)}
)}
) })()} {preview.discount_label && (
{preview.discount_label}
)} {!preview.can_purchase && preview.status_message && (
{preview.status_message}
)}
) : null}
)} {/* Navigation Buttons */}
{!isFirstStep && ( )} {isFirstStep && ( )} {!isLastStep ? ( ) : ( )}
{purchaseMutation.isError && (
{getErrorMessage(purchaseMutation.error)}
)}
)}
)} {/* Connection Modal */} {showConnectionModal && ( setShowConnectionModal(false)} /> )}
) }