import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { subscriptionApi } from '../../../api/subscription'; import { useTheme } from '../../../hooks/useTheme'; import { useCurrency } from '../../../hooks/useCurrency'; import { usePromoDiscount } from '../../../hooks/usePromoDiscount'; import { useCloseOnSuccessNotification } from '../../../store/successNotification'; import { getGlassColors } from '../../../utils/glassTheme'; import { getErrorMessage, type PurchaseStep } from '../../../utils/subscriptionHelpers'; import { CheckIcon } from '../../icons'; import InsufficientBalancePrompt from '../../InsufficientBalancePrompt'; import Twemoji from 'react-twemoji'; import type { ClassicPurchaseOptions, PeriodOption, PurchaseSelection, Subscription, } from '../../../types'; // ────────────────────────────────────────────────────────────────── // ClassicPurchaseWizard // // The classic-mode (non-tariff) purchase flow: a step wizard with // period → traffic (optional) → servers (when >1) → devices // (optional) → confirm. Self-owns: // - the preview query (gated to confirm step) // - the submitPurchase mutation // - all six pieces of wizard state (currentStep, selectedPeriod, // selectedTraffic, selectedServers, selectedDevices, showForm) // - the steps memo + currentSelection memo // - the init effect that seeds defaults from classicOptions.selection // - useCloseOnSuccessNotification (resets form + step on global // success notification) // // Parent only supplies the loaded options and the subscription // context. Replaces ~411 lines of inline JSX in SubscriptionPurchase. // ────────────────────────────────────────────────────────────────── export interface ClassicPurchaseWizardProps { classicOptions: ClassicPurchaseOptions; subscription: Subscription | null; subscriptionId: number | undefined; } export function ClassicPurchaseWizard({ classicOptions, subscription, subscriptionId, }: ClassicPurchaseWizardProps) { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { isDark } = useTheme(); const g = getGlassColors(isDark); const { formatAmount, currencySymbol } = useCurrency(); const { activeDiscount, applyPromoDiscount } = usePromoDiscount(); const formatPrice = (kopeks: number) => kopeks === 0 ? t('subscription.free', 'Бесплатно') : `${formatAmount(kopeks / 100)} ${currencySymbol}`; // Wizard state 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); // Global success-notification handler — resets back to the closed, // period-step state. Mirrors the parent's old handleCloseAllModals // contract for this flow. useCloseOnSuccessNotification(() => { setShowPurchaseForm(false); setCurrentStep('period'); }); const getAvailableServers = useCallback( (period: PeriodOption | null) => { if (!period?.servers.options) return []; return period.servers.options.filter((server) => { if (!server.is_available) return false; if (subscription?.is_trial && server.name.toLowerCase().includes('trial')) return false; return true; }); }, [subscription?.is_trial], ); const steps = useMemo(() => { const result: PurchaseStep[] = ['period']; if (selectedPeriod?.traffic.selectable && (selectedPeriod.traffic.options?.length ?? 0) > 0) { result.push('traffic'); } const availableServers = getAvailableServers(selectedPeriod); if (availableServers.length > 1) { result.push('servers'); } if (selectedPeriod && selectedPeriod.devices.max > selectedPeriod.devices.min) { result.push('devices'); } result.push('confirm'); return result; }, [selectedPeriod, getAvailableServers]); const currentStepIndex = steps.indexOf(currentStep); const isFirstStep = currentStepIndex === 0; const isLastStep = currentStepIndex === steps.length - 1; // Seed defaults from classicOptions.selection on first mount. useEffect(() => { if (!selectedPeriod) { const defaultPeriod = classicOptions.periods.find((p) => p.id === classicOptions.selection.period_id) || classicOptions.periods[0]; setSelectedPeriod(defaultPeriod); setSelectedTraffic(classicOptions.selection.traffic_value); const availableServers = getAvailableServers(defaultPeriod); const availableServerUuids = new Set(availableServers.map((s) => s.uuid)); if (availableServers.length === 1) { setSelectedServers([availableServers[0].uuid]); } else { setSelectedServers( classicOptions.selection.servers.filter((uuid) => availableServerUuids.has(uuid)), ); } setSelectedDevices(classicOptions.selection.devices); } }, [classicOptions, selectedPeriod, getAvailableServers]); 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], ); const { data: preview, isLoading: previewLoading } = useQuery({ queryKey: ['purchase-preview', currentSelection], queryFn: () => subscriptionApi.previewPurchase(currentSelection, subscriptionId), enabled: !!selectedPeriod && showPurchaseForm && currentStep === 'confirm', }); const purchaseMutation = useMutation({ mutationFn: () => subscriptionApi.submitPurchase(currentSelection, subscriptionId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); queryClient.invalidateQueries({ queryKey: ['purchase-options', subscriptionId] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); navigate('/subscriptions', { replace: true }); }, }); 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'); } }; return (

{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.periods.map((period) => { const promoPeriod = applyPromoDiscount( period.price_kopeks, period.original_price_kopeks, ); return ( ); })}
)} {/* Step: Traffic Selection */} {currentStep === 'traffic' && selectedPeriod?.traffic.options && (
{selectedPeriod.traffic.options.map((option) => { const promoTraffic = applyPromoDiscount( option.price_kopeks, option.original_price_kopeks, ); return ( ); })}
)} {/* Step: Server Selection */} {currentStep === 'servers' && selectedPeriod?.servers.options && (
{selectedPeriod.servers.options .filter((server) => { if (!server.is_available) return false; if (subscription?.is_trial && server.name.toLowerCase().includes('trial')) { return false; } return true; }) .map((server) => { const promoServer = applyPromoDiscount( server.price_kopeks, server.original_price_kopeks, ); 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 ? (
{activeDiscount?.is_active && activeDiscount.discount_percent && (
{t('promo.discountApplied')} -{activeDiscount.discount_percent}%
)} {preview.breakdown.map((item, idx) => (
{item.label} {item.value}
))} {(() => { const promoTotal = applyPromoDiscount( preview.total_price_kopeks, preview.original_price_kopeks, ); return (
{t('subscription.total')}
{formatPrice(promoTotal.price)}
{promoTotal.original && promoTotal.original > promoTotal.price && (
{formatPrice(promoTotal.original)}
)}
); })()} {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)}
)}
)}
); }