Allow Lightning invoice reuse and re-payment from the dashboard.
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>
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { ticketsApi } from '@/lib/api';
|
||||
import type { BookingStep } from '../_types';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function useLightningWatcher(
|
||||
step: BookingStep,
|
||||
ticketId: string | undefined,
|
||||
locale: string,
|
||||
setPaymentPending: (value: boolean) => void,
|
||||
setStep: (value: BookingStep) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (step !== 'paying' || !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!');
|
||||
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 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);
|
||||
};
|
||||
}, [step, ticketId, locale]);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
PaymentMethod,
|
||||
} from './_types';
|
||||
import { buildPaymentMethods, formatRuc, rucPattern } from './_logic/booking';
|
||||
import { useLightningWatcher } from './_hooks/useLightningWatcher';
|
||||
import { useLightningWatcher } from '@/hooks/useLightningWatcher';
|
||||
import { PayingStep } from './_steps/PayingStep';
|
||||
import { ManualPaymentStep } from './_steps/ManualPaymentStep';
|
||||
import { PendingApprovalStep } from './_steps/PendingApprovalStep';
|
||||
@@ -35,7 +35,6 @@ export default function BookingPage() {
|
||||
const [step, setStep] = useState<BookingStep>('form');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [bookingResult, setBookingResult] = useState<BookingResult | null>(null);
|
||||
const [, setPaymentPending] = useState(false);
|
||||
const [markingPaid, setMarkingPaid] = useState(false);
|
||||
|
||||
// State for payer name (when paid under different name)
|
||||
@@ -245,7 +244,13 @@ export default function BookingPage() {
|
||||
};
|
||||
|
||||
// Watch for Lightning payment confirmation while on the paying step.
|
||||
useLightningWatcher(step, bookingResult?.ticketId, locale, setPaymentPending, setStep);
|
||||
useLightningWatcher(
|
||||
step === 'paying',
|
||||
bookingResult?.ticketId,
|
||||
locale,
|
||||
() => setStep('success'),
|
||||
() => {}
|
||||
);
|
||||
|
||||
// Handle "I Have Paid" button click
|
||||
const handleMarkPaymentSent = async () => {
|
||||
@@ -330,7 +335,6 @@ export default function BookingPage() {
|
||||
};
|
||||
setBookingResult(result);
|
||||
setStep('paying');
|
||||
setPaymentPending(true);
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user