diff --git a/src/components/subscription/sheets/DeviceTopupSheet.tsx b/src/components/subscription/sheets/DeviceTopupSheet.tsx new file mode 100644 index 0000000..e59dacf --- /dev/null +++ b/src/components/subscription/sheets/DeviceTopupSheet.tsx @@ -0,0 +1,254 @@ +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-devices sheet. Self-owns its devicePrice query + purchase mutation; +// parent only passes the subscription / id / open state and the shared +// purchaseOptions (which it already holds for sibling sheets and balance +// gating). +// +// Extracted from Subscription.tsx to drop ~190 lines from the god page. +// ────────────────────────────────────────────────────────────────── + +export interface DeviceTopupSheetProps { + open: boolean; + onOpen: () => void; + onClose: () => void; + subscription: Subscription; + subscriptionId: number | undefined; + devicesToAdd: number; + onDevicesToAddChange: (n: number) => void; + purchaseOptions: PurchaseOptions | undefined; + isDark: boolean; +} + +export function DeviceTopupSheet({ + open, + onOpen, + onClose, + subscription, + subscriptionId, + devicesToAdd, + onDevicesToAddChange, + purchaseOptions, + isDark, +}: DeviceTopupSheetProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const formatPrice = (kopeks: number) => { + const rubles = kopeks / 100; + return rubles % 1 === 0 ? `${rubles} ₽` : `${rubles.toFixed(2)} ₽`; + }; + + const { data: devicePriceData } = useQuery({ + queryKey: ['device-price', devicesToAdd, subscriptionId], + queryFn: () => subscriptionApi.getDevicePrice(devicesToAdd, subscriptionId), + enabled: open && !!subscription, + }); + + const devicePurchaseMutation = useMutation({ + mutationFn: () => subscriptionApi.purchaseDevices(devicesToAdd, subscriptionId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] }); + queryClient.invalidateQueries({ queryKey: ['device-price'] }); + queryClient.invalidateQueries({ queryKey: ['balance'] }); + onClose(); + onDevicesToAddChange(1); + }, + }); + + if (!open) { + return ( + + ); + } + + return ( +
+
+

{t('subscription.buyDevices')}

