import { useState, useEffect, useRef, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { subscriptionApi } from '../api/subscription'; import TrafficProgressBar from '../components/dashboard/TrafficProgressBar'; import { HoverBorderGradient } from '../components/ui/hover-border-gradient'; import { getTrafficZone } from '../utils/trafficZone'; import { formatTraffic } from '../utils/formatTraffic'; import { getGlassColors } from '../utils/glassTheme'; import { useTheme } from '../hooks/useTheme'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import { useCurrency } from '../hooks/useCurrency'; import { useCloseOnSuccessNotification } from '../store/successNotification'; import PurchaseCTAButton from '../components/subscription/PurchaseCTAButton'; import { CopyIcon, CheckIcon } from '../components/icons'; import { getErrorMessage, getInsufficientBalanceError, getFlagEmoji, } from '../utils/subscriptionHelpers'; export default function Subscription() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { formatAmount, currencySymbol } = useCurrency(); const navigate = useNavigate(); const { isDark } = useTheme(); const g = getGlassColors(isDark); const [copied, setCopied] = useState(false); // Helper to format price from kopeks const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`; // 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; // Live countdown timer const [countdown, setCountdown] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); useEffect(() => { if (!subscription?.end_date) return; const endTime = new Date(subscription.end_date).getTime(); const tick = () => { const diff = Math.max(0, endTime - Date.now()); setCountdown({ days: Math.floor(diff / 86_400_000), hours: Math.floor((diff % 86_400_000) / 3_600_000), minutes: Math.floor((diff % 3_600_000) / 60_000), seconds: Math.floor((diff % 60_000) / 1_000), }); }; tick(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, [subscription?.end_date]); // Purchase options (needed for balance_kopeks in device/traffic/server management) const { data: purchaseOptions } = useQuery({ queryKey: ['purchase-options'], queryFn: subscriptionApi.getPurchaseOptions, }); const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs'; 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'] }); }, }); // Auto-close all modals/forms when success notification appears const handleCloseAllModals = useCallback(() => { setShowDeviceTopup(false); setShowDeviceReduction(false); setShowTrafficTopup(false); setShowServerManagement(false); }, []); useCloseOnSuccessNotification(handleCloseAllModals); // 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(); }, [subscription, refreshTrafficMutation]); const copyUrl = () => { if (subscription?.subscription_url) { navigator.clipboard.writeText(subscription.subscription_url); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; if (isLoading) { return (
); } return (

{t('subscription.title')}

{/* Current Subscription */} {subscription ? ( (() => { const usedPercent = trafficData?.traffic_used_percent ?? subscription.traffic_used_percent; const usedGb = trafficData?.traffic_used_gb ?? subscription.traffic_used_gb; const isUnlimited = (trafficData?.is_unlimited ?? false) || subscription.traffic_limit_gb === 0; const zone = getTrafficZone(usedPercent); const connectedDevices = devicesData?.total ?? 0; const formattedDate = new Date(subscription.end_date).toLocaleDateString(); return (
{/* Trial shimmer border */} {subscription.is_trial && (