Add fiat invoice inputs to UI

This commit is contained in:
xmrcheckout
2026-01-05 12:03:03 +00:00
parent 87219a70eb
commit b8a86c0c25
7 changed files with 614 additions and 91 deletions
+25 -5
View File
@@ -73,6 +73,8 @@ export async function createInvoiceAction(
_prevState: InvoiceState,
formData: FormData
): Promise<InvoiceState> {
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,
+124 -2
View File
@@ -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<string | null>(null);
const [origin, setOrigin] = useState<string | null>(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;
@@ -148,6 +183,27 @@ export default function CreateInvoiceCard() {
required
/>
</div>
<div className="grid gap-2">
<label className={labelClass} htmlFor="amount_mode">
Amount type
</label>
<select
className={inputClass}
id="amount_mode"
name="amount_mode"
value={amountMode}
onChange={(event) => {
const value = event.target.value;
if (value === "xmr" || value === "fiat") {
setAmountMode(value);
}
}}
>
<option value="xmr">XMR</option>
<option value="fiat">Fiat</option>
</select>
</div>
{amountMode === "xmr" ? (
<div className="grid gap-2">
<label className={labelClass} htmlFor="amount_xmr">
Amount (XMR)
@@ -179,6 +235,72 @@ export default function CreateInvoiceCard() {
</>
) : null}
</div>
) : (
<div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_140px]">
<div className="grid gap-2">
<label className={labelClass} htmlFor="amount_fiat">
Amount (fiat)
</label>
<input
className={inputClass}
id="amount_fiat"
name="amount_fiat"
type="number"
step="0.01"
min="0"
placeholder="100.00"
required
onChange={(event) => setAmountInput(event.target.value)}
/>
</div>
<div className="grid gap-2">
<label className={labelClass} htmlFor="currency">
Currency
</label>
<input
className={inputClass}
id="currency"
name="currency"
type="text"
placeholder="USD"
list="fiat-currency-options"
value={fiatCurrency}
onChange={(event) => setFiatCurrency(event.target.value.toUpperCase())}
required
/>
</div>
<datalist id="fiat-currency-options">
{FIAT_CURRENCY_SUGGESTIONS.map((code) => (
<option
key={code}
value={code}
label={`${getCurrencyFlag(code) ?? ""} ${code}`.trim()}
/>
))}
</datalist>
<p className="text-sm text-ink-soft sm:col-span-2">
Fiat inputs create an XMR invoice using a non-binding rate at request time.
</p>
{showFiatEstimate ? (
<div className="sm:col-span-2">
<p className="text-sm text-ink-soft">
Approx. XMR value: {fiatEstimateLabel}
{fiatCurrencyFlag ? ` · ${fiatCurrencyFlag} ${fiatCurrency}` : ""}
</p>
{xmrEstimate ? (
<details className="w-fit text-xs text-ink-soft">
<summary className="cursor-pointer select-none underline underline-offset-4">
About this estimate
</summary>
<p className="mt-2 max-w-[46ch] leading-relaxed">
Reference only, uses CoinGecko spot rate. Not a quote or guarantee.
</p>
</details>
) : null}
</div>
) : null}
</div>
)}
<div className="grid gap-2">
<label className={labelClass} htmlFor="confirmation_target">
Confirmation target
+177 -7
View File
@@ -7,6 +7,11 @@ import { useRouter } from "next/navigation";
import { formatRelativeTime } from "./relative-time";
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";
import {
createInvoiceAction,
@@ -100,6 +105,8 @@ export default function InvoicePanel({
const [createStep, setCreateStep] = useState<1 | 2 | 3>(1);
const [formKey, setFormKey] = useState(0);
const [amount, setAmount] = useState("");
const [amountMode, setAmountMode] = useState<"xmr" | "fiat">("xmr");
const [fiatCurrency, setFiatCurrency] = useState("USD");
const [confirmationTarget, setConfirmationTarget] = useState(
String(defaultConfirmationTarget ?? 10)
);
@@ -117,6 +124,9 @@ export default function InvoicePanel({
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/+$/, "") ?? null
);
const { rate: usdRate } = useXmrUsdRate();
const { rate: fiatRate, status: fiatRateStatus } = useXmrFiatRate(
amountMode === "fiat" ? fiatCurrency : null
);
const activeList = activeInvoices;
const archivedToggleHref = includeArchived
@@ -192,26 +202,59 @@ export default function InvoicePanel({
? new URL(`/invoice/${state.invoiceId}`, createInvoiceOrigin).toString()
: `/invoice/${state.invoiceId}`
: null;
const formattedDraftAmount = amount ? formatXmrAmount(amount) : "";
const formattedDraftAmount =
amountMode === "xmr" && amount ? formatXmrAmount(amount) : amount;
const draftAmountValue = useMemo(() => {
if (amountMode !== "xmr") {
return null;
}
const parsed = Number.parseFloat(amount);
return Number.isFinite(parsed) ? parsed : null;
}, [amount]);
}, [amount, amountMode]);
const draftUsdEstimate = useMemo(() => {
if (!usdRate || draftAmountValue === null || draftAmountValue <= 0) {
return null;
}
return formatUsdAmount(usdRate * draftAmountValue);
}, [usdRate, draftAmountValue]);
const fiatAmountValue = useMemo(() => {
if (amountMode !== "fiat") {
return null;
}
const parsed = Number.parseFloat(amount);
return Number.isFinite(parsed) ? parsed : null;
}, [amount, amountMode]);
const draftXmrEstimate = 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 draftXmrEstimateLabel = draftXmrEstimate
? `~${draftXmrEstimate} XMR`
: fiatRateStatus === "loading"
? "Fetching estimate..."
: "Estimate unavailable";
const fiatCurrencyFlag = getCurrencyFlag(fiatCurrency);
const fiatCurrencyLabel = fiatCurrencyFlag
? `${fiatCurrencyFlag} ${fiatCurrency}`
: fiatCurrency;
const expiryTimeValue = expiresTime || "00:00";
const expiryValue = expiresDate ? `${expiresDate}T${expiryTimeValue}` : "";
const expiryIsValid = (!expiresDate && !expiresTime) || !!expiresDate;
const isFiatCurrencyValid =
amountMode !== "fiat" || Boolean(fiatCurrency.trim());
const openCreate = () => {
setIsCreateOpen(true);
setCreateStep(1);
setFormKey((prev) => prev + 1);
setAmount("");
setAmountMode("xmr");
setFiatCurrency("USD");
setConfirmationTarget(String(defaultConfirmationTarget ?? 10));
setExpiresDate("");
setExpiresTime("");
@@ -612,12 +655,33 @@ export default function InvoicePanel({
{createStep === 1 ? (
<div className="grid gap-4">
<div className="grid gap-2">
<label className={labelClass} htmlFor="wizard_amount">
<label className={labelClass} htmlFor="wizard_amount_mode">
Amount type
</label>
<select
className={inputClass}
id="wizard_amount_mode"
name="amount_mode"
value={amountMode}
onChange={(event) => {
const value = event.target.value;
if (value === "xmr" || value === "fiat") {
setAmountMode(value);
}
}}
>
<option value="xmr">XMR</option>
<option value="fiat">Fiat</option>
</select>
</div>
{amountMode === "xmr" ? (
<div className="grid gap-2">
<label className={labelClass} htmlFor="wizard_amount_xmr">
Amount (XMR)
</label>
<input
className={inputClass}
id="wizard_amount"
id="wizard_amount_xmr"
name="amount_xmr"
type="number"
step="0.000001"
@@ -643,6 +707,77 @@ export default function InvoicePanel({
</>
) : null}
</div>
) : (
<div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_140px]">
<div className="grid gap-2">
<label className={labelClass} htmlFor="wizard_amount_fiat">
Amount (fiat)
</label>
<input
className={inputClass}
id="wizard_amount_fiat"
name="amount_fiat"
type="number"
step="0.01"
min="0"
placeholder="100.00"
value={amount}
onChange={(event) => setAmount(event.target.value)}
required
/>
</div>
<div className="grid gap-2">
<label className={labelClass} htmlFor="wizard_currency">
Currency
</label>
<input
className={inputClass}
id="wizard_currency"
name="currency"
type="text"
placeholder="USD"
list="wizard-fiat-currency-options"
value={fiatCurrency}
onChange={(event) =>
setFiatCurrency(event.target.value.toUpperCase())
}
required
/>
</div>
<p className="text-sm text-ink-soft sm:col-span-2">
Fiat inputs create an XMR invoice using a non-binding rate at request time.
</p>
{showFiatEstimate ? (
<div className="sm:col-span-2">
<p className="text-sm text-ink-soft">
Approx. XMR value: {draftXmrEstimateLabel}
{fiatCurrencyFlag ? ` · ${fiatCurrencyLabel}` : ""}
</p>
{draftXmrEstimate ? (
<details className="w-fit text-xs text-ink-soft">
<summary className="cursor-pointer select-none underline underline-offset-4">
About this estimate
</summary>
<p className="mt-2 max-w-[46ch] leading-relaxed">
Reference only, uses CoinGecko spot rate. Not a quote or guarantee.
</p>
</details>
) : null}
</div>
) : null}
</div>
)}
{amountMode === "fiat" ? (
<datalist id="wizard-fiat-currency-options">
{FIAT_CURRENCY_SUGGESTIONS.map((code) => (
<option
key={code}
value={code}
label={`${getCurrencyFlag(code) ?? ""} ${code}`.trim()}
/>
))}
</datalist>
) : null}
<div className="grid gap-2">
<label className={labelClass} htmlFor="wizard_confirmation_target">
Confirmation target
@@ -771,7 +906,15 @@ export default function InvoicePanel({
{createStep === 2 ? (
<div className="grid gap-4">
<input type="hidden" name="amount_mode" value={amountMode} />
{amountMode === "xmr" ? (
<input type="hidden" name="amount_xmr" value={amount} />
) : (
<>
<input type="hidden" name="amount_fiat" value={amount} />
<input type="hidden" name="currency" value={fiatCurrency} />
</>
)}
<input
type="hidden"
name="confirmation_target"
@@ -791,9 +934,31 @@ export default function InvoicePanel({
Review details
</p>
<p className="mt-3 text-sm text-ink">
Amount:{" "}
<strong>{formattedDraftAmount || "-"} XMR</strong>
{amountMode === "xmr" ? "Amount:" : "Fiat amount:"}{" "}
<strong>
{formattedDraftAmount || "-"}{" "}
{amountMode === "xmr" ? "XMR" : fiatCurrencyLabel}
</strong>
</p>
{amountMode === "fiat" ? (
<p className="mt-2 text-sm text-ink">
Estimated XMR:{" "}
<strong>{draftXmrEstimateLabel}</strong>
</p>
) : null}
{amountMode === "fiat" && !isFiatCurrencyValid ? (
<p className="mt-2 text-sm font-semibold text-red-700">
Currency is required to create a fiat-denominated invoice.
</p>
) : null}
{amountMode === "fiat" &&
fiatRateStatus === "error" &&
isFiatCurrencyValid ? (
<p className="mt-2 text-sm text-amber-800">
Estimate unavailable for {fiatCurrencyLabel}. We will validate the
currency when the invoice is created.
</p>
) : null}
<p className="mt-2 text-sm text-ink">
Confirmations:{" "}
<strong>{confirmationTarget || "-"}</strong>
@@ -866,6 +1031,7 @@ export default function InvoicePanel({
onClick={() => setCreateStep(2)}
disabled={
!amount ||
(amountMode === "fiat" && !fiatCurrency) ||
!confirmationTarget ||
!expiryIsValid ||
(qrLogoMode === "custom" && !qrLogoDataUrl)
@@ -883,7 +1049,11 @@ export default function InvoicePanel({
>
Back
</button>
<button className={primaryButton} type="submit">
<button
className={primaryButton}
type="submit"
disabled={!isFiatCurrencyValid}
>
Create invoice
</button>
</>
+54
View File
@@ -0,0 +1,54 @@
export const FIAT_CURRENCY_SUGGESTIONS = [
"USD",
"EUR",
"GBP",
"CAD",
"AUD",
"NZD",
"CHF",
"JPY",
"CNY",
"HKD",
"SGD",
"INR",
"BRL",
"MXN",
"SEK",
"NOK",
"DKK",
"ZAR",
"PLN",
"KRW",
];
const CURRENCY_FLAGS: Record<string, string> = {
USD: "🇺🇸",
EUR: "🇪🇺",
GBP: "🇬🇧",
CAD: "🇨🇦",
AUD: "🇦🇺",
NZD: "🇳🇿",
CHF: "🇨🇭",
JPY: "🇯🇵",
CNY: "🇨🇳",
HKD: "🇭🇰",
SGD: "🇸🇬",
INR: "🇮🇳",
BRL: "🇧🇷",
MXN: "🇲🇽",
SEK: "🇸🇪",
NOK: "🇳🇴",
DKK: "🇩🇰",
ZAR: "🇿🇦",
PLN: "🇵🇱",
KRW: "🇰🇷",
};
export const getCurrencyFlag = (
code: string | null | undefined
): string | null => {
if (!code) {
return null;
}
return CURRENCY_FLAGS[code.trim().toUpperCase()] ?? null;
};
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState } from "react";
const RATE_TTL_MS = 5 * 60 * 1000;
const BASE_URL = "https://api.coingecko.com/api/v3/simple/price";
type CachedRate = {
rate: number;
updatedAt: number;
};
const cachedRates = new Map<string, CachedRate>();
const inflight = new Map<string, Promise<number | null>>();
const fetchRate = async (currency: string): Promise<number | null> => {
try {
const params = new URLSearchParams({
ids: "monero",
vs_currencies: currency,
});
const response = await fetch(`${BASE_URL}?${params.toString()}`, {
cache: "no-store",
});
if (!response.ok) {
return null;
}
const data = (await response.json()) as {
monero?: Record<string, number>;
};
const rate = data?.monero?.[currency];
if (typeof rate !== "number" || !Number.isFinite(rate)) {
return null;
}
cachedRates.set(currency, { rate, updatedAt: Date.now() });
return rate;
} catch {
return null;
}
};
type RateStatus = "idle" | "loading" | "ready" | "error";
export const useXmrFiatRate = (currency: string | null) => {
const normalized = currency?.trim().toLowerCase() ?? "";
const cached = normalized ? cachedRates.get(normalized) : null;
const [rate, setRate] = useState<number | null>(cached?.rate ?? null);
const [updatedAt, setUpdatedAt] = useState<number | null>(cached?.updatedAt ?? null);
const [status, setStatus] = useState<RateStatus>(() => {
if (!normalized) {
return "idle";
}
return cached?.rate ? "ready" : "loading";
});
useEffect(() => {
let active = true;
if (!normalized) {
setRate(null);
setUpdatedAt(null);
setStatus("idle");
return;
}
const now = Date.now();
const cachedEntry = cachedRates.get(normalized);
const hasFreshRate =
cachedEntry && now - cachedEntry.updatedAt < RATE_TTL_MS;
if (hasFreshRate) {
setRate(cachedEntry.rate);
setUpdatedAt(cachedEntry.updatedAt);
setStatus("ready");
return;
}
setStatus("loading");
const request =
inflight.get(normalized) ??
fetchRate(normalized).finally(() => {
inflight.delete(normalized);
});
inflight.set(normalized, request);
request.then((nextRate) => {
if (!active) {
return;
}
const nextCached = cachedRates.get(normalized);
if (nextRate !== null && nextCached) {
setRate(nextCached.rate);
setUpdatedAt(nextCached.updatedAt);
setStatus("ready");
return;
}
setStatus("error");
});
return () => {
active = false;
};
}, [normalized]);
return { rate, updatedAt, status, source: "coingecko" as const };
};
+73 -22
View File
@@ -6,10 +6,13 @@
"": {
"name": "xmrcheckout-ui",
"dependencies": {
"doctrine": "^3.0.0",
"esutils": "^2.0.3",
"next": "15.5.9",
"qrcode": "1.5.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@types/node": "20.14.12",
@@ -2373,16 +2376,15 @@
"license": "MIT"
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=0.10.0"
"node": ">=6.0.0"
}
},
"node_modules/dunder-proto": {
@@ -2830,6 +2832,32 @@
"ms": "^2.1.1"
}
},
"node_modules/eslint-plugin-import/node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-import/node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"json5": "lib/cli.js"
}
},
"node_modules/eslint-plugin-import/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -2840,6 +2868,19 @@
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-import/node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
}
},
"node_modules/eslint-plugin-jsx-a11y": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
@@ -2923,6 +2964,19 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react/node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -3104,7 +3158,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -4128,16 +4181,15 @@
"license": "MIT"
},
"node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsx-ast-utils": {
@@ -4303,7 +4355,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -5633,7 +5684,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -5914,16 +5964,17 @@
"license": "Apache-2.0"
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
"dev": true,
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"license": "MIT",
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.2",
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tslib": {
+4 -1
View File
@@ -9,10 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"doctrine": "^3.0.0",
"esutils": "^2.0.3",
"next": "15.5.9",
"qrcode": "1.5.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@types/node": "20.14.12",