import { useState, useEffect, useMemo, useRef, useCallback } 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 InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import { useCurrency } from '../hooks/useCurrency'; import { useCloseOnSuccessNotification } from '../store/successNotification'; import i18n from '../i18n'; // 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 i18n.t('common.error'); }; // Helper to extract insufficient balance error details const getInsufficientBalanceError = ( error: unknown, ): { required: number; balance: number; missingAmount?: number } | null => { if (error instanceof AxiosError) { const detail = error.response?.data?.detail; // Support both 'insufficient_balance' and 'insufficient_funds' codes if ( typeof detail === 'object' && (detail?.code === 'insufficient_balance' || detail?.code === 'insufficient_funds') ) { return { required: detail.required || detail.total_price || 0, balance: detail.balance || 0, missingAmount: detail.missing_amount || detail.missingAmount || 0, }; } } return null; }; // 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 [showDeviceReduction, setShowDeviceReduction] = useState(false); const [targetDeviceLimit, setTargetDeviceLimit] = useState(1); const [showTrafficTopup, setShowTrafficTopup] = useState(false); const [selectedTrafficPackage, setSelectedTrafficPackage] = useState(null); const [showServerManagement, setShowServerManagement] = useState(false); const [selectedServersToUpdate, setSelectedServersToUpdate] = useState([]); // Traffic refresh state const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0); const [trafficData, setTrafficData] = useState<{ traffic_used_gb: number; traffic_used_percent: number; is_unlimited: boolean; } | null>(null); const { data: subscriptionResponse, isLoading } = useQuery({ queryKey: ['subscription'], queryFn: subscriptionApi.getSubscription, retry: false, staleTime: 0, // Always refetch to get latest data refetchOnMount: 'always', }); // Extract subscription from response (null if no subscription) const subscription = subscriptionResponse?.subscription ?? null; 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); // Auto-close all modals/forms when success notification appears (e.g., subscription purchased via WebSocket) const handleCloseAllModals = useCallback(() => { setShowConnectionModal(false); setShowPurchaseForm(false); setShowTariffPurchase(false); setShowDeviceTopup(false); setShowDeviceReduction(false); setShowTrafficTopup(false); setShowServerManagement(false); setSwitchTariffId(null); setSelectedTariff(null); setSelectedTariffPeriod(null); }, []); useCloseOnSuccessNotification(handleCloseAllModals); 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); }, onError: (error: unknown) => { // Handle subscription_expired error - redirect to purchase flow if (error instanceof AxiosError) { const detail = error.response?.data?.detail; if ( typeof detail === 'object' && detail?.error_code === 'subscription_expired' && detail?.use_purchase_flow === true ) { // Find the tariff user was trying to switch to and open purchase form const targetTariff = tariffs.find((t) => t.id === switchTariffId); if (targetTariff) { setSwitchTariffId(null); setSelectedTariff(targetTariff); setSelectedTariffPeriod(targetTariff.periods[0] || null); setShowTariffPurchase(true); // Refetch purchase-options to get updated expired status queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); } } } }, }); // 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); }, }); // Device reduction info query const { data: deviceReductionInfo } = useQuery({ queryKey: ['device-reduction-info'], queryFn: subscriptionApi.getDeviceReductionInfo, enabled: showDeviceReduction && !!subscription, }); // Initialize target device limit when reduction info loads useEffect(() => { if (deviceReductionInfo && showDeviceReduction) { setTargetDeviceLimit( Math.max( deviceReductionInfo.min_device_limit, deviceReductionInfo.current_device_limit - 1, ), ); } }, [deviceReductionInfo, showDeviceReduction]); // Device reduction mutation const deviceReductionMutation = useMutation({ mutationFn: () => subscriptionApi.reduceDevices(targetDeviceLimit), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['devices'] }); queryClient.invalidateQueries({ queryKey: ['device-reduction-info'] }); setShowDeviceReduction(false); }, }); // 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); }, }); // Countries/servers query const { data: countriesData, isLoading: countriesLoading } = useQuery({ queryKey: ['countries'], queryFn: subscriptionApi.getCountries, enabled: showServerManagement && !!subscription && !subscription.is_trial, }); // Initialize selected servers when data loads useEffect(() => { if (countriesData && showServerManagement) { const connected = countriesData.countries.filter((c) => c.is_connected).map((c) => c.uuid); setSelectedServersToUpdate(connected); } }, [countriesData, showServerManagement]); // Countries update mutation const updateCountriesMutation = useMutation({ mutationFn: (countries: string[]) => subscriptionApi.updateCountries(countries), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['countries'] }); setShowServerManagement(false); }, }); // Traffic refresh mutation const refreshTrafficMutation = useMutation({ mutationFn: subscriptionApi.refreshTraffic, onSuccess: (data) => { setTrafficData({ traffic_used_gb: data.traffic_used_gb, traffic_used_percent: data.traffic_used_percent, is_unlimited: data.is_unlimited, }); localStorage.setItem('traffic_refresh_ts', Date.now().toString()); if (data.rate_limited && data.retry_after_seconds) { setTrafficRefreshCooldown(data.retry_after_seconds); } else { setTrafficRefreshCooldown(30); } queryClient.invalidateQueries({ queryKey: ['subscription'] }); }, onError: (error: { response?: { status?: number; headers?: { get?: (key: string) => string } }; }) => { if (error.response?.status === 429) { const retryAfter = error.response.headers?.get?.('Retry-After'); setTrafficRefreshCooldown(retryAfter ? parseInt(retryAfter, 10) : 30); } }, }); // Track if we've already triggered auto-refresh this session const hasAutoRefreshed = useRef(false); // Cooldown timer for traffic refresh useEffect(() => { if (trafficRefreshCooldown <= 0) return; const timer = setInterval(() => { setTrafficRefreshCooldown((prev) => Math.max(0, prev - 1)); }, 1000); return () => clearInterval(timer); }, [trafficRefreshCooldown]); // Auto-refresh traffic on mount (with 30s caching) useEffect(() => { if (!subscription) return; if (hasAutoRefreshed.current) return; hasAutoRefreshed.current = true; const lastRefresh = localStorage.getItem('traffic_refresh_ts'); const now = Date.now(); const cacheMs = 30 * 1000; if (lastRefresh && now - parseInt(lastRefresh, 10) < cacheMs) { const elapsed = now - parseInt(lastRefresh, 10); const remaining = Math.ceil((cacheMs - elapsed) / 1000); if (remaining > 0) { setTrafficRefreshCooldown(remaining); } return; } refreshTrafficMutation.mutate(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [subscription]); // 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')}
{/* Trial Period Info Banner */} {subscription.is_trial && subscription.is_active && (
{t('subscription.trialInfo.title')}
{t('subscription.trialInfo.description')}
{subscription.days_left > 0 ? t('subscription.days', { count: subscription.days_left }) : `${subscription.hours_left}${t('subscription.hours')} ${subscription.minutes_left}${t('subscription.minutes')}`} {t('subscription.trialInfo.remaining')}
{subscription.traffic_limit_gb || '∞'} GB {t('subscription.traffic')}
{subscription.device_limit} {t('subscription.devices')}
)} {/* Connection Data - Top Priority */} {subscription.subscription_url && (
{t('subscription.connectionInfo')}
{/* Get Config Button */} {/* Subscription URL - hidden when hide_subscription_link is true */} {!subscription.hide_subscription_link && (
{subscription.subscription_url}
)}
)}
{t('subscription.daysLeft')}
{subscription.days_left > 0 ? ( t('subscription.days', { count: subscription.days_left }) ) : subscription.hours_left > 0 ? ( `${t('subscription.hours', { count: subscription.hours_left })} ${t('subscription.minutes', { count: subscription.minutes_left })}` ) : subscription.minutes_left > 0 ? ( t('subscription.minutes', { count: subscription.minutes_left }) ) : ( {t('subscription.expired')} )}
{t('subscription.expiresAt')}
{new Date(subscription.end_date).toLocaleDateString()}
{t('subscription.traffic')}
{(trafficData?.traffic_used_gb ?? 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.serversLabel')}
{subscription.servers.map((server) => ( {server.country_code && ( {getFlagEmoji(server.country_code)} )} {server.name} ))}
)} {/* Traffic Usage */} {subscription.traffic_limit_gb > 0 && (
{t('subscription.trafficUsed')} {(trafficData?.traffic_used_percent ?? 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} {t('common.units.gb')}
{purchase.days_remaining === 0 ? ( {t('subscription.expired')} ) : ( {t('subscription.days', { count: purchase.days_remaining })} )}
{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')}
{t('subscription.daysBeforeExpiry', { count: subscription.autopay_days_before })}
)}
) : (
{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 && (() => { const balanceError = getInsufficientBalanceError(pauseMutation.error); if (balanceError) { const missingAmount = balanceError.required - balanceError.balance; return (
); } return (
{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()} ( {t('subscription.pause.days', { count: subscription.days_left })})
) : ( 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 && (

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

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

{t('subscription.buyDevices')}

{/* Check if completely unavailable (no subscription, price not set, etc.) */} {devicePriceData?.available === false && !devicePriceData?.max_device_limit ? (
{devicePriceData.reason || t('subscription.additionalOptions.devicesUnavailable')}
) : (
{/* Device selector - show even at max limit */}
{devicesToAdd}
{t('subscription.additionalOptions.devicesUnit')}
{/* Show limit info when at or near max */} {devicePriceData?.max_device_limit && (
{t('subscription.additionalOptions.currentDeviceLimit', { count: devicePriceData.current_device_limit || subscription.device_limit, })}{' '} /{' '} {t('subscription.additionalOptions.maxDevices', { count: devicePriceData.max_device_limit, })}
)} {/* Show reason if can't add requested amount */} {devicePriceData?.available === false && devicePriceData?.reason && (
{devicePriceData.reason}
)} {/* Price info - only when available */} {devicePriceData?.available && devicePriceData.price_per_device_label && (
{devicePriceData.price_per_device_label}/ {t('subscription.perDevice').replace('/ ', '')} ( {t('subscription.days', { count: devicePriceData.days_left })})
{devicePriceData.total_price_label}
)} {devicePriceData?.available && purchaseOptions && devicePriceData.total_price_kopeks && devicePriceData.total_price_kopeks > purchaseOptions.balance_kopeks && ( { await subscriptionApi.saveDevicesCart(devicesToAdd); }} /> )} {devicePurchaseMutation.isError && (
{getErrorMessage(devicePurchaseMutation.error)}
)}
)}
)} {/* Reduce Devices */}
{!showDeviceReduction ? ( ) : (

{t('subscription.additionalOptions.reduceDevicesTitle')}

{deviceReductionInfo?.available === false ? (
{deviceReductionInfo.reason || t('subscription.additionalOptions.reduceUnavailable')}
) : deviceReductionInfo ? (
{/* Device limit selector */}
{targetDeviceLimit}
{t('subscription.additionalOptions.devicesUnit')}
{/* Info */}
{t('subscription.additionalOptions.currentDeviceLimit', { count: deviceReductionInfo.current_device_limit, })}
{t('subscription.additionalOptions.minDeviceLimit', { count: deviceReductionInfo.min_device_limit, })}
{t('subscription.additionalOptions.connectedDevices', { count: deviceReductionInfo.connected_devices_count, })}
{/* Warning if connected devices block reduction */} {deviceReductionInfo.connected_devices_count > deviceReductionInfo.min_device_limit && (
{t('subscription.additionalOptions.disconnectDevicesFirst', { count: deviceReductionInfo.connected_devices_count, })}
)} {/* New limit preview */}
{t('subscription.additionalOptions.newDeviceLimit', { count: targetDeviceLimit, })}
{deviceReductionMutation.isError && (
{getErrorMessage(deviceReductionMutation.error)}
)}
) : (
)}
)}
{/* Buy Traffic */} {subscription.traffic_limit_gb > 0 && (
{!showTrafficTopup ? ( ) : (

{t('subscription.additionalOptions.buyTrafficTitle')}

⚠️ {t('subscription.additionalOptions.trafficWarning')}
{!trafficPackages || trafficPackages.length === 0 ? (
{t('subscription.additionalOptions.trafficUnavailable')}
) : (
{trafficPackages.map((pkg) => ( ))}
{selectedTrafficPackage && (() => { const selectedPkg = trafficPackages.find( (p) => p.gb === selectedTrafficPackage, ); const hasEnoughBalance = !selectedPkg || !purchaseOptions || selectedPkg.price_kopeks <= purchaseOptions.balance_kopeks; const missingAmount = selectedPkg && purchaseOptions ? selectedPkg.price_kopeks - purchaseOptions.balance_kopeks : 0; return ( <> {!hasEnoughBalance && missingAmount > 0 && ( { await subscriptionApi.saveTrafficCart(selectedTrafficPackage); }} /> )} ); })()} {trafficPurchaseMutation.isError && (
{getErrorMessage(trafficPurchaseMutation.error)}
)}
)}
)}
)} {/* Server Management - only in classic mode */} {!isTariffsMode && (
{!showServerManagement ? ( ) : (

{t('subscription.additionalOptions.manageServersTitle')}

{countriesLoading ? (
) : countriesData && countriesData.countries.length > 0 ? (
{t('subscription.serverManagement.statusLegend')}
{countriesData.discount_percent > 0 && (
🎁{' '} {t('subscription.serverManagement.discountBanner', { percent: countriesData.discount_percent, })}
)}
{countriesData.countries.map((country) => { const isCurrentlyConnected = country.is_connected; const isSelected = selectedServersToUpdate.includes(country.uuid); const willBeAdded = !isCurrentlyConnected && isSelected; const willBeRemoved = isCurrentlyConnected && !isSelected; return ( ); })}
{(() => { const currentConnected = countriesData.countries .filter((c) => c.is_connected) .map((c) => c.uuid); const added = selectedServersToUpdate.filter( (u) => !currentConnected.includes(u), ); const removed = currentConnected.filter( (u) => !selectedServersToUpdate.includes(u), ); const hasChanges = added.length > 0 || removed.length > 0; // Calculate cost for added servers const addedServers = countriesData.countries.filter((c) => added.includes(c.uuid), ); const totalCost = addedServers.reduce((sum, s) => sum + s.price_kopeks, 0); const hasEnoughBalance = !purchaseOptions || totalCost <= purchaseOptions.balance_kopeks; const missingAmount = purchaseOptions ? totalCost - purchaseOptions.balance_kopeks : 0; return hasChanges ? (
{added.length > 0 && (
{t('subscription.serverManagement.toAdd')} {' '} {addedServers.map((s) => s.name).join(', ')}
)} {removed.length > 0 && (
{t('subscription.serverManagement.toDisconnect')} {' '} {countriesData.countries .filter((c) => removed.includes(c.uuid)) .map((s) => s.name) .join(', ')}
)} {totalCost > 0 && (
{t('subscription.serverManagement.paymentProrated')}
{formatPrice(totalCost)}
)} {totalCost > 0 && !hasEnoughBalance && missingAmount > 0 && ( )}
) : (
{t('subscription.serverManagement.selectServersHint')}
); })()} {updateCountriesMutation.isError && (
{getErrorMessage(updateCountriesMutation.error)}
)}
) : (
{t('subscription.serverManagement.noServersAvailable')}
)}
)}
)}
)} {/* My Devices Section */} {subscription && (

{t('subscription.myDevices')}

{devicesData && devicesData.devices.length > 0 && ( )}
{devicesLoading ? (
) : devicesData && devicesData.devices.length > 0 ? (
{devicesData.total} /{' '} {t('subscription.devices', { count: devicesData.device_limit })}
{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')}

{/* Trial upgrade prompt */} {subscription?.is_trial && (
{t('subscription.trialUpgrade.title')}
{t('subscription.trialUpgrade.description')}
)} {/* Expired subscription notice - prompt to purchase new tariff */} {isTariffsMode && purchaseOptions && 'subscription_is_expired' in purchaseOptions && purchaseOptions.subscription_is_expired && (
{t('subscription.expiredBanner.title')}
{t('subscription.expiredBanner.selectTariff')}
)} {/* Legacy subscription notice - if user has subscription without tariff */} {subscription && !subscription.is_trial && !subscription.tariff_id && (
{t('subscription.legacy.selectTariffTitle')}
{t('subscription.legacy.selectTariffDescription')}
{t('subscription.legacy.currentSubContinues')}
)} {/* 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 && (
{t('subscription.switchTariff.dailyPayment')}
{formatPrice(dailyPrice)}
{t('subscription.switchTariff.dailyChargeDescription')}
)}
{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 && ( )} {/* Show error (except subscription_expired which redirects to purchase) */} {switchTariffMutation.isError && (() => { const detail = switchTariffMutation.error instanceof AxiosError ? switchTariffMutation.error.response?.data?.detail : null; // Skip displaying if it's subscription_expired (handled by redirect) if ( typeof detail === 'object' && detail?.error_code === 'subscription_expired' ) { return null; } return (
{getErrorMessage(switchTariffMutation.error)}
); })()} ); })() )}
)} {!showTariffPurchase ? ( <> {/* Promo group discount banner */} {tariffs.some((t) => t.promo_group_name) && (
{t('subscription.promoGroup.yourGroup', { name: tariffs.find((t) => t.promo_group_name)?.promo_group_name, })}
{t('subscription.promoGroup.personalDiscountsApplied')}
)} {/* 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; // Check if subscription is expired from purchaseOptions const isSubscriptionExpired = isTariffsMode && purchaseOptions && 'subscription_is_expired' in purchaseOptions && purchaseOptions.subscription_is_expired === true; // canSwitch only if subscription is active (not expired, not trial) const canSwitch = subscription && subscription.tariff_id && !isCurrentTariff && !subscription.is_trial && !isSubscriptionExpired && subscription.is_active; // Если есть подписка БЕЗ tariff_id (классическая) - разрешить выбрать тариф const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id; return (
{tariff.name}
{tariff.description && (
{tariff.description}
)}
{isCurrentTariff && ( {t('subscription.currentTariff')} )}
{/* Traffic */}
{tariff.traffic_limit_label}
{/* Devices */}
{t('subscription.devices', { count: tariff.device_limit })}
{/* Servers */}
{t('subscription.servers', { count: tariff.servers_count })}
{/* 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!, )} )} {t('subscription.tariff.perDay')} {/* 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 ( {t('subscription.tariff.flexiblePayment')} ); })()}
{/* 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} {selectedTariff.extra_devices_count > 0 && ( (+{selectedTariff.extra_devices_count}) )}
{t('subscription.serversLabel')}: {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 && ( )} {tariffPurchaseMutation.isError && !getInsufficientBalanceError(tariffPurchaseMutation.error) && (
{getErrorMessage(tariffPurchaseMutation.error)}
)} {tariffPurchaseMutation.isError && getInsufficientBalanceError(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 && (
{t('subscription.customDays.title')}
{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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-center text-dark-100" />
{(() => { 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 (
{t('subscription.days', { count: customDays })} ×{' '} {formatPrice(selectedTariff.price_per_day_kopeks ?? 0)}/ {t('subscription.customDays.perDay')}
{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 && (
{t('subscription.customTraffic.label')}
{t('subscription.customTraffic.selectVolume')}
{!useCustomTraffic && (
{t('subscription.customTraffic.default', { label: 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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-center text-dark-100" /> {t('common.units.gb')}
{customTrafficGb} {t('common.units.gb')} ×{' '} {formatPrice(selectedTariff.traffic_price_per_gb_kopeks ?? 0)}/ {t('common.units.gb')} + {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; return ( <> {/* Price breakdown */}
{useCustomDays ? (
{t('subscription.stepPeriod')}:{' '} {t('subscription.days', { count: customDays })}
{formatPrice(promoPeriod.price)} {promoPeriod.original && ( {formatPrice(promoPeriod.original)} )}
) : ( selectedTariffPeriod && ( <> {/* Если есть доп. устройства - показываем разбивку */} {(selectedTariffPeriod.extra_devices_count ?? 0) > 0 && selectedTariffPeriod.base_tariff_price_kopeks ? ( <>
{t('subscription.baseTariff')}:{' '} {selectedTariffPeriod.label} {formatPrice( selectedTariffPeriod.base_tariff_price_kopeks, )}
{t('subscription.extraDevices')} ( {selectedTariffPeriod.extra_devices_count}) + {formatPrice( selectedTariffPeriod.extra_devices_cost_kopeks ?? 0, )}
) : (
{t('subscription.summary.period', { label: selectedTariffPeriod.label, })}
{formatPrice(promoPeriod.price)} {(hasExistingPeriodDiscount || promoPeriod.original) && ( {formatPrice( hasExistingPeriodDiscount ? selectedTariffPeriod.original_price_kopeks! : promoPeriod.original!, )} )}
)} ) )} {useCustomTraffic && selectedTariff.custom_traffic_enabled && (
{t('subscription.summary.traffic', { gb: customTrafficGb })} +{formatPrice(trafficPrice)}
)}
{/* Promo discount info */} {promoPeriod.percent && (
{t('promo.discountApplied')} -{promoPeriod.percent}%
)}
{t('subscription.total')}
{formatPrice(totalPrice)} {originalTotal && (
{formatPrice(originalTotal)}
)}
); })()} {tariffPurchaseMutation.isError && !getInsufficientBalanceError(tariffPurchaseMutation.error) && (
{getErrorMessage(tariffPurchaseMutation.error)}
)} {tariffPurchaseMutation.isError && getInsufficientBalanceError(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')}
{t('subscription.devicesFree', { count: selectedPeriod.devices.min })}
{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.missing_amount_kopeks > 0 ? ( ) : preview.status_message ? (
{preview.status_message}
) : null)}
) : null}
)} {/* Navigation Buttons */}
{!isFirstStep && ( )} {isFirstStep && ( )} {!isLastStep ? ( ) : ( )}
{purchaseMutation.isError && (
{getErrorMessage(purchaseMutation.error)}
)}
)}
)} {/* Connection Modal */} {showConnectionModal && setShowConnectionModal(false)} />}
); }