Add sponsors system with time slider, LNbits invoices, and UX improvements
- Sponsors table, LNbits createInvoice, webhook handler - Sponsor routes: create, homepage, list, my-ads, click, extend, check-payment - Admin routes for sponsor management - Frontend: SponsorForm, SponsorTimeSlider, SponsorCard, SponsorsSection - Sponsors page, My Ads page, homepage sponsor block - Header login dropdown with My Ads, Create Sponsor - Transactions integration for sponsor payments - View/click tracking - OG meta fetch for sponsor images - Sponsor modal spacing, invoice polling fallback - Remove Lightning address and Category fields from sponsor form Made-with: Cursor
This commit is contained in:
98
frontend/src/components/SponsorInvoiceModal.tsx
Normal file
98
frontend/src/components/SponsorInvoiceModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import QRCode from "qrcode";
|
||||
import { Modal } from "./Modal";
|
||||
import { getSponsorCheckPayment } from "../api";
|
||||
|
||||
const POLL_INTERVAL_MS = 10_000;
|
||||
const INVOICE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
export interface PendingInvoice {
|
||||
payment_hash: string;
|
||||
payment_request: string;
|
||||
price_sats: number;
|
||||
duration_days: number;
|
||||
}
|
||||
|
||||
interface SponsorInvoiceModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
result: PendingInvoice | null;
|
||||
/** Called when payment is detected (e.g. to refresh sponsor list). */
|
||||
onPaid?: () => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function SponsorInvoiceModal({ open, onClose, result, onPaid, title }: SponsorInvoiceModalProps) {
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const onPaidRef = useRef(onPaid);
|
||||
const onCloseRef = useRef(onClose);
|
||||
onPaidRef.current = onPaid;
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
if (!result?.payment_request) {
|
||||
setQrDataUrl(null);
|
||||
return;
|
||||
}
|
||||
QRCode.toDataURL(result.payment_request, { width: 200 }).then(setQrDataUrl).catch(() => setQrDataUrl(null));
|
||||
}, [result?.payment_request]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !result?.payment_hash) return;
|
||||
const openedAt = Date.now();
|
||||
let cancelled = false;
|
||||
|
||||
const check = async () => {
|
||||
if (cancelled) return;
|
||||
if (Date.now() - openedAt > INVOICE_EXPIRY_MS) return;
|
||||
try {
|
||||
const { paid: isPaid } = await getSponsorCheckPayment(result!.payment_hash);
|
||||
if (cancelled) return;
|
||||
if (isPaid) {
|
||||
onPaidRef.current?.();
|
||||
onCloseRef.current();
|
||||
}
|
||||
} catch {
|
||||
// Ignore; will retry on next poll
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
const interval = setInterval(check, POLL_INTERVAL_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [open, result?.payment_hash]);
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!result?.payment_request) return;
|
||||
navigator.clipboard.writeText(result.payment_request).then(() => {
|
||||
// Could show a toast
|
||||
});
|
||||
};
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={title ?? "Pay to activate sponsor"} variant="sponsor">
|
||||
<div className="sponsor-invoice">
|
||||
<p className="sponsor-invoice-desc">
|
||||
Scan or copy the Lightning invoice to pay <strong>{result.price_sats.toLocaleString()} sats</strong>.
|
||||
Your sponsor will activate after payment.
|
||||
</p>
|
||||
{qrDataUrl && (
|
||||
<div className="sponsor-invoice-qr">
|
||||
<img src={qrDataUrl} alt="Lightning invoice QR" />
|
||||
</div>
|
||||
)}
|
||||
<button type="button" className="sponsor-invoice-copy" onClick={handleCopy}>
|
||||
Copy invoice
|
||||
</button>
|
||||
<p className="sponsor-invoice-note">
|
||||
Duration: {result.duration_days} days.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user