From a89ab969530f824df7f5b1a5597f8923460582d0 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 23:52:42 +0300 Subject: [PATCH] refactor(purchase): extract TariffPurchaseForm from god page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single biggest extraction in the SubscriptionPurchase decomp. Moves the per-tariff purchase form into a self-contained component at src/components/subscription/purchase/TariffPurchaseForm.tsx. The form self-owns: - the purchaseTariff mutation (mode-aware: daily-tariff activate vs period-based + custom-days / custom-traffic toggles + summary) - the auto-scroll-into-view ref + effect on mount - selectedTariffPeriod / customDays / customTrafficGb / useCustomDays / useCustomTraffic — five pieces of state that were parent-level but only ever read inside this form. Form remounts with fresh state when the parent passes a new tariff (key={selectedTariff.id}). Parent only owns selectedTariff + showTariffPurchase (the two pieces the orchestrator needs to decide what to render) and threads them into . Tariff card click handlers drop their period-seeding line (the form seeds itself from tariff.periods[0] on mount). The subscription_expired fallback callback simplifies for the same reason. SubscriptionPurchase.tsx: 1838 → 1222 lines (-616). Drops useRef, TariffPeriod, and getInsufficientBalanceError from parent imports. --- .../purchase/TariffPurchaseForm.tsx | 641 ++++++++++++++++ src/pages/SubscriptionPurchase.tsx | 684 +----------------- 2 files changed, 665 insertions(+), 660 deletions(-) create mode 100644 src/components/subscription/purchase/TariffPurchaseForm.tsx diff --git a/src/components/subscription/purchase/TariffPurchaseForm.tsx b/src/components/subscription/purchase/TariffPurchaseForm.tsx new file mode 100644 index 0000000..720ed8d --- /dev/null +++ b/src/components/subscription/purchase/TariffPurchaseForm.tsx @@ -0,0 +1,641 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { subscriptionApi } from '../../../api/subscription'; +import { getErrorMessage, getInsufficientBalanceError } from '../../../utils/subscriptionHelpers'; +import { useCurrency } from '../../../hooks/useCurrency'; +import { usePromoDiscount } from '../../../hooks/usePromoDiscount'; +import InsufficientBalancePrompt from '../../InsufficientBalancePrompt'; +import type { Tariff, TariffPeriod } from '../../../types'; + +// ────────────────────────────────────────────────────────────────── +// TariffPurchaseForm +// +// The full per-tariff purchase form: period picker (or daily-tariff +// activate), custom-days toggle + slider, custom-traffic toggle + +// slider, summary, and the confirm CTA. Self-owns: +// - the purchaseTariff mutation +// - the auto-scroll-into-view ref + effect on mount +// - selectedTariffPeriod / customDays / customTrafficGb / +// useCustomDays / useCustomTraffic (form-internal state, reset +// by re-mount when the parent passes a new `tariff` via key=) +// +// The parent (SubscriptionPurchase) supplies the chosen tariff, +// the current balance (for inline insufficient-balance prompts), +// the subscription id (for the renew-this-subscription flow), and +// onBack to clear its own selection state. +// ────────────────────────────────────────────────────────────────── + +export interface TariffPurchaseFormProps { + tariff: Tariff; + subscriptionId: number | undefined; + balanceKopeks: number | undefined; + onBack: () => void; +} + +export function TariffPurchaseForm({ + tariff, + subscriptionId, + balanceKopeks, + onBack, +}: TariffPurchaseFormProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { formatAmount, currencySymbol } = useCurrency(); + const { applyPromoDiscount } = usePromoDiscount(); + const ref = useRef(null); + + const formatPrice = (kopeks: number) => + kopeks === 0 + ? t('subscription.free', 'Бесплатно') + : `${formatAmount(kopeks / 100)} ${currencySymbol}`; + + // Form-internal state — seeded from the tariff prop. Resets via + // `key={tariff.id}` on the parent's render. + const [selectedTariffPeriod, setSelectedTariffPeriod] = useState( + tariff.periods[0] || null, + ); + const [customDays, setCustomDays] = useState(30); + const [customTrafficGb, setCustomTrafficGb] = useState(50); + const [useCustomDays, setUseCustomDays] = useState(false); + const [useCustomTraffic, setUseCustomTraffic] = useState(false); + + const purchaseMutation = useMutation({ + mutationFn: () => { + const isDailyTariff = + tariff.is_daily || (tariff.daily_price_kopeks && tariff.daily_price_kopeks > 0); + const days = isDailyTariff + ? 1 + : useCustomDays + ? customDays + : selectedTariffPeriod?.days || 30; + const trafficGb = + useCustomTraffic && tariff.custom_traffic_enabled ? customTrafficGb : undefined; + // Forward the subscription_id when the user landed here via the + // "Renew this subscription" flow (?subscriptionId=N). The backend + // uses it to resolve the exact target row by ID, avoiding the + // race with concurrent panel webhooks that would otherwise hit + // the partial UNIQUE on uq_subscriptions_user_tariff_active. + return subscriptionApi.purchaseTariff( + tariff.id, + days, + trafficGb, + subscriptionId ?? undefined, + ); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + navigate('/subscriptions', { replace: true }); + }, + }); + + // Smooth scroll the form into view when first mounted. + useEffect(() => { + if (ref.current) { + const timer = setTimeout(() => { + ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 100); + return () => clearTimeout(timer); + } + }, []); + + return ( +
+
+

