From fbf34a486162fcd0a5728bf564aef6f0c70c43d2 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 22:17:41 +0300 Subject: [PATCH] refactor(subscription): extract TrafficTopupSheet from god page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the buy-traffic sheet (~170 lines of inline JSX + its packages useQuery + purchase useMutation) into src/components/subscription/sheets/TrafficTopupSheet.tsx. Parent keeps selectedTrafficPackage in its state so the global 'close all modals' callback can still reset it; the sheet owns the network calls and only fires while open. Subscription.tsx: 2288 → ~2120 lines. --- .../subscription/sheets/TrafficTopupSheet.tsx | 228 ++++++++++++++++++ src/pages/Subscription.tsx | 197 +-------------- 2 files changed, 241 insertions(+), 184 deletions(-) create mode 100644 src/components/subscription/sheets/TrafficTopupSheet.tsx 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} + />
)}