From 5902f40187a5f55878a806709f0ea9d9919c34cb Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 22:15:15 +0300 Subject: [PATCH] refactor(subscription): extract DeviceReductionSheet from god page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the reduce-devices sheet (~170 lines of inline JSX + its useQuery + useMutation + seeding useEffect) into src/components/subscription/sheets/DeviceReductionSheet.tsx. Parent passes the shared targetDeviceLimit state (also used by the revoke flow earlier) plus open/close + ids; the sheet owns the reduction-info query and the reduce mutation, so neither fires until the sheet is open. Subscription.tsx: 2477 → 2288 lines (−189). --- .../sheets/DeviceReductionSheet.tsx | 235 ++++++++++++++++++ src/pages/Subscription.tsx | 213 +--------------- 2 files changed, 247 insertions(+), 201 deletions(-) create mode 100644 src/components/subscription/sheets/DeviceReductionSheet.tsx diff --git a/src/components/subscription/sheets/DeviceReductionSheet.tsx b/src/components/subscription/sheets/DeviceReductionSheet.tsx new file mode 100644 index 0000000..2a536a9 --- /dev/null +++ b/src/components/subscription/sheets/DeviceReductionSheet.tsx @@ -0,0 +1,235 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { subscriptionApi } from '../../../api/subscription'; +import { getErrorMessage } from '../../../utils/subscriptionHelpers'; + +// ────────────────────────────────────────────────────────────────── +// Reduce-devices sheet. Self-owns the reduction-info query + mutation; +// parent passes shared state (target limit + setter + ids + open flag). +// +// Extracted from Subscription.tsx to keep one cohesive feature in +// one file (~170 lines off the god page). +// ────────────────────────────────────────────────────────────────── + +export interface DeviceReductionSheetProps { + open: boolean; + onOpen: () => void; + onClose: () => void; + subscriptionPresent: boolean; + subscriptionId: number | undefined; + targetDeviceLimit: number; + onTargetDeviceLimitChange: (n: number) => void; + isDark: boolean; +} + +export function DeviceReductionSheet({ + open, + onOpen, + onClose, + subscriptionPresent, + subscriptionId, + targetDeviceLimit, + onTargetDeviceLimitChange, + isDark, +}: DeviceReductionSheetProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { data: deviceReductionInfo } = useQuery({ + queryKey: ['device-reduction-info', subscriptionId], + queryFn: () => subscriptionApi.getDeviceReductionInfo(subscriptionId), + enabled: open && subscriptionPresent, + }); + + // Seed the target limit once the info comes back. + useEffect(() => { + if (deviceReductionInfo && open) { + onTargetDeviceLimitChange( + Math.max( + deviceReductionInfo.min_device_limit, + deviceReductionInfo.current_device_limit - 1, + ), + ); + } + // onTargetDeviceLimitChange is a setter from the parent and stable + // enough; intentionally narrow deps to avoid clobbering user input. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deviceReductionInfo, open]); + + const reduceMutation = useMutation({ + mutationFn: () => subscriptionApi.reduceDevices(targetDeviceLimit, subscriptionId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] }); + queryClient.invalidateQueries({ queryKey: ['device-reduction-info', subscriptionId] }); + onClose(); + }, + }); + + if (!open) { + return ( + + ); + } + + return ( +
+
+

+ {t('subscription.additionalOptions.reduceDevicesTitle')} +

