import { createEffect, createResource, createSignal, Show } from "solid-js" import QRCode from "qrcode" import Modal from "@/components/Modal" import PaymentSetup from "@/components/PaymentSetup" import InvoiceItemsList from "@/components/payment/InvoiceItemsList" import LightningPayBody from "@/components/payment/LightningPayBody" import { setToastMessage } from "@/lib/state" import { copyToClipboard } from "@/lib/clipboard" import { useInvoiceCheckout } from "@/lib/usePaymentSetup" import { ensureInvoiceBolt11, listInvoiceItems, reconcileInvoice, type Invoice } from "@/lib/api" import { autopayConfigured } from "@/lib/paymentMethod" import { billingTenant } from "@/lib/state" import { formatUsd, formatPeriod } from "@/lib/format" type PayStatus = "idle" | "loading" | "success" | "error" type Bolt11Status = "idle" | "loading" | "ready" | "error" type PayMethod = "lightning" | "card" type PaymentInvoice = Pick & Partial> type PaymentDialogProps = { invoice: PaymentInvoice open: boolean onClose: () => void } export default function PaymentDialog(props: PaymentDialogProps) { const [bolt11, setBolt11] = createSignal("") const [qrDataUrl, setQrDataUrl] = createSignal("") const [bolt11Status, setBolt11Status] = createSignal("idle") const [bolt11Error, setBolt11Error] = createSignal("") const [payStatus, setPayStatus] = createSignal("idle") const [payMethod, setPayMethod] = createSignal("lightning") const [showPaymentSetup, setShowPaymentSetup] = createSignal(false) const [setupSaved, setSetupSaved] = createSignal(false) const [items] = createResource( () => (props.open ? props.invoice.id : undefined), listInvoiceItems, ) // Paying by card opens a Stripe Checkout session scoped to this invoice (which // can clear a 3D Secure challenge the off-session charge can't), then returns // here where the payment is reconciled. Distinct from PaymentSetup, which // manages the recurring card on file via the billing portal. const checkout = useInvoiceCheckout(() => props.invoice.id) const hasAutopay = () => { const t = billingTenant() return t ? autopayConfigured(t) : false } async function loadBolt11() { if (!props.invoice.id) return setBolt11Status("loading") setBolt11Error("") setBolt11("") setQrDataUrl("") try { const { lnbc } = await ensureInvoiceBolt11(props.invoice.id) setBolt11(lnbc) setQrDataUrl(await QRCode.toDataURL(lnbc, { width: 256, margin: 2 })) setBolt11Status("ready") } catch (e) { setBolt11Status("error") setBolt11Error(e instanceof Error ? e.message : "Failed to generate Lightning invoice") } } createEffect(() => { if (!props.open || !props.invoice.id) return void loadBolt11() }) // The checkout redirect lives in a shared hook, so surface its failures here // by mirroring its error signal into the toast. createEffect(() => { const err = checkout.error() if (err) setToastMessage(err) }) function copyBolt11() { void copyToClipboard(bolt11(), { successMessage: "Invoice copied" }) } async function checkPayment() { setPayStatus("loading") try { const invoice = await reconcileInvoice(props.invoice.id) if (invoice.paid_at != null) { setPayStatus("success") } else { setPayStatus("error") setToastMessage("Payment not yet confirmed. Please try again after sending.") } } catch (e) { setPayStatus("error") setToastMessage(e instanceof Error ? e.message : "Failed to check payment status") } } function handleClose() { setPayStatus("idle") setBolt11Status("idle") setBolt11Error("") setBolt11("") setQrDataUrl("") setPayMethod("lightning") checkout.reset() props.onClose() } const amountLabel = () => formatUsd(props.invoice.amount) const periodLabel = () => formatPeriod(props.invoice.period_start, props.invoice.period_end) return ( <> {/* Header */}

Pay Invoice

{amountLabel()}

Billing period {periodLabel()}

{/* Content */}
{/* What's being paid for — the invoice's actual line items */} 0}> {/* Method switcher */}
{/* Lightning: pay this invoice via a bolt11 QR */} void loadBolt11()} onCopy={copyBolt11} /> {/* Card: redirect to a Stripe Checkout session for this invoice */}

Pay this invoice on Stripe's secure checkout. You'll be redirected and brought back here once it's done.

} >

Payment confirmed!

Thank you. Your account is up to date.

{/* Footer */}
} >
{ setShowPaymentSetup(false) if (setupSaved()) { setSetupSaved(false) props.onClose() } }} onSaved={() => setSetupSaved(true)} /> ) }