import type { Metadata } from "next"; import BtcpayModalBridge from "../../../components/btcpay-modal-bridge"; import BtcpayAutoReturn from "../../../components/btcpay-auto-return"; import BtcpayClassicCheckout from "../../../components/btcpay-classic-checkout"; import InvoicePaymentDetails from "../../../components/invoice-payment-details"; import InvoiceStatusAutoRefresh from "../../../components/invoice-status-auto-refresh"; import { formatRelativeTime } from "../../../components/relative-time"; import StatusRefreshButton from "../../../components/status-refresh-button"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { title: "Payment Request", }; const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:8000"; type InvoiceStatus = | "pending" | "payment_detected" | "confirmed" | "expired" | "invalid"; type InvoiceStatusResponse = { id: string; address: string; subaddress_index: number | null; amount_xmr: string; amount_paid_xmr?: string | null; status: InvoiceStatus; confirmation_target: number; confirmations: number; checkout_continue_available?: boolean; created_at: string; expires_at: string | null; detected_at: string | null; confirmed_at: string | null; btcpay_amount?: string | null; btcpay_currency?: string | null; btcpay_checkout_style?: "standard" | "btcpay_classic" | null; btcpay_redirect_url?: string | null; btcpay_redirect_automatically?: boolean | null; btcpay_order_id?: string | null; btcpay_order_number?: string | null; qr_logo?: "monero" | "none" | "custom" | null; qr_logo_data_url?: string | null; quote?: { fiat_amount: string; fiat_currency: string; rate: string; source: string; quoted_at: string; } | null; }; type SystemStatusResponse = { wallet_rpc: "ok" | "unreachable"; daemon: "ok" | "unreachable" | "unknown"; daemon_height: number | null; invoice_reconcile_interval_seconds: number; last_reconcile_started_at: string | null; last_reconcile_completed_at: string | null; last_reconcile_error: string | null; }; const walletRpcBadge = (systemStatus: SystemStatusResponse | null) => { if (!systemStatus) { return { className: "bg-amber-100 text-amber-900", label: "Wallet RPC unavailable", }; } if (systemStatus.wallet_rpc === "ok") { return { className: "bg-emerald-100 text-emerald-900", label: "Wallet RPC ok", }; } return { className: "bg-red-100 text-red-700", label: "Wallet RPC down", }; }; const daemonBadge = (systemStatus: SystemStatusResponse | null) => { if (!systemStatus) { return { className: "bg-amber-100 text-amber-900", label: "Daemon unavailable", }; } if (systemStatus.daemon === "ok") { return { className: "bg-emerald-100 text-emerald-900", label: "Daemon connected", }; } if (systemStatus.daemon === "unknown") { return { className: "bg-amber-100 text-amber-900", label: "Daemon not configured", }; } return { className: "bg-red-100 text-red-700", label: "Daemon down", }; }; const formatStatus = (status: InvoiceStatus) => { if (status === "payment_detected") { return "Payment detected"; } if (status === "pending") { return "Awaiting funds"; } if (status === "confirmed") { return "Confirmed"; } if (status === "invalid") { return "Invalid"; } return "Expired"; }; const formatTimestamp = (value: string | null) => { if (!value) { return { label: "-", relative: null }; } return { label: new Date(value).toLocaleString(), relative: formatRelativeTime(value), }; }; const statusPillStyles: Record = { pending: "bg-amber-100 text-amber-900", payment_detected: "bg-emerald-100 text-emerald-900", confirmed: "bg-ink/20 text-ink", expired: "bg-red-100 text-red-700", invalid: "bg-clay/15 text-clay", }; export default async function BtcpayModalInvoicePage({ params, }: { params: Promise<{ invoiceId: string }>; }) { const { invoiceId } = await params; const [response, systemStatusResponse] = await Promise.all([ fetch(`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`, { cache: "no-store", }), fetch(`${apiBaseUrl}/api/core/public/system/status`, { cache: "no-store" }), ]); if (response.status === 404) { return (

Payment request

Invoice not found

The invoice id does not match a known invoice. Check the id and try again.

); } if (!response.ok) { return (

Payment request

Status unavailable

We could not load this invoice status. Refresh the page or try again later.

); } const invoice = (await response.json()) as InvoiceStatusResponse; const systemStatus = systemStatusResponse.ok ? ((await systemStatusResponse.json()) as SystemStatusResponse) : null; const statusLabel = formatStatus(invoice.status); const confirmations = Math.max(0, invoice.confirmations ?? 0); const confirmationTarget = Math.max(0, invoice.confirmation_target); const hasDetectedPayment = invoice.status === "payment_detected" || invoice.status === "confirmed"; const createdTimestamp = formatTimestamp(invoice.created_at); const expiresTimestamp = formatTimestamp(invoice.expires_at); const lastReconcileCompleted = formatTimestamp(systemStatus?.last_reconcile_completed_at ?? null); const walletStatusBadge = walletRpcBadge(systemStatus); const daemonStatusBadge = daemonBadge(systemStatus); const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency); const useClassicCheckout = isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic"; // The merchant's own order page is where a customer belongs once the payment has been seen, so // the way back appears at detection. The receipt still waits for the confirmation target, because // that is the point at which there is a settled payment to write one about. const btcpayPaymentSeen = isBtcpayInvoice && (invoice.status === "payment_detected" || invoice.status === "confirmed"); const shouldShowBtcpayActions = btcpayPaymentSeen; const checkoutContinueAvailable = !isBtcpayInvoice && invoice.status === "confirmed" && Boolean(invoice.checkout_continue_available); return (
{useClassicCheckout ? (

Payment request

) : ( <>

Payment request

Payment request

Status {statusLabel}

Confirmations

{confirmations}/{confirmationTarget}

Created

{createdTimestamp.label}

Expires

{expiresTimestamp.label}

Monero node

{walletStatusBadge.label} {daemonStatusBadge.label}

Current block height

{systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}

Detection poll

{systemStatus?.invoice_reconcile_interval_seconds ?? 30}s

Last successful scan

{lastReconcileCompleted.label}

{systemStatus?.last_reconcile_error ? (

Reconciler error: {systemStatus.last_reconcile_error}

) : null}
{checkoutContinueAvailable ? ( Continue to merchant ) : null}
{shouldShowBtcpayActions ? (
{invoice.status === "confirmed" ? ( View receipt ) : null} {invoice.btcpay_redirect_url ? ( Return to store ) : null}
) : null} )}
); }