From b8a86c0c252ce998e4939fcc1e32895377307d9f Mon Sep 17 00:00:00 2001 From: xmrcheckout Date: Mon, 5 Jan 2026 10:27:45 +0000 Subject: [PATCH] Add fiat invoice inputs to UI --- ui/app/(app)/dashboard/actions.ts | 30 +++- ui/components/create-invoice-card.tsx | 180 +++++++++++++++---- ui/components/invoice-panel.tsx | 238 ++++++++++++++++++++++---- ui/lib/fiat-currencies.ts | 54 ++++++ ui/lib/use-xmr-fiat-rate.ts | 103 +++++++++++ ui/package-lock.json | 95 +++++++--- ui/package.json | 5 +- 7 files changed, 614 insertions(+), 91 deletions(-) create mode 100644 ui/lib/fiat-currencies.ts create mode 100644 ui/lib/use-xmr-fiat-rate.ts diff --git a/ui/app/(app)/dashboard/actions.ts b/ui/app/(app)/dashboard/actions.ts index ee6586a..dabcf1f 100644 --- a/ui/app/(app)/dashboard/actions.ts +++ b/ui/app/(app)/dashboard/actions.ts @@ -73,6 +73,8 @@ export async function createInvoiceAction( _prevState: InvoiceState, formData: FormData ): Promise { + const amountMode = String(formData.get("amount_mode") ?? "xmr").trim(); + const isFiat = amountMode === "fiat"; const instantConfirmation = formData.get("instant_confirmation") === "1"; const confirmationRaw = String(formData.get("confirmation_target") ?? "").trim(); const confirmationTarget = instantConfirmation ? 0 : Number(confirmationRaw); @@ -92,10 +94,26 @@ export async function createInvoiceAction( warnings: null, }; } - const amount = Number(formData.get("amount_xmr")); + const amountRaw = String( + formData.get(isFiat ? "amount_fiat" : "amount_xmr") ?? "" + ).trim(); + const amount = Number(amountRaw); if (Number.isNaN(amount) || amount <= 0) { return { - error: "Enter a valid XMR amount.", + error: isFiat ? "Enter a valid fiat amount." : "Enter a valid XMR amount.", + invoiceId: null, + address: null, + amount: null, + recipientName: null, + description: null, + subaddressIndex: null, + warnings: null, + }; + } + const currency = String(formData.get("currency") ?? "").trim().toUpperCase(); + if (isFiat && !currency) { + return { + error: "Currency is required when using a fiat amount.", invoiceId: null, address: null, amount: null, @@ -105,7 +123,6 @@ export async function createInvoiceAction( warnings: null, }; } - const amountRaw = String(formData.get("amount_xmr") ?? ""); const recipientName = String(formData.get("recipient_name") ?? "").trim(); const description = String(formData.get("description") ?? "").trim(); const expiresAtRaw = String(formData.get("expires_at") ?? "").trim(); @@ -194,7 +211,9 @@ export async function createInvoiceAction( Authorization: `ApiKey ${apiKey}`, }, body: JSON.stringify({ - amount_xmr: amount, + amount_xmr: isFiat ? null : amountRaw, + amount_fiat: isFiat ? amountRaw : null, + currency: isFiat ? currency : null, confirmation_target: confirmationTarget, expires_at: expiresAt, metadata: @@ -227,13 +246,14 @@ export async function createInvoiceAction( id: string; address: string; subaddress_index?: number | null; + amount_xmr: string; warnings?: string[] | null; }; return { error: null, invoiceId: data.id, address: data.address, - amount: amountRaw, + amount: data.amount_xmr, recipientName: recipientName || null, description: description || null, subaddressIndex: data.subaddress_index ?? null, diff --git a/ui/components/create-invoice-card.tsx b/ui/components/create-invoice-card.tsx index 1facab2..ca84798 100644 --- a/ui/components/create-invoice-card.tsx +++ b/ui/components/create-invoice-card.tsx @@ -10,8 +10,13 @@ import { createInvoiceAction, type InvoiceState, } from "../app/(app)/dashboard/actions"; -import { formatUsdAmount } from "../lib/formatting"; +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, @@ -45,8 +50,13 @@ export default function CreateInvoiceCard() { 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"; @@ -69,16 +79,41 @@ export default function CreateInvoiceCard() { 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, state.amount]); + }, [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} XMR` + : fiatRateStatus === "loading" + ? "Fetching estimate..." + : "Estimate unavailable"; + const fiatCurrencyFlag = getCurrencyFlag(fiatCurrency); useEffect(() => { let active = true; @@ -149,36 +184,123 @@ export default function CreateInvoiceCard() { />
-
+ {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 an XMR invoice using a non-binding rate at request time. +

+ {showFiatEstimate ? ( +
+

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

+ {xmrEstimate ? ( +
+ + About this estimate + +

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

+
+ ) : null} +
+ ) : null} +
+ )}