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.
This commit is contained in:
c0mrade
2026-01-30 19:54:29 +03:00
parent 175b5608b2
commit 0be1ef0340
4 changed files with 57 additions and 3 deletions

View File

@@ -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<PaymentMethod | null>(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,

View File

@@ -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) => {

View File

@@ -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<number | null>(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!),

View File

@@ -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<SuccessNotificationState>((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]);
}