{tariff.name}

+ +
+ + {/* Tariff Info */} +
+
+
+ {t('subscription.traffic')}: + {tariff.traffic_limit_label} +
+
+ {t('subscription.devices')}: + + {tariff.device_limit === 0 ? '∞' : tariff.device_limit} + {tariff.extra_devices_count > 0 && ( + + (+{tariff.extra_devices_count}) + + )} + +
+
+
+ + {/* Daily Tariff Purchase */} + {tariff.is_daily || (tariff.daily_price_kopeks && tariff.daily_price_kopeks > 0) ? ( +
+
+
+ {t('subscription.dailyPurchase.costPerDay')} +
+
+ {formatPrice(tariff.daily_price_kopeks || 0)} +
+
+
+
+ + {t('subscription.dailyPurchase.chargedDaily')} +
+
+ + {t('subscription.dailyPurchase.canPause')} +
+
+ + {t('subscription.dailyPurchase.pausedOnLowBalance')} +
+
+ + {(() => { + const dailyPrice = tariff.daily_price_kopeks || 0; + const hasEnoughBalance = balanceKopeks !== undefined && dailyPrice <= balanceKopeks; + + return ( +
+ {balanceKopeks !== undefined && !hasEnoughBalance && ( + + )} + + + + {purchaseMutation.isError && + !getInsufficientBalanceError(purchaseMutation.error) && ( +
+ {getErrorMessage(purchaseMutation.error)} +
+ )} + {purchaseMutation.isError && + getInsufficientBalanceError(purchaseMutation.error) && ( +
+ +
+ )} +
+ ); + })()} +
+ ) : ( + <> + {/* Period Selection for non-daily tariffs */} +
+
{t('subscription.selectPeriod')}
+ + {tariff.periods.length > 0 && !useCustomDays && ( +
+ {tariff.periods.map((period) => { + const promoPeriod = applyPromoDiscount( + period.price_kopeks, + period.original_price_kopeks, + ); + const displayDiscount = promoPeriod.percent; + const displayOriginal = promoPeriod.original; + const displayPrice = promoPeriod.price; + const displayPerMonth = + displayPrice !== period.price_kopeks + ? Math.round(displayPrice / Math.max(1, period.days / 30)) + : period.price_per_month_kopeks; + + return ( + + ); + })} +
+ )} + + {/* No periods available fallback */} + {tariff.periods.length === 0 && + !useCustomDays && + !(tariff.custom_days_enabled && (tariff.price_per_day_kopeks ?? 0) > 0) && ( +
+
+ {t('subscription.noPeriodsAvailable')} +
+
+ {t('subscription.noPeriodsAvailableHint')} +
+ +
+ )} + + {/* Custom days option */} + {tariff.custom_days_enabled && (tariff.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( + tariff.min_days ?? 1, + Math.min( + tariff.max_days ?? 365, + parseInt(e.target.value) || (tariff.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 * (tariff.price_per_day_kopeks ?? 0); + const existingOriginal = + tariff.original_price_per_day_kopeks && + tariff.original_price_per_day_kopeks > (tariff.price_per_day_kopeks ?? 0) + ? customDays * tariff.original_price_per_day_kopeks + : undefined; + const promoCustom = applyPromoDiscount(basePrice, existingOriginal); + return ( +
+ + {t('subscription.days', { count: customDays })} ×{' '} + {formatPrice(tariff.price_per_day_kopeks ?? 0)}/ + {t('subscription.customDays.perDay')} + +
+ + {formatPrice(promoCustom.price)} + + {promoCustom.original && promoCustom.original > promoCustom.price && ( + <> + + {formatPrice(promoCustom.original)} + + + -{promoCustom.percent}% + + + )} +
+
+ ); + })()} +
+ )} +
+ )} +
+ + {/* Custom traffic option */} + {tariff.custom_traffic_enabled && (tariff.traffic_price_per_gb_kopeks ?? 0) > 0 && ( +
+
+ {t('subscription.customTraffic.label')} +
+
+
+ + {t('subscription.customTraffic.selectVolume')} + + +
+ {!useCustomTraffic && ( +
+ {t('subscription.customTraffic.default', { + label: tariff.traffic_limit_label, + })} +
+ )} + {useCustomTraffic && ( +
+
+ setCustomTrafficGb(parseInt(e.target.value))} + className="flex-1 accent-accent-500" + /> +
+ + setCustomTrafficGb( + Math.max( + tariff.min_traffic_gb ?? 1, + Math.min( + tariff.max_traffic_gb ?? 1000, + parseInt(e.target.value) || (tariff.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(tariff.traffic_price_per_gb_kopeks ?? 0)}/ + {t('common.units.gb')} + + + +{formatPrice(customTrafficGb * (tariff.traffic_price_per_gb_kopeks ?? 0))} + +
+
+ )} +
+
+ )} + + {/* Summary & Purchase */} + {(selectedTariffPeriod || useCustomDays) && ( +
+ {(() => { + const basePeriodPrice = useCustomDays + ? customDays * (tariff.price_per_day_kopeks ?? 0) + : selectedTariffPeriod?.price_kopeks || 0; + const existingPeriodOriginal = useCustomDays + ? tariff.original_price_per_day_kopeks && + tariff.original_price_per_day_kopeks > (tariff.price_per_day_kopeks ?? 0) + ? customDays * tariff.original_price_per_day_kopeks + : undefined + : selectedTariffPeriod?.original_price_kopeks && + selectedTariffPeriod.original_price_kopeks > selectedTariffPeriod.price_kopeks + ? selectedTariffPeriod.original_price_kopeks + : undefined; + const promoPeriod = applyPromoDiscount(basePeriodPrice, existingPeriodOriginal); + + const trafficPrice = + useCustomTraffic && tariff.custom_traffic_enabled + ? customTrafficGb * (tariff.traffic_price_per_gb_kopeks ?? 0) + : 0; + + const totalPrice = promoPeriod.price + trafficPrice; + const originalTotal = promoPeriod.original + ? promoPeriod.original + trafficPrice + : null; + + return ( + <> +
+ {useCustomDays ? ( +
+ + {t('subscription.stepPeriod')}:{' '} + {t('subscription.days', { count: customDays })} + +
+ {formatPrice(promoPeriod.price)} + {promoPeriod.original && promoPeriod.original > promoPeriod.price && ( + + {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)} + {promoPeriod.original && + promoPeriod.original > promoPeriod.price && ( + + {formatPrice(promoPeriod.original)} + + )} +
+
+ )} + + ) + )} + {useCustomTraffic && tariff.custom_traffic_enabled && ( +
+ {t('subscription.summary.traffic', { gb: customTrafficGb })} + +{formatPrice(trafficPrice)} +
+ )} +
+ + {promoPeriod.percent && ( +
+ + {t('promo.discountApplied')} -{promoPeriod.percent}% + +
+ )} + +
+ {t('subscription.total')} +
+ + {formatPrice(totalPrice)} + + {originalTotal && ( +
+ {formatPrice(originalTotal)} +
+ )} +
+
+ + + + ); + })()} + + {purchaseMutation.isError && !getInsufficientBalanceError(purchaseMutation.error) && ( +
+ {getErrorMessage(purchaseMutation.error)} +
+ )} + {purchaseMutation.isError && getInsufficientBalanceError(purchaseMutation.error) && ( +
+ +
+ )} +
+ )} + + )} +
+ ); +} diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index 170ed7e..f3c108d 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router'; @@ -7,24 +7,15 @@ import { usePromoDiscount } from '../hooks/usePromoDiscount'; import { WebBackButton } from '../components/WebBackButton'; import { getGlassColors } from '../utils/glassTheme'; import { useTheme } from '../hooks/useTheme'; -import type { - PurchaseSelection, - PeriodOption, - Tariff, - TariffPeriod, - ClassicPurchaseOptions, -} from '../types'; +import type { PurchaseSelection, PeriodOption, Tariff, ClassicPurchaseOptions } from '../types'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import { useCurrency } from '../hooks/useCurrency'; import { useCloseOnSuccessNotification } from '../store/successNotification'; import { CheckIcon } from '../components/icons'; import Twemoji from 'react-twemoji'; import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet'; -import { - getErrorMessage, - getInsufficientBalanceError, - type PurchaseStep, -} from '../utils/subscriptionHelpers'; +import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm'; +import { getErrorMessage, type PurchaseStep } from '../utils/subscriptionHelpers'; export default function SubscriptionPurchase() { const { t } = useTranslation(); @@ -95,15 +86,13 @@ export default function SubscriptionPurchase() { // Tariffs mode state const [selectedTariff, setSelectedTariff] = useState(null); - const [selectedTariffPeriod, setSelectedTariffPeriod] = useState(null); const [showTariffPurchase, setShowTariffPurchase] = useState(false); - const [customDays, setCustomDays] = useState(30); - const [customTrafficGb, setCustomTrafficGb] = useState(50); - const [useCustomDays, setUseCustomDays] = useState(false); - const [useCustomTraffic, setUseCustomTraffic] = useState(false); + // (selectedTariffPeriod / customDays / customTrafficGb / useCustomDays / + // useCustomTraffic moved into ; form remounts with + // fresh state via key=tariff.id when the parent picks a new tariff) - // Refs for auto-scroll (switch-modal ref moved into ) - const tariffPurchaseRef = useRef(null); + // (tariffPurchaseRef moved into ; switch-modal ref + // moved into ) // Tariff switch const [switchTariffId, setSwitchTariffId] = useState(null); @@ -115,7 +104,7 @@ export default function SubscriptionPurchase() { setSwitchTariffId(null); setSelectedTariff(null); - setSelectedTariffPeriod(null); + // (selectedTariffPeriod lives inside now; unmount clears it) }; useCloseOnSuccessNotification(handleCloseAllModals); @@ -207,51 +196,9 @@ export default function SubscriptionPurchase() { // (switch preview query + switchTariffMutation moved into ) - // Tariff purchase mutation - const tariffPurchaseMutation = useMutation({ - mutationFn: () => { - if (!selectedTariff) { - throw new Error('Tariff not selected'); - } - 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; - // Forward the subscription_id when the user landed here via the - // "Renew this subscription" flow (?subscriptionId=N). The backend - // uses it to resolve the exact target row by ID, avoiding the - // race with concurrent panel webhooks that would otherwise hit - // the partial UNIQUE on uq_subscriptions_user_tariff_active. - return subscriptionApi.purchaseTariff( - selectedTariff.id, - days, - trafficGb, - subscriptionId ?? undefined, - ); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['subscription'] }); - queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); - queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); - navigate('/subscriptions', { replace: true }); - }, - }); - - // Auto-scroll effects (switch-tariff scroll moved into the sheet) - useEffect(() => { - if (showTariffPurchase && tariffPurchaseRef.current) { - const timer = setTimeout(() => { - tariffPurchaseRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }, 100); - return () => clearTimeout(timer); - } - }, [showTariffPurchase]); + // (tariffPurchaseMutation moved into ) + // (auto-scroll effects: switch-modal into , + // tariff-purchase into ) // Classic mode helpers const toggleServer = (uuid: string) => { @@ -484,7 +431,6 @@ export default function SubscriptionPurchase() { onClose={() => setSwitchTariffId(null)} onExpiredFallback={(tariff) => { setSelectedTariff(tariff); - setSelectedTariffPeriod(tariff.periods[0] || null); setShowTariffPurchase(true); }} /> @@ -756,7 +702,6 @@ export default function SubscriptionPurchase() { - - - {/* Tariff Info */} -
-
-
- {t('subscription.traffic')}: - - {selectedTariff.traffic_limit_label} - -
-
- {t('subscription.devices')}: - - {selectedTariff.device_limit === 0 ? '∞' : selectedTariff.device_limit} - {selectedTariff.extra_devices_count > 0 && ( - - (+{selectedTariff.extra_devices_count}) - - )} - -
-
-
- - {/* 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')} -
-
- - {(() => { - 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')} -
- - {selectedTariff.periods.length > 0 && !useCustomDays && ( -
- {selectedTariff.periods.map((period) => { - const promoPeriod = applyPromoDiscount( - period.price_kopeks, - period.original_price_kopeks, - ); - const displayDiscount = promoPeriod.percent; - const displayOriginal = promoPeriod.original; - const displayPrice = promoPeriod.price; - const displayPerMonth = - displayPrice !== period.price_kopeks - ? Math.round(displayPrice / Math.max(1, period.days / 30)) - : period.price_per_month_kopeks; - - return ( - - ); - })} -
- )} - - {/* No periods available fallback */} - {selectedTariff.periods.length === 0 && - !useCustomDays && - !( - selectedTariff.custom_days_enabled && - (selectedTariff.price_per_day_kopeks ?? 0) > 0 - ) && ( -
-
- {t('subscription.noPeriodsAvailable')} -
-
- {t('subscription.noPeriodsAvailableHint')} -
- -
- )} - - {/* 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 existingOriginal = - selectedTariff.original_price_per_day_kopeks && - selectedTariff.original_price_per_day_kopeks > - (selectedTariff.price_per_day_kopeks ?? 0) - ? customDays * selectedTariff.original_price_per_day_kopeks - : undefined; - const promoCustom = applyPromoDiscount( - basePrice, - existingOriginal, - ); - return ( -
- - {t('subscription.days', { count: customDays })} ×{' '} - {formatPrice(selectedTariff.price_per_day_kopeks ?? 0)}/ - {t('subscription.customDays.perDay')} - -
- - {formatPrice(promoCustom.price)} - - {promoCustom.original && - promoCustom.original > promoCustom.price && ( - <> - - {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) && ( -
- {(() => { - const basePeriodPrice = useCustomDays - ? customDays * (selectedTariff.price_per_day_kopeks ?? 0) - : selectedTariffPeriod?.price_kopeks || 0; - const existingPeriodOriginal = useCustomDays - ? selectedTariff.original_price_per_day_kopeks && - selectedTariff.original_price_per_day_kopeks > - (selectedTariff.price_per_day_kopeks ?? 0) - ? customDays * selectedTariff.original_price_per_day_kopeks - : undefined - : selectedTariffPeriod?.original_price_kopeks && - selectedTariffPeriod.original_price_kopeks > - selectedTariffPeriod.price_kopeks - ? selectedTariffPeriod.original_price_kopeks - : undefined; - const promoPeriod = applyPromoDiscount( - basePeriodPrice, - existingPeriodOriginal, - ); - - 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 ( - <> -
- {useCustomDays ? ( -
- - {t('subscription.stepPeriod')}:{' '} - {t('subscription.days', { count: customDays })} - -
- {formatPrice(promoPeriod.price)} - {promoPeriod.original && - promoPeriod.original > promoPeriod.price && ( - - {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)} - {promoPeriod.original && - promoPeriod.original > promoPeriod.price && ( - - {formatPrice(promoPeriod.original)} - - )} -
-
- )} - - ) - )} - {useCustomTraffic && selectedTariff.custom_traffic_enabled && ( -
- - {t('subscription.summary.traffic', { gb: customTrafficGb })} - - +{formatPrice(trafficPrice)} -
- )} -
- - {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) && ( -
- -
- )} -
- )} - - )} - + /* Tariff Purchase Form (extracted into its own component) */ + { + setShowTariffPurchase(false); + setSelectedTariff(null); + }} + /> ) )}