diff --git a/src/components/subscription/sheets/TrafficTopupSheet.tsx b/src/components/subscription/sheets/TrafficTopupSheet.tsx
new file mode 100644
index 0000000..6171bc5
--- /dev/null
+++ b/src/components/subscription/sheets/TrafficTopupSheet.tsx
@@ -0,0 +1,228 @@
+import { useTranslation } from 'react-i18next';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { subscriptionApi } from '../../../api/subscription';
+import { getErrorMessage } from '../../../utils/subscriptionHelpers';
+import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
+import type { PurchaseOptions, Subscription } from '../../../types';
+
+// ──────────────────────────────────────────────────────────────────
+// Buy-traffic sheet. Self-owns the packages query + purchase mutation;
+// parent passes the selectedTrafficPackage state (the parent already
+// resets it on global "close all modals", which is why it stays up
+// top), shared purchaseOptions, and ids/flags.
+//
+// Extracted from Subscription.tsx — ~170 lines off the god page.
+// ──────────────────────────────────────────────────────────────────
+
+export interface TrafficTopupSheetProps {
+ open: boolean;
+ onOpen: () => void;
+ onClose: () => void;
+ subscription: Subscription;
+ subscriptionId: number | undefined;
+ selectedTrafficPackage: number | null;
+ onSelectedTrafficPackageChange: (gb: number | null) => void;
+ purchaseOptions: PurchaseOptions | undefined;
+ isDark: boolean;
+}
+
+export function TrafficTopupSheet({
+ open,
+ onOpen,
+ onClose,
+ subscription,
+ subscriptionId,
+ selectedTrafficPackage,
+ onSelectedTrafficPackageChange,
+ purchaseOptions,
+ isDark,
+}: TrafficTopupSheetProps) {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+
+ const formatPrice = (kopeks: number) => {
+ const rubles = kopeks / 100;
+ return rubles % 1 === 0 ? `${rubles} ₽` : `${rubles.toFixed(2)} ₽`;
+ };
+
+ const { data: trafficPackages } = useQuery({
+ queryKey: ['traffic-packages', subscriptionId],
+ queryFn: () => subscriptionApi.getTrafficPackages(subscriptionId),
+ enabled: open && !!subscription,
+ });
+
+ const purchaseMutation = useMutation({
+ mutationFn: (gb: number) => subscriptionApi.purchaseTraffic(gb, subscriptionId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] });
+ queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
+ queryClient.invalidateQueries({ queryKey: ['balance'] });
+ queryClient.invalidateQueries({ queryKey: ['traffic-packages', subscriptionId] });
+ onClose();
+ onSelectedTrafficPackageChange(null);
+ },
+ });
+
+ if (!open) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {t('subscription.additionalOptions.buyTrafficTitle')}
+
+
+
+
+
+ ⚠️ {t('subscription.additionalOptions.trafficWarning')}
+
+
+ {!trafficPackages || trafficPackages.length === 0 ? (
+
+ {t('subscription.additionalOptions.trafficUnavailable')}
+
+ ) : (
+
+
+ {trafficPackages.map((pkg) => (
+
+ ))}
+
+
+ {selectedTrafficPackage !== null &&
+ (() => {
+ 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,
+ subscriptionId,
+ );
+ }}
+ />
+ )}
+
+ >
+ );
+ })()}
+
+ {purchaseMutation.isError && (
+
+ {getErrorMessage(purchaseMutation.error)}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx
index 5e89baa..29fd983 100644
--- a/src/pages/Subscription.tsx
+++ b/src/pages/Subscription.tsx
@@ -29,6 +29,7 @@ import {
import Twemoji from 'react-twemoji';
import { DeviceTopupSheet } from '../components/subscription/sheets/DeviceTopupSheet';
import { DeviceReductionSheet } from '../components/subscription/sheets/DeviceReductionSheet';
+import { TrafficTopupSheet } from '../components/subscription/sheets/TrafficTopupSheet';
/** Isolated countdown so 1s interval doesn't re-render the whole page */
const CountdownTimer = memo(function CountdownTimer({
@@ -367,25 +368,7 @@ export default function Subscription() {
// (device price + purchase moved into )
// (device reduction info + mutation moved into )
- // Traffic packages query
- const { data: trafficPackages } = useQuery({
- queryKey: ['traffic-packages', subscriptionId],
- queryFn: () => subscriptionApi.getTrafficPackages(subscriptionId),
- enabled: showTrafficTopup && !!subscription,
- });
-
- // Traffic purchase mutation
- const trafficPurchaseMutation = useMutation({
- mutationFn: (gb: number) => subscriptionApi.purchaseTraffic(gb, subscriptionId),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] });
- queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
- queryClient.invalidateQueries({ queryKey: ['balance'] });
- queryClient.invalidateQueries({ queryKey: ['traffic-packages', subscriptionId] });
- setShowTrafficTopup(false);
- setSelectedTrafficPackage(null);
- },
- });
+ // (traffic packages + purchase moved into )
// Countries/servers query
const { data: countriesData, isLoading: countriesLoading } = useQuery({
@@ -1519,171 +1502,17 @@ export default function Subscription() {
{/* 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 !== null &&
- (() => {
- 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,
- subscriptionId,
- );
- }}
- />
- )}
-
- >
- );
- })()}
-
- {trafficPurchaseMutation.isError && (
-
- {getErrorMessage(trafficPurchaseMutation.error)}
-
- )}
-
- )}
-
- )}
+
setShowTrafficTopup(true)}
+ onClose={() => setShowTrafficTopup(false)}
+ subscription={subscription}
+ subscriptionId={subscriptionId}
+ selectedTrafficPackage={selectedTrafficPackage}
+ onSelectedTrafficPackageChange={setSelectedTrafficPackage}
+ purchaseOptions={purchaseOptions}
+ isDark={isDark}
+ />
)}