import { useEffect } from 'react'; import toast from 'react-hot-toast'; import { ticketsApi } from '@/lib/api'; /** * Watch for Lightning payment confirmation while an invoice is on screen. * SSE gives instant updates; a 3s poll runs in parallel as a safety net so a * buffered/stuck stream (e.g. a proxy that doesn't flush SSE) can't strand the UI. */ export function useLightningWatcher( active: boolean, ticketId: string | undefined, locale: string, onPaid: () => void, onExpired: () => void ) { useEffect(() => { if (!active || !ticketId) return; let settled = false; let pollTimer: ReturnType | null = null; const confirmPaid = () => { if (settled) return; settled = true; toast.success(locale === 'es' ? '¡Pago confirmado!' : 'Payment confirmed!'); onPaid(); }; const expire = () => { if (settled) return; settled = true; toast.error(locale === 'es' ? 'La factura ha expirado' : 'Invoice has expired'); onExpired(); }; // Always same-origin so the streaming proxy route handler is used (it // bypasses the rewrite, which buffers SSE). const eventSource = new EventSource(`/api/lnbits/stream/${ticketId}`); eventSource.addEventListener('payment', (event) => { try { const data = JSON.parse((event as MessageEvent).data); if (data.type === 'paid' || data.type === 'already_paid') { confirmPaid(); } else if (data.type === 'expired') { expire(); } } catch (e) { console.error('Error parsing payment event:', e); } }); eventSource.onerror = () => { // SSE failed or was closed; the poll below remains the source of truth. eventSource.close(); }; const poll = async () => { try { const status = await ticketsApi.checkPaymentStatus(ticketId); if (status.isPaid) { confirmPaid(); return; } } catch (error) { console.error('Error checking payment status:', error); } if (!settled) { pollTimer = setTimeout(poll, 3000); } }; pollTimer = setTimeout(poll, 3000); return () => { settled = true; eventSource.close(); if (pollTimer) clearTimeout(pollTimer); }; }, [active, ticketId, locale]); }