import { useState, useEffect, useMemo, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/auth'; import { displayName } from '../utils/displayName'; import { useBlockingStore } from '../store/blocking'; import { subscriptionApi } from '../api/subscription'; import { referralApi } from '../api/referral'; import { balanceApi } from '../api/balance'; import { wheelApi } from '../api/wheel'; import Onboarding, { useOnboarding } from '../components/Onboarding'; import PromoOffersSection from '../components/PromoOffersSection'; import NewsSection from '../components/news/NewsSection'; import SubscriptionCardActive from '../components/dashboard/SubscriptionCardActive'; import SubscriptionCardExpired from '../components/dashboard/SubscriptionCardExpired'; import TrialOfferCard from '../components/dashboard/TrialOfferCard'; import StatsGrid from '../components/dashboard/StatsGrid'; import { giftApi } from '../api/gift'; import { promoApi } from '../api/promo'; import PendingGiftCard from '../components/dashboard/PendingGiftCard'; import SubscriptionListCard from '../components/subscription/SubscriptionListCard'; import { API } from '../config/constants'; import { ChevronRightIcon, StarIcon } from '@/components/icons'; export default function Dashboard() { const { t } = useTranslation(); const navigate = useNavigate(); const user = useAuthStore((state) => state.user); const refreshUser = useAuthStore((state) => state.refreshUser); const queryClient = useQueryClient(); const { isCompleted: isOnboardingCompleted, complete: completeOnboarding } = useOnboarding(); const [showOnboarding, setShowOnboarding] = useState(false); const blockingType = useBlockingStore((state) => state.blockingType); const [trialError, setTrialError] = useState(null); // Refresh user data on mount useEffect(() => { refreshUser(); }, [refreshUser]); // Fetch balance from API const { data: balanceData } = useQuery({ queryKey: ['balance'], queryFn: balanceApi.getBalance, staleTime: API.BALANCE_STALE_TIME_MS, refetchOnMount: 'always', }); // Multi-tariff: check if user has multiple subscriptions const { data: multiSubData } = useQuery({ queryKey: ['subscriptions-list'], queryFn: () => subscriptionApi.getSubscriptions(), staleTime: 60_000, }); const isMultiTariff = multiSubData?.multi_tariff_enabled ?? false; const { data: subscriptionResponse, isLoading: subLoading } = useQuery({ queryKey: ['subscription'], queryFn: () => subscriptionApi.getSubscription(), retry: false, staleTime: API.BALANCE_STALE_TIME_MS, refetchOnMount: 'always', enabled: !isMultiTariff, }); const subscription = subscriptionResponse?.subscription ?? null; const { data: trialInfo, isLoading: trialLoading } = useQuery({ queryKey: ['trial-info'], queryFn: () => subscriptionApi.getTrialInfo(), enabled: !subscription && !subLoading, }); const { data: devicesData } = useQuery({ queryKey: ['devices'], queryFn: () => subscriptionApi.getDevices(), enabled: !!subscription && !isMultiTariff, staleTime: API.BALANCE_STALE_TIME_MS, }); const { data: referralInfo, isLoading: refLoading } = useQuery({ queryKey: ['referral-info'], queryFn: referralApi.getReferralInfo, }); const { data: wheelConfig } = useQuery({ queryKey: ['wheel-config'], queryFn: wheelApi.getConfig, staleTime: 60000, retry: false, }); const { data: pendingGifts } = useQuery({ queryKey: ['pending-gifts'], queryFn: giftApi.getPendingGifts, staleTime: 30_000, retry: false, }); const { data: promoGroupData } = useQuery({ queryKey: ['promo-group-discounts'], queryFn: promoApi.getGroupDiscounts, staleTime: 60_000, retry: false, }); const activateTrialMutation = useMutation({ mutationFn: () => subscriptionApi.activateTrial(), onSuccess: () => { setTrialError(null); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); queryClient.invalidateQueries({ queryKey: ['trial-info'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); }, onError: (error: { response?: { data?: { detail?: string } } }) => { setTrialError(error.response?.data?.detail || t('common.error')); }, }); // Traffic refresh state and mutation const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0); const [trafficData, setTrafficData] = useState<{ traffic_used_gb: number; traffic_used_percent: number; is_unlimited: boolean; } | null>(null); const refreshTrafficMutation = useMutation({ mutationFn: () => subscriptionApi.refreshTraffic(subscription?.id), 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_${subscription?.id ?? 'default'}`, Date.now().toString(), ); if (data.rate_limited && data.retry_after_seconds) { setTrafficRefreshCooldown(data.retry_after_seconds); } else { setTrafficRefreshCooldown(30); } queryClient.invalidateQueries({ queryKey: ['subscription', subscription?.id] }); }, 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); } }, }); // Cooldown timer 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) const hasAutoRefreshed = useRef(false); useEffect(() => { if (!subscription) return; if (hasAutoRefreshed.current) return; hasAutoRefreshed.current = true; const lastRefresh = localStorage.getItem(`traffic_refresh_ts_${subscription?.id ?? 'default'}`); const now = Date.now(); const cacheMs = API.TRAFFIC_CACHE_MS; 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]); // В multi-tariff /cabinet/subscription отключён, поэтому subscriptionResponse=undefined. // Используем список из /cabinet/subscriptions/list — пустой массив означает «нет подписок», // и тогда показываем TrialOfferCard. Без этой ветки multi-tariff юзер никогда не видел триал. const hasNoSubscription = isMultiTariff ? multiSubData !== undefined && (multiSubData.subscriptions?.length ?? 0) === 0 : subscriptionResponse?.has_subscription === false && !subLoading; // Есть ли НАСТОЯЩАЯ (платная, не триал) живая подписка — от этого зависит CTA: // «+ Купить ещё» только при наличии платной; иначе явная «Посмотреть тарифы». const hasActivePaid = (multiSubData?.subscriptions ?? []).some( (s) => !s.is_trial && (s.status === 'active' || s.status === 'limited'), ); // Show onboarding for new users after data loads useEffect(() => { if (!isOnboardingCompleted && !subLoading && !refLoading && !blockingType) { const timer = setTimeout(() => setShowOnboarding(true), 500); return () => clearTimeout(timer); } }, [isOnboardingCompleted, subLoading, refLoading, blockingType]); const onboardingSteps = useMemo(() => { type Placement = 'top' | 'bottom' | 'left' | 'right'; const steps: Array<{ target: string; title: string; description: string; placement: Placement; }> = [ { target: 'welcome', title: t('onboarding.steps.welcome.title'), description: t('onboarding.steps.welcome.description'), placement: 'bottom', }, { target: 'balance', title: t('onboarding.steps.balance.title'), description: t('onboarding.steps.balance.description'), placement: 'bottom', }, ]; if (subscription?.subscription_url) { steps.splice(1, 0, { target: 'connect-devices', title: t('onboarding.steps.connectDevices.title'), description: t('onboarding.steps.connectDevices.description'), placement: 'bottom', }); } return steps; }, [t, subscription]); const handleOnboardingComplete = () => { completeOnboarding(); setShowOnboarding(false); }; const userName = displayName(user); return (
{/* Header */}

{userName ? t('dashboard.welcome', { name: userName }) : t('dashboard.welcomeNoName')}

{t('dashboard.yourSubscription')}

{promoGroupData?.group_name && ( {promoGroupData.group_name} )}
{/* Pending Gift Activations */} {pendingGifts && pendingGifts.length > 0 && } {/* Multi-tariff: show subscription cards (max 3) — только когда подписки реально есть. Пустой случай (нет подписок) ведёт блок ниже (триал/покупка), иначе кнопка покупки дублировалась. */} {isMultiTariff && multiSubData?.subscriptions && multiSubData.subscriptions.length > 0 && (
{t('dashboard.subscriptions', 'Подписки')} {t('dashboard.manageAll', 'Управление')} →
{multiSubData.subscriptions.slice(0, 3).map((sub) => ( navigate(`/subscriptions/${sub.id}`)} /> ))} {multiSubData.subscriptions.length > 3 && ( {t('dashboard.showAll', 'Показать все')} ({multiSubData.subscriptions.length}) )} {hasActivePaid ? ( +{' '} {t('subscriptions.buyAnother', 'Купить ещё тариф')} ) : ( +{' '} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} )}
)} {/* Subscription Status Card — hidden in multi-tariff (managed via /subscriptions) */} {!isMultiTariff && (subLoading ? (
) : subscription?.is_expired || subscription?.status === 'disabled' || subscription?.is_limited ? ( ) : subscription ? ( ) : null)} {/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше при доступном триале это был единственный экран без кнопки покупки (Telegram-баг #605056/#605063). Единственная кнопка тут (вместо дубля с мульти-тариф блоком). */} {hasNoSubscription && !trialLoading && (
{trialInfo?.is_available && ( )} +{' '} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
)} {/* Promo Offers */} {/* Stats Grid */} {/* Fortune Wheel Banner */} {wheelConfig?.is_enabled && (
🎰

{t('wheel.banner.title')}

{t('wheel.banner.description')}

)} {/* News Section */} {/* Onboarding Tutorial */} {showOnboarding && ( )}
); }