forked from coracle/caravel
feat(frontend): handle bolt11 generation failures in payment dialog (#11)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com> Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { getInvoice, getInvoiceBolt11 } from "@/lib/api"
|
|||||||
import { tenantNeedsPaymentSetup } from "@/lib/hooks"
|
import { tenantNeedsPaymentSetup } from "@/lib/hooks"
|
||||||
|
|
||||||
type PayStatus = "idle" | "loading" | "success" | "error"
|
type PayStatus = "idle" | "loading" | "success" | "error"
|
||||||
|
type Bolt11Status = "idle" | "loading" | "ready" | "error"
|
||||||
|
|
||||||
type PaymentInvoice = {
|
type PaymentInvoice = {
|
||||||
id: string
|
id: string
|
||||||
@@ -21,20 +22,34 @@ type PaymentDialogProps = {
|
|||||||
export default function PaymentDialog(props: PaymentDialogProps) {
|
export default function PaymentDialog(props: PaymentDialogProps) {
|
||||||
const [bolt11, setBolt11] = createSignal("")
|
const [bolt11, setBolt11] = createSignal("")
|
||||||
const [qrDataUrl, setQrDataUrl] = createSignal("")
|
const [qrDataUrl, setQrDataUrl] = createSignal("")
|
||||||
|
const [bolt11Status, setBolt11Status] = createSignal<Bolt11Status>("idle")
|
||||||
|
const [bolt11Error, setBolt11Error] = createSignal("")
|
||||||
const [payStatus, setPayStatus] = createSignal<PayStatus>("idle")
|
const [payStatus, setPayStatus] = createSignal<PayStatus>("idle")
|
||||||
const [payError, setPayError] = createSignal("")
|
const [payError, setPayError] = createSignal("")
|
||||||
const [showSetup, setShowSetup] = createSignal(false)
|
const [showSetup, setShowSetup] = createSignal(false)
|
||||||
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
||||||
|
|
||||||
createEffect(async () => {
|
async function loadBolt11() {
|
||||||
if (!props.open || !props.invoice.id) return
|
if (!props.invoice.id) return
|
||||||
|
setBolt11Status("loading")
|
||||||
|
setBolt11Error("")
|
||||||
|
setBolt11("")
|
||||||
|
setQrDataUrl("")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { bolt11: invoice } = await getInvoiceBolt11(props.invoice.id)
|
const { bolt11: invoice } = await getInvoiceBolt11(props.invoice.id)
|
||||||
setBolt11(invoice)
|
setBolt11(invoice)
|
||||||
setQrDataUrl(await QRCode.toDataURL(invoice, { width: 256, margin: 2 }))
|
setQrDataUrl(await QRCode.toDataURL(invoice, { width: 256, margin: 2 }))
|
||||||
} catch {
|
setBolt11Status("ready")
|
||||||
// bolt11 generation may fail
|
} 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()
|
||||||
})
|
})
|
||||||
|
|
||||||
function copyBolt11() {
|
function copyBolt11() {
|
||||||
@@ -62,6 +77,8 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
function handleClose() {
|
function handleClose() {
|
||||||
setPayStatus("idle")
|
setPayStatus("idle")
|
||||||
setPayError("")
|
setPayError("")
|
||||||
|
setBolt11Status("idle")
|
||||||
|
setBolt11Error("")
|
||||||
setBolt11("")
|
setBolt11("")
|
||||||
setQrDataUrl("")
|
setQrDataUrl("")
|
||||||
setShowSetup(false)
|
setShowSetup(false)
|
||||||
@@ -104,33 +121,46 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
when={payStatus() === "success"}
|
when={payStatus() === "success"}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="w-full space-y-3">
|
<div class="w-full space-y-3">
|
||||||
<Show
|
<Show when={bolt11Status() === "idle" || bolt11Status() === "loading"}>
|
||||||
when={qrDataUrl()}
|
<div class="flex items-center justify-center py-12 text-sm text-gray-400">Generating invoice...</div>
|
||||||
fallback={<div class="flex items-center justify-center py-12 text-sm text-gray-400">Generating invoice...</div>}
|
|
||||||
>
|
|
||||||
<img src={qrDataUrl()} alt="Lightning invoice QR code" class="mx-auto rounded-lg" />
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={bolt11()}>
|
<Show when={bolt11Status() === "error"}>
|
||||||
<div class="flex rounded-lg border border-gray-300">
|
<div class="rounded-lg border border-red-200 bg-red-50 p-4">
|
||||||
<input
|
<p class="text-sm font-medium text-red-700">Unable to generate invoice</p>
|
||||||
type="text"
|
<p class="mt-1 text-xs text-red-600 wrap-break-word">{bolt11Error()}</p>
|
||||||
readOnly
|
|
||||||
value={bolt11()}
|
|
||||||
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-xs text-gray-500 bg-transparent focus:outline-none"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center px-3 text-gray-400 hover:text-gray-700"
|
onClick={() => void loadBolt11()}
|
||||||
onClick={copyBolt11}
|
class="mt-3 inline-flex items-center rounded-lg bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700"
|
||||||
title="Copy invoice"
|
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
Retry
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
||||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={bolt11Status() === "ready"}>
|
||||||
|
<img src={qrDataUrl()} alt="Lightning invoice QR code" class="mx-auto rounded-lg" />
|
||||||
|
<Show when={bolt11()}>
|
||||||
|
<div class="flex rounded-lg border border-gray-300">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={bolt11()}
|
||||||
|
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-xs text-gray-500 bg-transparent focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center px-3 text-gray-400 hover:text-gray-700"
|
||||||
|
onClick={copyBolt11}
|
||||||
|
title="Copy invoice"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||||
|
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -188,7 +218,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={checkPayment}
|
onClick={checkPayment}
|
||||||
disabled={payStatus() === "loading"}
|
disabled={payStatus() === "loading" || bolt11Status() !== "ready"}
|
||||||
class="py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
class="py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
{payStatus() === "loading" ? "Checking..." : "Complete Payment"}
|
{payStatus() === "loading" ? "Checking..." : "Complete Payment"}
|
||||||
|
|||||||
Reference in New Issue
Block a user