Store LNbits invoice data on payments, add an invoice endpoint that reuses valid invoices or regenerates expired ones, and wire the booking payment page to fetch and display invoices via a shared watcher hook. Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
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<typeof setTimeout> | 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]);
|
|
}
|