Fix Lightning payment UI not updating after LNbits invoice is paid.

Ensure the booking page receives payment confirmation via a streaming SSE proxy, parallel status polling, and more reliable backend event delivery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-06-25 04:15:02 +00:00
co-authored by Cursor
parent a6840ea953
commit f0e2de2834
4 changed files with 143 additions and 75 deletions
@@ -299,71 +299,75 @@ export default function BookingPage() {
);
};
// Connect to SSE for real-time payment updates
const connectPaymentStream = (ticketId: string) => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
const eventSource = new EventSource(`${apiUrl}/api/lnbits/stream/${ticketId}`);
// Watch for Lightning payment confirmation while on the paying step.
// 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.
useEffect(() => {
if (step !== 'paying' || !bookingResult?.ticketId) return;
const ticketId = bookingResult.ticketId;
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!');
setPaymentPending(false);
setStep('success');
};
const expire = () => {
if (settled) return;
settled = true;
toast.error(locale === 'es' ? 'La factura ha expirado' : 'Invoice has expired');
setPaymentPending(false);
};
// 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.data);
console.log('Payment event:', data);
if (data.type === 'paid') {
toast.success(locale === 'es' ? '¡Pago confirmado!' : 'Payment confirmed!');
setPaymentPending(false);
setStep('success');
eventSource.close();
const data = JSON.parse((event as MessageEvent).data);
if (data.type === 'paid' || data.type === 'already_paid') {
confirmPaid();
} else if (data.type === 'expired') {
toast.error(locale === 'es' ? 'La factura ha expirado' : 'Invoice has expired');
setPaymentPending(false);
eventSource.close();
} else if (data.type === 'already_paid') {
setPaymentPending(false);
setStep('success');
eventSource.close();
expire();
}
} catch (e) {
console.error('Error parsing payment event:', e);
}
});
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// Fallback to polling if SSE fails
eventSource.onerror = () => {
// SSE failed or was closed; the poll below remains the source of truth.
eventSource.close();
fallbackPoll(ticketId);
};
return eventSource;
};
// Fallback polling if SSE is not available
const fallbackPoll = async (ticketId: string) => {
const maxAttempts = 60;
let attempts = 0;
const poll = async () => {
attempts++;
try {
const status = await ticketsApi.checkPaymentStatus(ticketId);
if (status.isPaid) {
toast.success(locale === 'es' ? '¡Pago confirmado!' : 'Payment confirmed!');
setPaymentPending(false);
setStep('success');
confirmPaid();
return;
}
} catch (error) {
console.error('Error checking payment status:', error);
}
if (attempts < maxAttempts && paymentPending) {
setTimeout(poll, 5000);
if (!settled) {
pollTimer = setTimeout(poll, 3000);
}
};
poll();
};
pollTimer = setTimeout(poll, 3000);
return () => {
settled = true;
eventSource.close();
if (pollTimer) clearTimeout(pollTimer);
};
}, [step, bookingResult?.ticketId, locale]);
// Copy invoice to clipboard
const copyInvoiceToClipboard = (invoice: string) => {
@@ -464,9 +468,7 @@ export default function BookingPage() {
setBookingResult(result);
setStep('paying');
setPaymentPending(true);
// Connect to SSE for real-time payment updates
connectPaymentStream(primaryTicket.id);
// Payment confirmation is handled by the paying-step watcher effect.
} else if (formData.paymentMethod === 'bank_transfer' || formData.paymentMethod === 'tpago') {
// Manual payment methods - show payment details
setBookingResult({