"use client"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useFormState } from "react-dom"; import QRCode from "qrcode"; import { createInvoiceAction, type InvoiceState, } from "../app/(app)/dashboard/actions"; import { formatUsdAmount, formatXmrAmount } from "../lib/formatting"; import { useXmrUsdRate } from "../lib/use-xmr-usd-rate"; import { useXmrFiatRate } from "../lib/use-xmr-fiat-rate"; import { FIAT_CURRENCY_SUGGESTIONS, getCurrencyFlag, } from "../lib/fiat-currencies"; const initialState: InvoiceState = { error: null, invoiceId: null, address: null, amount: null, recipientName: null, description: null, subaddressIndex: null, warnings: null, }; const buildMoneroUri = (state: InvoiceState): string | null => { if (!state.address || !state.amount) { return null; } const params = new URLSearchParams(); params.set("tx_amount", state.amount); if (state.recipientName) { params.set("recipient_name", state.recipientName); } if (state.description) { params.set("tx_description", state.description); } return `wownero:${state.address}?${params.toString()}`; }; export default function CreateInvoiceCard() { const [state, formAction] = useFormState(createInvoiceAction, initialState); const [qrDataUrl, setQrDataUrl] = useState(null); const [qrColor, setQrColor] = useState("#1b1b1a"); const [qrLogo, setQrLogo] = useState(null); const [origin, setOrigin] = useState(null); const [amountMode, setAmountMode] = useState<"xmr" | "fiat">("xmr"); const [amountInput, setAmountInput] = useState(""); const [fiatCurrency, setFiatCurrency] = useState("USD"); const { rate: usdRate } = useXmrUsdRate(); const { rate: fiatRate, status: fiatRateStatus } = useXmrFiatRate( amountMode === "fiat" ? fiatCurrency : null ); const labelClass = "text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft"; const inputClass = "w-full rounded-xl border border-stroke bg-white/80 px-4 py-3 text-sm text-ink shadow-[inset_0_1px_0_rgba(255,255,255,0.6)] outline-none transition focus:border-ink/40 focus:ring-2 focus:ring-ink/10"; const secondaryButton = "inline-flex items-center justify-center rounded-full border border-stroke bg-white/60 px-5 py-2.5 text-sm font-semibold text-ink transition hover:-translate-y-0.5"; useEffect(() => { setOrigin(window.location.origin); }, []); const publicInvoiceUrl = useMemo(() => { if (!state.invoiceId) { return null; } if (origin) { return new URL(`/invoice/${state.invoiceId}`, origin).toString(); } return `/invoice/${state.invoiceId}`; }, [origin, state.invoiceId]); const uri = useMemo(() => buildMoneroUri(state), [state]); const amountValue = useMemo(() => { if (amountMode !== "xmr") { return null; } const raw = amountInput || state.amount || ""; const parsed = Number.parseFloat(raw); return Number.isFinite(parsed) ? parsed : null; }, [amountInput, amountMode, state.amount]); const usdEstimate = useMemo(() => { if (!usdRate || amountValue === null || amountValue <= 0) { return null; } return formatUsdAmount(usdRate * amountValue); }, [usdRate, amountValue]); const fiatAmountValue = useMemo(() => { if (amountMode !== "fiat") { return null; } const parsed = Number.parseFloat(amountInput); return Number.isFinite(parsed) ? parsed : null; }, [amountInput, amountMode]); const xmrEstimate = useMemo(() => { if (!fiatRate || fiatAmountValue === null || fiatAmountValue <= 0) { return null; } const estimate = fiatAmountValue / fiatRate; return formatXmrAmount(estimate.toFixed(12)); }, [fiatAmountValue, fiatRate]); const showFiatEstimate = amountMode === "fiat" && fiatAmountValue !== null && fiatAmountValue > 0; const fiatEstimateLabel = xmrEstimate ? `~${xmrEstimate} WOW` : fiatRateStatus === "loading" ? "Fetching estimate..." : "Estimate unavailable"; const fiatCurrencyFlag = getCurrencyFlag(fiatCurrency); useEffect(() => { let active = true; if (!uri) { setQrDataUrl(null); return undefined; } QRCode.toDataURL(uri, { margin: 1, width: 220, errorCorrectionLevel: "H", color: { dark: qrColor, light: "#ffffff", }, }) .then((url) => { if (active) { setQrDataUrl(url); } }) .catch(() => { if (active) { setQrDataUrl(null); } }); return () => { active = false; }; }, [uri, qrColor]); const handleLogoChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { setQrLogo(null); return; } const reader = new FileReader(); reader.onload = () => { const result = reader.result; if (typeof result === "string") { setQrLogo(result); } }; reader.readAsDataURL(file); }; return (

Create test invoice

Generate an invoice address and draft invoice for validation.

{amountMode === "xmr" ? (
setAmountInput(event.target.value)} /> {usdEstimate ? ( <>

Approx. USD reference: ~{usdEstimate}

About this estimate

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

) : null}
) : (
setAmountInput(event.target.value)} />
setFiatCurrency(event.target.value.toUpperCase())} required />
{FIAT_CURRENCY_SUGGESTIONS.map((code) => (

Fiat inputs create a WOW invoice using a non-binding rate at request time.

{showFiatEstimate ? (

Approx. WOW value: {fiatEstimateLabel} {fiatCurrencyFlag ? ` ยท ${fiatCurrencyFlag} ${fiatCurrency}` : ""}

{xmrEstimate ? (
About this estimate

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

) : null}
) : null}
)}

Expiry defaults to 60 minutes.

Leave blank to keep the default 60-minute expiry.

After confirmation, the hosted invoice page shows a "Continue" button that navigates to this URL.

setQrColor(event.target.value)} aria-label="QR accent color" />

Logo overlays the QR center for branding.

{qrLogo ? ( ) : null}
{state.error ? (

{state.error}

) : null} {state.invoiceId ? (

Invoice created

{state.invoiceId} {publicInvoiceUrl ? ( <>

Status link

{publicInvoiceUrl} ) : null}

Address

{state.address} {state.subaddressIndex !== null ? ( <>

Subaddress index

{state.subaddressIndex} ) : null} {state.warnings && state.warnings.length > 0 ? (
{state.warnings.map((warning) => (

{warning}

))}
) : null} {uri ? ( <>

Payment request

{uri} ) : null} {qrDataUrl ? (
Payment request QR {qrLogo ? ( Logo overlay ) : null}
) : null}
) : null}
); }