From 0be1ef034054d01b76e262515f20761910323a74 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Fri, 30 Jan 2026 19:54:29 +0300 Subject: [PATCH] feat: auto-close modals when success notification appears - Add closeOthersSignal to success notification store - Create useCloseOnSuccessNotification hook for modals to subscribe - Update TopUpModal to auto-close on success events - Update InsufficientBalancePrompt to auto-close on success events - Update Subscription page to close all modals/forms on success events This ensures that when a WebSocket event indicates successful payment or subscription purchase, all related modals close automatically, providing clear feedback to users. --- src/components/InsufficientBalancePrompt.tsx | 10 ++++++- src/components/TopUpModal.tsx | 4 +++ src/pages/Subscription.tsx | 18 ++++++++++++- src/store/successNotification.ts | 28 +++++++++++++++++++- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/components/InsufficientBalancePrompt.tsx b/src/components/InsufficientBalancePrompt.tsx index a2802af..1ef404b 100644 --- a/src/components/InsufficientBalancePrompt.tsx +++ b/src/components/InsufficientBalancePrompt.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { balanceApi } from '../api/balance'; import { useCurrency } from '../hooks/useCurrency'; +import { useCloseOnSuccessNotification } from '../store/successNotification'; import TopUpModal from './TopUpModal'; import type { PaymentMethod } from '../types'; @@ -28,6 +29,13 @@ export default function InsufficientBalancePrompt({ const [showMethodSelect, setShowMethodSelect] = useState(false); const [selectedMethod, setSelectedMethod] = useState(null); + // Auto-close modals when success notification appears + const handleCloseAll = useCallback(() => { + setShowMethodSelect(false); + setSelectedMethod(null); + }, []); + useCloseOnSuccessNotification(handleCloseAll); + const { data: paymentMethods } = useQuery({ queryKey: ['payment-methods'], queryFn: balanceApi.getPaymentMethods, diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index 0ebcd3d..5f2975d 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -6,6 +6,7 @@ import { balanceApi } from '../api/balance'; import { useCurrency } from '../hooks/useCurrency'; import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'; +import { useCloseOnSuccessNotification } from '../store/successNotification'; import type { PaymentMethod } from '../types'; import BentoCard from './ui/BentoCard'; @@ -144,6 +145,9 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top onClose(); }, [onClose]); + // Auto-close when success notification appears (e.g., balance topped up via WebSocket) + useCloseOnSuccessNotification(handleClose); + // Keyboard: Escape to close (PC) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index d58f85d..df35be0 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -15,6 +15,7 @@ import type { import ConnectionModal from '../components/ConnectionModal'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import { useCurrency } from '../hooks/useCurrency'; +import { useCloseOnSuccessNotification } from '../store/successNotification'; import i18n from '../i18n'; // Helper to extract error message from axios/api errors @@ -279,6 +280,21 @@ export default function Subscription() { // Tariff switch preview const [switchTariffId, setSwitchTariffId] = useState(null); + + // Auto-close all modals/forms when success notification appears (e.g., subscription purchased via WebSocket) + const handleCloseAllModals = useCallback(() => { + setShowConnectionModal(false); + setShowPurchaseForm(false); + setShowTariffPurchase(false); + setShowDeviceTopup(false); + setShowTrafficTopup(false); + setShowServerManagement(false); + setSwitchTariffId(null); + setSelectedTariff(null); + setSelectedTariffPeriod(null); + }, []); + useCloseOnSuccessNotification(handleCloseAllModals); + const { data: switchPreview, isLoading: switchPreviewLoading } = useQuery({ queryKey: ['tariff-switch-preview', switchTariffId], queryFn: () => subscriptionApi.previewTariffSwitch(switchTariffId!), diff --git a/src/store/successNotification.ts b/src/store/successNotification.ts index 72a92ec..eb04ab0 100644 --- a/src/store/successNotification.ts +++ b/src/store/successNotification.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { create } from 'zustand'; export type SuccessNotificationType = @@ -25,6 +26,8 @@ export interface SuccessNotificationData { interface SuccessNotificationState { isOpen: boolean; data: SuccessNotificationData | null; + /** Signal that increments when other modals should close */ + closeOthersSignal: number; show: (data: SuccessNotificationData) => void; hide: () => void; @@ -33,7 +36,30 @@ interface SuccessNotificationState { export const useSuccessNotification = create((set) => ({ isOpen: false, data: null, + closeOthersSignal: 0, - show: (data) => set({ isOpen: true, data }), + show: (data) => + set((state) => ({ + isOpen: true, + data, + // Increment signal to tell other modals to close + closeOthersSignal: state.closeOthersSignal + 1, + })), hide: () => set({ isOpen: false, data: null }), })); + +/** + * Hook that calls onClose when a success notification appears. + * Use this in modals that should auto-close on success events. + */ +export function useCloseOnSuccessNotification(onClose: () => void) { + const closeOthersSignal = useSuccessNotification((state) => state.closeOthersSignal); + + useEffect(() => { + // Skip the initial render (signal = 0) + if (closeOthersSignal > 0) { + onClose(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [closeOthersSignal]); +}