"use client"; import Image from "next/image"; import { useEffect, useMemo, useState } from "react"; import QRCode from "qrcode"; import { formatUsdAmount, formatXmrAmount } from "../lib/formatting"; import { useXmrUsdRate } from "../lib/use-xmr-usd-rate"; type InvoicePaymentDetailsProps = { address: string; amount: string; hasDetectedPayment: boolean; status: "pending" | "payment_detected" | "confirmed" | "expired" | "invalid"; confirmationTarget: number; qrLogoMode?: "monero" | "none" | "custom" | null; qrLogoDataUrl?: string | null; }; const buildMoneroUri = (address: string, amount: string) => { const params = new URLSearchParams(); params.set("tx_amount", amount); return `monero:${address}?${params.toString()}`; }; export default function InvoicePaymentDetails({ address, amount, hasDetectedPayment, status, confirmationTarget, qrLogoMode, qrLogoDataUrl, }: InvoicePaymentDetailsProps) { const [qrDataUrl, setQrDataUrl] = useState(null); const [copiedField, setCopiedField] = useState(null); const { rate: usdRate } = useXmrUsdRate(); const isInvalid = status === "invalid"; const shouldPromptPayment = !hasDetectedPayment && !isInvalid; const formattedAmount = useMemo(() => formatXmrAmount(amount), [amount]); const uri = useMemo( () => buildMoneroUri(address, formattedAmount), [address, formattedAmount] ); const amountValue = useMemo(() => { const parsed = Number.parseFloat(amount); return Number.isFinite(parsed) ? parsed : null; }, [amount]); const usdEstimate = useMemo(() => { if (!usdRate || amountValue === null || amountValue <= 0) { return null; } return formatUsdAmount(usdRate * amountValue); }, [usdRate, amountValue]); 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 = 200; 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 resolvedQrLogoMode = qrLogoMode ?? "monero"; const resolvedLogoSrc = resolvedQrLogoMode === "custom" ? qrLogoDataUrl : resolvedQrLogoMode === "monero" ? "/monero-logo.svg" : null; return (

Payment details

{isInvalid ? "Invoice marked invalid. Do not send payment." : hasDetectedPayment ? status === "confirmed" ? `Payment confirmed on-chain at ${confirmationTarget} confirmations.` : "Payment detected. Confirmations are in progress." : "Send the exact amount to this address."}

Show details Hide details

Amount (XMR)

{formattedAmount}

Your wallet adds the network fee on top. Do not subtract it from the amount shown.

{usdEstimate ? ( <>

Approx. USD reference: ~{usdEstimate}

About this estimate

Reference only, uses CoinGecko spot rate. Not a quote or guarantee.

) : null}

Payment address

{address}

Payment request URI

{uri}

{shouldPromptPayment ? (

After you pay, this page updates automatically once the payment is detected on-chain.

) : null}

{isInvalid ? "This invoice is marked invalid. Do not send a payment to this address." : hasDetectedPayment ? "Details remain available if you need to reference the address or request URI." : "Tip: use the QR or payment request URI to avoid copy errors."}

{qrDataUrl ? (
Payment request QR {resolvedLogoSrc ? (
QR logo
) : null}
) : null}
); }