+ +
+ + {deviceReductionInfo?.available === false ? ( +
+ {deviceReductionInfo.reason || t('subscription.additionalOptions.reduceUnavailable')} +
+ ) : deviceReductionInfo ? ( +
+
+ +
+
{targetDeviceLimit}
+
+ {t('subscription.additionalOptions.devicesUnit')} +
+
+ +
+ +
+
+ {t('subscription.additionalOptions.currentDeviceLimit', { + count: deviceReductionInfo.current_device_limit, + })} +
+
+ {t('subscription.additionalOptions.minDeviceLimit', { + count: deviceReductionInfo.min_device_limit, + })} +
+
+ {t('subscription.additionalOptions.connectedDevices', { + count: deviceReductionInfo.connected_devices_count, + })} +
+
+ + {deviceReductionInfo.connected_devices_count > deviceReductionInfo.min_device_limit && ( +
+ {t('subscription.additionalOptions.disconnectDevicesFirst', { + count: deviceReductionInfo.connected_devices_count, + })} +
+ )} + +
+
+ {t('subscription.additionalOptions.newDeviceLimit', { + count: targetDeviceLimit, + })} +
+
+ + + + {reduceMutation.isError && ( +
+ {getErrorMessage(reduceMutation.error)} +
+ )} +
+ ) : ( +
+ +
+ )} +
+ ); +} diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 97296c0..5e89baa 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -28,6 +28,7 @@ import { } from '../utils/subscriptionHelpers'; import Twemoji from 'react-twemoji'; import { DeviceTopupSheet } from '../components/subscription/sheets/DeviceTopupSheet'; +import { DeviceReductionSheet } from '../components/subscription/sheets/DeviceReductionSheet'; /** Isolated countdown so 1s interval doesn't re-render the whole page */ const CountdownTimer = memo(function CountdownTimer({ @@ -364,37 +365,7 @@ export default function Subscription() { useCloseOnSuccessNotification(handleCloseAllModals); // (device price + purchase moved into ) - - // Device reduction info query - const { data: deviceReductionInfo } = useQuery({ - queryKey: ['device-reduction-info', subscriptionId], - queryFn: () => subscriptionApi.getDeviceReductionInfo(subscriptionId), - enabled: showDeviceReduction && !!subscription, - }); - - // Initialize target device limit when reduction info loads - useEffect(() => { - if (deviceReductionInfo && showDeviceReduction) { - setTargetDeviceLimit( - Math.max( - deviceReductionInfo.min_device_limit, - deviceReductionInfo.current_device_limit - 1, - ), - ); - } - }, [deviceReductionInfo, showDeviceReduction]); - - // Device reduction mutation - const deviceReductionMutation = useMutation({ - mutationFn: () => subscriptionApi.reduceDevices(targetDeviceLimit, subscriptionId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); - queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); - queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] }); - queryClient.invalidateQueries({ queryKey: ['device-reduction-info', subscriptionId] }); - setShowDeviceReduction(false); - }, - }); + // (device reduction info + mutation moved into ) // Traffic packages query const { data: trafficPackages } = useQuery({ @@ -1533,176 +1504,16 @@ export default function Subscription() { {/* Reduce Devices */}
- {!showDeviceReduction ? ( - - ) : ( -
-
-

- {t('subscription.additionalOptions.reduceDevicesTitle')} -

- -
- - {deviceReductionInfo?.available === false ? ( -
- {deviceReductionInfo.reason || - t('subscription.additionalOptions.reduceUnavailable')} -
- ) : deviceReductionInfo ? ( -
- {/* Device limit selector */} -
- -
-
- {targetDeviceLimit} -
-
- {t('subscription.additionalOptions.devicesUnit')} -
-
- -
- - {/* Info */} -
-
- {t('subscription.additionalOptions.currentDeviceLimit', { - count: deviceReductionInfo.current_device_limit, - })} -
-
- {t('subscription.additionalOptions.minDeviceLimit', { - count: deviceReductionInfo.min_device_limit, - })} -
-
- {t('subscription.additionalOptions.connectedDevices', { - count: deviceReductionInfo.connected_devices_count, - })} -
-
- - {/* Warning if connected devices block reduction */} - {deviceReductionInfo.connected_devices_count > - deviceReductionInfo.min_device_limit && ( -
- {t('subscription.additionalOptions.disconnectDevicesFirst', { - count: deviceReductionInfo.connected_devices_count, - })} -
- )} - - {/* New limit preview */} -
-
- {t('subscription.additionalOptions.newDeviceLimit', { - count: targetDeviceLimit, - })} -
-
- - - - {deviceReductionMutation.isError && ( -
- {getErrorMessage(deviceReductionMutation.error)} -
- )} -
- ) : ( -
- -
- )} -
- )} + setShowDeviceReduction(true)} + onClose={() => setShowDeviceReduction(false)} + subscriptionPresent={!!subscription} + subscriptionId={subscriptionId} + targetDeviceLimit={targetDeviceLimit} + onTargetDeviceLimitChange={setTargetDeviceLimit} + isDark={isDark} + />
{/* Buy Traffic */}