diff --git a/src/hooks/chats/useChatSocket.ts b/src/hooks/chats/useChatSocket.ts index a26af5b..394ff6a 100644 --- a/src/hooks/chats/useChatSocket.ts +++ b/src/hooks/chats/useChatSocket.ts @@ -113,6 +113,11 @@ export const useChatSocket = ( emit("fetch_balance", { chatId }); }, [emit, chatId]); + // Auto top-up: silent flow (no UI prompt) + const autoTopUpInProgressRef = useRef(false); + // Timer for delayed unlock after error + const autoTopUpUnlockTimerRef = useRef(null); + // Auto top-up: use existing single checkout flow const { handleSingleCheckout, isLoading: isAutoTopUpLoading } = useSingleCheckout({ @@ -120,15 +125,18 @@ export const useChatSocket = ( onError: () => { // eslint-disable-next-line no-console console.error("Auto top-up payment failed"); - // Release in-flight lock on error so a future event can retry - autoTopUpInProgressRef.current = false; + // Throttle retries: keep the lock for 30 seconds after an error + if (autoTopUpUnlockTimerRef.current) { + clearTimeout(autoTopUpUnlockTimerRef.current); + } + autoTopUpUnlockTimerRef.current = setTimeout(() => { + autoTopUpInProgressRef.current = false; + autoTopUpUnlockTimerRef.current = null; + }, 30_000); }, returnUrl: typeof window !== "undefined" ? window.location.href : "", }); - // Auto top-up: silent flow (no UI prompt) - const autoTopUpInProgressRef = useRef(false); - const balancePollId = useRef(null); // Avoid immediate leave_chat right after join in React 18 StrictMode (dev) double-invoke // We debounce leave so that a quick remount cancels it, but real navigation (or chat switch) proceeds. @@ -203,12 +211,20 @@ export const useChatSocket = ( // If auto top-up was in-flight, release the lock only after balance became positive if (autoTopUpInProgressRef.current && b?.data?.balance > 0) { autoTopUpInProgressRef.current = false; + if (autoTopUpUnlockTimerRef.current) { + clearTimeout(autoTopUpUnlockTimerRef.current); + autoTopUpUnlockTimerRef.current = null; + } } }); useSocketEvent("balance_updated", b => { setBalance(prev => (prev ? { ...prev, balance: b.data.balance } : null)); if (autoTopUpInProgressRef.current && b?.data?.balance > 0) { autoTopUpInProgressRef.current = false; + if (autoTopUpUnlockTimerRef.current) { + clearTimeout(autoTopUpUnlockTimerRef.current); + autoTopUpUnlockTimerRef.current = null; + } } }); useSocketEvent("session_started", s => setSession(s.data)); @@ -310,6 +326,16 @@ export const useChatSocket = ( }; }, [session, endSession]); + // Cleanup pending unlock timer on unmount + useEffect(() => { + return () => { + if (autoTopUpUnlockTimerRef.current) { + clearTimeout(autoTopUpUnlockTimerRef.current); + autoTopUpUnlockTimerRef.current = null; + } + }; + }, []); + const isAvailableChatting = !!balance?.balance && !!session && !isSessionExpired;