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 */}