"use client"; import Image from "next/image"; import { useEffect, useMemo, useState } from "react"; import QRCode from "qrcode"; import { formatXmrAmount } from "../lib/formatting"; type InvoiceStatus = | "pending" | "payment_detected" | "confirmed" | "expired" | "invalid"; type QuotePayload = { fiat_amount: string; fiat_currency: string; rate: string; source: string; quoted_at: string; }; type BtcpayClassicCheckoutProps = { invoiceId: string; address: string; amountXmr: string; amountPaidXmr: string | null; btcpayAmount: string | null; btcpayCurrency: string | null; quote: QuotePayload | null; status: InvoiceStatus; confirmationTarget: number; redirectUrl: string | null; redirectAutomatically: boolean | null; orderId: string | null; orderNumber: string | null; qrLogoMode?: "monero" | "none" | "custom" | null; qrLogoDataUrl?: string | null; }; const buildMoneroUri = (address: string, amount: string) => { const params = new URLSearchParams(); params.set("tx_amount", amount); return `wownero:${address}?${params.toString()}`; }; const formatFiatAmount = (value: string, currency: string) => { const amount = Number.parseFloat(value); if (!Number.isFinite(amount)) { return `${value} ${currency}`; } try { return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 2, minimumFractionDigits: 2, }).format(amount); } catch { return `${amount.toFixed(2)} ${currency}`; } }; const formatReturnLabel = (redirectUrl: string) => { try { const parsed = new URL(redirectUrl); return parsed.hostname ? `Return to ${parsed.hostname}` : "Return to store"; } catch { return "Return to store"; } }; const statusMessage = ( status: InvoiceStatus, confirmationTarget: number ): string => { if (status === "invalid") { return "Invoice marked invalid. Do not send payment."; } if (status === "expired") { return "Invoice expired. Do not send payment."; } if (status === "confirmed") { return `Payment confirmed at ${confirmationTarget} confirmations.`; } if (status === "payment_detected") { return "Payment detected. Awaiting confirmations."; } return "Awaiting funds."; }; export default function BtcpayClassicCheckout({ invoiceId, address, amountXmr, amountPaidXmr, btcpayAmount, btcpayCurrency, quote, status, confirmationTarget, redirectUrl, redirectAutomatically, orderId, orderNumber, qrLogoMode, qrLogoDataUrl, }: BtcpayClassicCheckoutProps) { const [qrDataUrl, setQrDataUrl] = useState(null); const [copiedField, setCopiedField] = useState(null); const formattedAmount = useMemo(() => formatXmrAmount(amountXmr), [amountXmr]); const formattedPaidAmount = useMemo(() => { if (!amountPaidXmr) { return formattedAmount; } return formatXmrAmount(amountPaidXmr); }, [amountPaidXmr, formattedAmount]); const uri = useMemo( () => buildMoneroUri(address, formattedAmount), [address, formattedAmount] ); const totalFiat = btcpayAmount && btcpayCurrency && btcpayCurrency !== "WOW" ? formatFiatAmount(btcpayAmount, btcpayCurrency) : null; const exchangeRate = quote?.rate && quote.fiat_currency ? `1 WOW = ${formatFiatAmount(quote.rate, quote.fiat_currency)}` : null; useEffect(() => { if ((status !== "confirmed" && status !== "payment_detected") || !redirectUrl || !redirectAutomatically) { return; } const timer = window.setTimeout(() => { try { window.top?.location.assign(redirectUrl); } catch { window.location.assign(redirectUrl); } }, 3000); return () => { window.clearTimeout(timer); }; }, [redirectAutomatically, redirectUrl, status]); const handleCopy = async (value: string, field: string) => { try { await navigator.clipboard.writeText(value); setCopiedField(field); window.setTimeout(() => { setCopiedField((current) => (current === field ? null : current)); }, 1500); } catch { setCopiedField(null); } }; useEffect(() => { let active = true; const size = 260; const canvas = document.createElement("canvas"); QRCode.toCanvas(canvas, uri, { margin: 1, width: size, errorCorrectionLevel: "H", }) .then(() => { const context = canvas.getContext("2d"); if (!context) { return null; } const mode = qrLogoMode ?? "monero"; const wantsLogo = mode !== "none"; if (wantsLogo) { const blankSize = Math.round(size * 0.28); const blankOffset = Math.round((size - blankSize) / 2); context.fillStyle = "#ffffff"; context.fillRect(blankOffset, blankOffset, blankSize, blankSize); } return canvas.toDataURL(); }) .then((url) => { if (active) { setQrDataUrl(url ?? null); } }) .catch(() => { if (active) { setQrDataUrl(null); } }); return () => { active = false; }; }, [uri, qrLogoMode]); const isActionDisabled = status === "expired" || status === "invalid"; const resolvedQrLogoMode = qrLogoMode ?? "monero"; const resolvedLogoSrc = resolvedQrLogoMode === "custom" ? qrLogoDataUrl : resolvedQrLogoMode === "monero" ? "/wownero-logo.svg" : null; if (status === "confirmed") { return (

Invoice paid

Invoice id
{invoiceId}
{orderNumber || orderId ? (
Order id
{orderNumber ?? orderId}
) : null}
Total price
{formattedAmount} WOW
Total fiat
{totalFiat ?? "-"}
Exchange rate
{exchangeRate ?? "-"}
Network cost
Added by wallet
Amount paid
{formattedPaidAmount} WOW
View receipt {redirectUrl ? ( {formatReturnLabel(redirectUrl)} ) : null}
); } return (

Payment request

{formattedAmount} WOW

{statusMessage(status, confirmationTarget)}

View details Hide details
Total price
{formattedAmount} WOW
Total fiat
{totalFiat ?? "-"}
Exchange rate
{exchangeRate ?? "-"}
Network cost
Added by wallet
Amount due
{formattedAmount} WOW
{qrDataUrl ? (
Payment request QR {resolvedLogoSrc ? (
QR logo
) : null}
) : null}

Address

{address}

Pay in wallet
); }