+ +
+ + {devicePriceData?.available === false ? ( +
+ {devicePriceData.reason || t('subscription.additionalOptions.devicesUnavailable')} +
+ ) : ( +
+ {/* Device counter */} +
+ +
+
{devicesToAdd}
+
+ {t('subscription.additionalOptions.devicesUnit')} +
+
+ +
+ + {devicePriceData?.max_device_limit && ( +
+ {t('subscription.additionalOptions.currentDeviceLimit', { + count: devicePriceData.current_device_limit || subscription.device_limit, + })}{' '} + /{' '} + {t('subscription.additionalOptions.maxDevices', { + count: devicePriceData.max_device_limit, + })} +
+ )} + + {/* Price info */} + {devicePriceData?.available && devicePriceData.price_per_device_label && ( +
+
+ {devicePriceData.discount_percent && + devicePriceData.discount_percent > 0 && + devicePriceData.original_price_per_device_kopeks ? ( + + + {formatPrice(devicePriceData.original_price_per_device_kopeks)} + + {devicePriceData.price_per_device_label} + + ) : ( + devicePriceData.price_per_device_label + )} + /{t('subscription.perDevice').replace('/ ', '')} ( + {t('subscription.days', { count: devicePriceData.days_left })}) +
+ {devicePriceData.discount_percent && devicePriceData.discount_percent > 0 && ( +
+ + -{devicePriceData.discount_percent}% + +
+ )} + {devicePriceData.total_price_kopeks === 0 ? ( +
+ {t('subscription.switchTariff.free')} +
+ ) : ( +
+ {devicePriceData.discount_percent && + devicePriceData.discount_percent > 0 && + devicePriceData.base_total_price_kopeks && ( + + {formatPrice(devicePriceData.base_total_price_kopeks)} + + )} + {devicePriceData.total_price_label} +
+ )} +
+ )} + + {/* Insufficient balance */} + {devicePriceData?.available && + purchaseOptions && + devicePriceData.total_price_kopeks && + devicePriceData.total_price_kopeks > purchaseOptions.balance_kopeks && ( + { + await subscriptionApi.saveDevicesCart(devicesToAdd, subscriptionId); + }} + /> + )} + + + + {devicePurchaseMutation.isError && ( +
+ {getErrorMessage(devicePurchaseMutation.error)} +
+ )} +
+ )} +
+ ); +} diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index ba083b6..97296c0 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -27,6 +27,7 @@ import { getFlagEmoji, } from '../utils/subscriptionHelpers'; import Twemoji from 'react-twemoji'; +import { DeviceTopupSheet } from '../components/subscription/sheets/DeviceTopupSheet'; /** Isolated countdown so 1s interval doesn't re-render the whole page */ const CountdownTimer = memo(function CountdownTimer({ @@ -362,26 +363,7 @@ export default function Subscription() { }, []); useCloseOnSuccessNotification(handleCloseAllModals); - // Device price query - const { data: devicePriceData } = useQuery({ - queryKey: ['device-price', devicesToAdd, subscriptionId], - queryFn: () => subscriptionApi.getDevicePrice(devicesToAdd, subscriptionId), - enabled: showDeviceTopup && !!subscription, - }); - - // Device purchase mutation - const devicePurchaseMutation = useMutation({ - mutationFn: () => subscriptionApi.purchaseDevices(devicesToAdd, subscriptionId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); - queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); - queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] }); - queryClient.invalidateQueries({ queryKey: ['device-price'] }); - queryClient.invalidateQueries({ queryKey: ['balance'] }); - setShowDeviceTopup(false); - setDevicesToAdd(1); - }, - }); + // (device price + purchase moved into ) // Device reduction info query const { data: deviceReductionInfo } = useQuery({ @@ -1537,193 +1519,17 @@ export default function Subscription() { {/* Buy Devices */} - {!showDeviceTopup ? ( - - ) : ( -
-
-

{t('subscription.buyDevices')}

- -
- - {/* Check if completely unavailable (no subscription, price not set, etc.) */} - {devicePriceData?.available === false ? ( -
- {devicePriceData.reason || - t('subscription.additionalOptions.devicesUnavailable')} -
- ) : ( -
- {/* Device selector - show even at max limit */} -
- -
-
{devicesToAdd}
-
- {t('subscription.additionalOptions.devicesUnit')} -
-
- -
- - {/* Show limit info when at or near max */} - {devicePriceData?.max_device_limit && ( -
- {t('subscription.additionalOptions.currentDeviceLimit', { - count: devicePriceData.current_device_limit || subscription.device_limit, - })}{' '} - /{' '} - {t('subscription.additionalOptions.maxDevices', { - count: devicePriceData.max_device_limit, - })} -
- )} - - {/* Price info - only when available */} - {devicePriceData?.available && devicePriceData.price_per_device_label && ( -
-
- {/* Show original price with strikethrough if discount */} - {devicePriceData.discount_percent && - devicePriceData.discount_percent > 0 && - devicePriceData.original_price_per_device_kopeks ? ( - - - {formatPrice(devicePriceData.original_price_per_device_kopeks)} - - {devicePriceData.price_per_device_label} - - ) : ( - devicePriceData.price_per_device_label - )} - /{t('subscription.perDevice').replace('/ ', '')} ( - {t('subscription.days', { count: devicePriceData.days_left })}) -
- {/* Discount badge */} - {devicePriceData.discount_percent && - devicePriceData.discount_percent > 0 && ( -
- - -{devicePriceData.discount_percent}% - -
- )} - {/* Total price - show as free if 100% discount or 0 */} - {devicePriceData.total_price_kopeks === 0 ? ( -
- {t('subscription.switchTariff.free')} -
- ) : ( -
- {/* Show original total with strikethrough if discount */} - {devicePriceData.discount_percent && - devicePriceData.discount_percent > 0 && - devicePriceData.base_total_price_kopeks && ( - - {formatPrice(devicePriceData.base_total_price_kopeks)} - - )} - {devicePriceData.total_price_label} -
- )} -
- )} - - {devicePriceData?.available && - purchaseOptions && - devicePriceData.total_price_kopeks && - devicePriceData.total_price_kopeks > purchaseOptions.balance_kopeks && ( - { - await subscriptionApi.saveDevicesCart(devicesToAdd, subscriptionId); - }} - /> - )} - - - - {devicePurchaseMutation.isError && ( -
- {getErrorMessage(devicePurchaseMutation.error)} -
- )} -
- )} -
- )} + setShowDeviceTopup(true)} + onClose={() => setShowDeviceTopup(false)} + subscription={subscription} + subscriptionId={subscriptionId} + devicesToAdd={devicesToAdd} + onDevicesToAddChange={setDevicesToAdd} + purchaseOptions={purchaseOptions} + isDark={isDark} + /> {/* Reduce Devices */}