mirror of
https://github.com/Such-Software/xmrcheckout.git
synced 2026-10-17 05:51:34 +02:00
Add fiat invoice inputs to UI
This commit is contained in:
@@ -73,6 +73,8 @@ export async function createInvoiceAction(
|
|||||||
_prevState: InvoiceState,
|
_prevState: InvoiceState,
|
||||||
formData: FormData
|
formData: FormData
|
||||||
): Promise<InvoiceState> {
|
): Promise<InvoiceState> {
|
||||||
|
const amountMode = String(formData.get("amount_mode") ?? "xmr").trim();
|
||||||
|
const isFiat = amountMode === "fiat";
|
||||||
const instantConfirmation = formData.get("instant_confirmation") === "1";
|
const instantConfirmation = formData.get("instant_confirmation") === "1";
|
||||||
const confirmationRaw = String(formData.get("confirmation_target") ?? "").trim();
|
const confirmationRaw = String(formData.get("confirmation_target") ?? "").trim();
|
||||||
const confirmationTarget = instantConfirmation ? 0 : Number(confirmationRaw);
|
const confirmationTarget = instantConfirmation ? 0 : Number(confirmationRaw);
|
||||||
@@ -92,10 +94,26 @@ export async function createInvoiceAction(
|
|||||||
warnings: null,
|
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) {
|
if (Number.isNaN(amount) || amount <= 0) {
|
||||||
return {
|
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,
|
invoiceId: null,
|
||||||
address: null,
|
address: null,
|
||||||
amount: null,
|
amount: null,
|
||||||
@@ -105,7 +123,6 @@ export async function createInvoiceAction(
|
|||||||
warnings: null,
|
warnings: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const amountRaw = String(formData.get("amount_xmr") ?? "");
|
|
||||||
const recipientName = String(formData.get("recipient_name") ?? "").trim();
|
const recipientName = String(formData.get("recipient_name") ?? "").trim();
|
||||||
const description = String(formData.get("description") ?? "").trim();
|
const description = String(formData.get("description") ?? "").trim();
|
||||||
const expiresAtRaw = String(formData.get("expires_at") ?? "").trim();
|
const expiresAtRaw = String(formData.get("expires_at") ?? "").trim();
|
||||||
@@ -194,7 +211,9 @@ export async function createInvoiceAction(
|
|||||||
Authorization: `ApiKey ${apiKey}`,
|
Authorization: `ApiKey ${apiKey}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
amount_xmr: amount,
|
amount_xmr: isFiat ? null : amountRaw,
|
||||||
|
amount_fiat: isFiat ? amountRaw : null,
|
||||||
|
currency: isFiat ? currency : null,
|
||||||
confirmation_target: confirmationTarget,
|
confirmation_target: confirmationTarget,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
metadata:
|
metadata:
|
||||||
@@ -227,13 +246,14 @@ export async function createInvoiceAction(
|
|||||||
id: string;
|
id: string;
|
||||||
address: string;
|
address: string;
|
||||||
subaddress_index?: number | null;
|
subaddress_index?: number | null;
|
||||||
|
amount_xmr: string;
|
||||||
warnings?: string[] | null;
|
warnings?: string[] | null;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
invoiceId: data.id,
|
invoiceId: data.id,
|
||||||
address: data.address,
|
address: data.address,
|
||||||
amount: amountRaw,
|
amount: data.amount_xmr,
|
||||||
recipientName: recipientName || null,
|
recipientName: recipientName || null,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
subaddressIndex: data.subaddress_index ?? null,
|
subaddressIndex: data.subaddress_index ?? null,
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ import {
|
|||||||
createInvoiceAction,
|
createInvoiceAction,
|
||||||
type InvoiceState,
|
type InvoiceState,
|
||||||
} from "../app/(app)/dashboard/actions";
|
} 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 { 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 = {
|
const initialState: InvoiceState = {
|
||||||
error: null,
|
error: null,
|
||||||
@@ -45,8 +50,13 @@ export default function CreateInvoiceCard() {
|
|||||||
const [qrColor, setQrColor] = useState("#1b1b1a");
|
const [qrColor, setQrColor] = useState("#1b1b1a");
|
||||||
const [qrLogo, setQrLogo] = useState<string | null>(null);
|
const [qrLogo, setQrLogo] = useState<string | null>(null);
|
||||||
const [origin, setOrigin] = useState<string | null>(null);
|
const [origin, setOrigin] = useState<string | null>(null);
|
||||||
|
const [amountMode, setAmountMode] = useState<"xmr" | "fiat">("xmr");
|
||||||
const [amountInput, setAmountInput] = useState("");
|
const [amountInput, setAmountInput] = useState("");
|
||||||
|
const [fiatCurrency, setFiatCurrency] = useState("USD");
|
||||||
const { rate: usdRate } = useXmrUsdRate();
|
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 labelClass = "text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft";
|
||||||
const inputClass =
|
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";
|
"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 uri = useMemo(() => buildMoneroUri(state), [state]);
|
||||||
const amountValue = useMemo(() => {
|
const amountValue = useMemo(() => {
|
||||||
|
if (amountMode !== "xmr") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const raw = amountInput || state.amount || "";
|
const raw = amountInput || state.amount || "";
|
||||||
const parsed = Number.parseFloat(raw);
|
const parsed = Number.parseFloat(raw);
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
}, [amountInput, state.amount]);
|
}, [amountInput, amountMode, state.amount]);
|
||||||
const usdEstimate = useMemo(() => {
|
const usdEstimate = useMemo(() => {
|
||||||
if (!usdRate || amountValue === null || amountValue <= 0) {
|
if (!usdRate || amountValue === null || amountValue <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return formatUsdAmount(usdRate * amountValue);
|
return formatUsdAmount(usdRate * amountValue);
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
@@ -149,36 +184,123 @@ export default function CreateInvoiceCard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label className={labelClass} htmlFor="amount_xmr">
|
<label className={labelClass} htmlFor="amount_mode">
|
||||||
Amount (XMR)
|
Amount type
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
id="amount_xmr"
|
id="amount_mode"
|
||||||
name="amount_xmr"
|
name="amount_mode"
|
||||||
type="number"
|
value={amountMode}
|
||||||
step="0.000001"
|
onChange={(event) => {
|
||||||
min="0"
|
const value = event.target.value;
|
||||||
placeholder="0.10"
|
if (value === "xmr" || value === "fiat") {
|
||||||
required
|
setAmountMode(value);
|
||||||
onChange={(event) => setAmountInput(event.target.value)}
|
}
|
||||||
/>
|
}}
|
||||||
{usdEstimate ? (
|
>
|
||||||
<>
|
<option value="xmr">XMR</option>
|
||||||
<p className="text-sm text-ink-soft">
|
<option value="fiat">Fiat</option>
|
||||||
Approx. USD reference: ~{usdEstimate}
|
</select>
|
||||||
</p>
|
|
||||||
<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>
|
</div>
|
||||||
|
{amountMode === "xmr" ? (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className={labelClass} htmlFor="amount_xmr">
|
||||||
|
Amount (XMR)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className={inputClass}
|
||||||
|
id="amount_xmr"
|
||||||
|
name="amount_xmr"
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
min="0"
|
||||||
|
placeholder="0.10"
|
||||||
|
required
|
||||||
|
onChange={(event) => setAmountInput(event.target.value)}
|
||||||
|
/>
|
||||||
|
{usdEstimate ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-ink-soft">
|
||||||
|
Approx. USD reference: ~{usdEstimate}
|
||||||
|
</p>
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<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">
|
<div className="grid gap-2">
|
||||||
<label className={labelClass} htmlFor="confirmation_target">
|
<label className={labelClass} htmlFor="confirmation_target">
|
||||||
Confirmation target
|
Confirmation target
|
||||||
|
|||||||
+204
-34
@@ -7,6 +7,11 @@ import { useRouter } from "next/navigation";
|
|||||||
import { formatRelativeTime } from "./relative-time";
|
import { formatRelativeTime } from "./relative-time";
|
||||||
import { formatUsdAmount, formatXmrAmount } from "../lib/formatting";
|
import { formatUsdAmount, formatXmrAmount } from "../lib/formatting";
|
||||||
import { useXmrUsdRate } from "../lib/use-xmr-usd-rate";
|
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 {
|
import {
|
||||||
createInvoiceAction,
|
createInvoiceAction,
|
||||||
@@ -100,6 +105,8 @@ export default function InvoicePanel({
|
|||||||
const [createStep, setCreateStep] = useState<1 | 2 | 3>(1);
|
const [createStep, setCreateStep] = useState<1 | 2 | 3>(1);
|
||||||
const [formKey, setFormKey] = useState(0);
|
const [formKey, setFormKey] = useState(0);
|
||||||
const [amount, setAmount] = useState("");
|
const [amount, setAmount] = useState("");
|
||||||
|
const [amountMode, setAmountMode] = useState<"xmr" | "fiat">("xmr");
|
||||||
|
const [fiatCurrency, setFiatCurrency] = useState("USD");
|
||||||
const [confirmationTarget, setConfirmationTarget] = useState(
|
const [confirmationTarget, setConfirmationTarget] = useState(
|
||||||
String(defaultConfirmationTarget ?? 10)
|
String(defaultConfirmationTarget ?? 10)
|
||||||
);
|
);
|
||||||
@@ -117,6 +124,9 @@ export default function InvoicePanel({
|
|||||||
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/+$/, "") ?? null
|
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/+$/, "") ?? null
|
||||||
);
|
);
|
||||||
const { rate: usdRate } = useXmrUsdRate();
|
const { rate: usdRate } = useXmrUsdRate();
|
||||||
|
const { rate: fiatRate, status: fiatRateStatus } = useXmrFiatRate(
|
||||||
|
amountMode === "fiat" ? fiatCurrency : null
|
||||||
|
);
|
||||||
|
|
||||||
const activeList = activeInvoices;
|
const activeList = activeInvoices;
|
||||||
const archivedToggleHref = includeArchived
|
const archivedToggleHref = includeArchived
|
||||||
@@ -192,26 +202,59 @@ export default function InvoicePanel({
|
|||||||
? new URL(`/invoice/${state.invoiceId}`, createInvoiceOrigin).toString()
|
? new URL(`/invoice/${state.invoiceId}`, createInvoiceOrigin).toString()
|
||||||
: `/invoice/${state.invoiceId}`
|
: `/invoice/${state.invoiceId}`
|
||||||
: null;
|
: null;
|
||||||
const formattedDraftAmount = amount ? formatXmrAmount(amount) : "";
|
const formattedDraftAmount =
|
||||||
|
amountMode === "xmr" && amount ? formatXmrAmount(amount) : amount;
|
||||||
const draftAmountValue = useMemo(() => {
|
const draftAmountValue = useMemo(() => {
|
||||||
|
if (amountMode !== "xmr") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const parsed = Number.parseFloat(amount);
|
const parsed = Number.parseFloat(amount);
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
}, [amount]);
|
}, [amount, amountMode]);
|
||||||
const draftUsdEstimate = useMemo(() => {
|
const draftUsdEstimate = useMemo(() => {
|
||||||
if (!usdRate || draftAmountValue === null || draftAmountValue <= 0) {
|
if (!usdRate || draftAmountValue === null || draftAmountValue <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return formatUsdAmount(usdRate * draftAmountValue);
|
return formatUsdAmount(usdRate * draftAmountValue);
|
||||||
}, [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 expiryTimeValue = expiresTime || "00:00";
|
||||||
const expiryValue = expiresDate ? `${expiresDate}T${expiryTimeValue}` : "";
|
const expiryValue = expiresDate ? `${expiresDate}T${expiryTimeValue}` : "";
|
||||||
const expiryIsValid = (!expiresDate && !expiresTime) || !!expiresDate;
|
const expiryIsValid = (!expiresDate && !expiresTime) || !!expiresDate;
|
||||||
|
const isFiatCurrencyValid =
|
||||||
|
amountMode !== "fiat" || Boolean(fiatCurrency.trim());
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setIsCreateOpen(true);
|
setIsCreateOpen(true);
|
||||||
setCreateStep(1);
|
setCreateStep(1);
|
||||||
setFormKey((prev) => prev + 1);
|
setFormKey((prev) => prev + 1);
|
||||||
setAmount("");
|
setAmount("");
|
||||||
|
setAmountMode("xmr");
|
||||||
|
setFiatCurrency("USD");
|
||||||
setConfirmationTarget(String(defaultConfirmationTarget ?? 10));
|
setConfirmationTarget(String(defaultConfirmationTarget ?? 10));
|
||||||
setExpiresDate("");
|
setExpiresDate("");
|
||||||
setExpiresTime("");
|
setExpiresTime("");
|
||||||
@@ -612,37 +655,129 @@ export default function InvoicePanel({
|
|||||||
{createStep === 1 ? (
|
{createStep === 1 ? (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label className={labelClass} htmlFor="wizard_amount">
|
<label className={labelClass} htmlFor="wizard_amount_mode">
|
||||||
Amount (XMR)
|
Amount type
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
id="wizard_amount"
|
id="wizard_amount_mode"
|
||||||
name="amount_xmr"
|
name="amount_mode"
|
||||||
type="number"
|
value={amountMode}
|
||||||
step="0.000001"
|
onChange={(event) => {
|
||||||
min="0"
|
const value = event.target.value;
|
||||||
placeholder="0.10"
|
if (value === "xmr" || value === "fiat") {
|
||||||
value={amount}
|
setAmountMode(value);
|
||||||
onChange={(event) => setAmount(event.target.value)}
|
}
|
||||||
required
|
}}
|
||||||
/>
|
>
|
||||||
{draftUsdEstimate ? (
|
<option value="xmr">XMR</option>
|
||||||
<>
|
<option value="fiat">Fiat</option>
|
||||||
<p className="text-sm text-ink-soft">
|
</select>
|
||||||
Approx. USD reference: ~{draftUsdEstimate}
|
|
||||||
</p>
|
|
||||||
<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>
|
</div>
|
||||||
|
{amountMode === "xmr" ? (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className={labelClass} htmlFor="wizard_amount_xmr">
|
||||||
|
Amount (XMR)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className={inputClass}
|
||||||
|
id="wizard_amount_xmr"
|
||||||
|
name="amount_xmr"
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
min="0"
|
||||||
|
placeholder="0.10"
|
||||||
|
value={amount}
|
||||||
|
onChange={(event) => setAmount(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{draftUsdEstimate ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-ink-soft">
|
||||||
|
Approx. USD reference: ~{draftUsdEstimate}
|
||||||
|
</p>
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<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">
|
<div className="grid gap-2">
|
||||||
<label className={labelClass} htmlFor="wizard_confirmation_target">
|
<label className={labelClass} htmlFor="wizard_confirmation_target">
|
||||||
Confirmation target
|
Confirmation target
|
||||||
@@ -771,7 +906,15 @@ export default function InvoicePanel({
|
|||||||
|
|
||||||
{createStep === 2 ? (
|
{createStep === 2 ? (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<input type="hidden" name="amount_xmr" value={amount} />
|
<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
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="confirmation_target"
|
name="confirmation_target"
|
||||||
@@ -791,9 +934,31 @@ export default function InvoicePanel({
|
|||||||
Review details
|
Review details
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-sm text-ink">
|
<p className="mt-3 text-sm text-ink">
|
||||||
Amount:{" "}
|
{amountMode === "xmr" ? "Amount:" : "Fiat amount:"}{" "}
|
||||||
<strong>{formattedDraftAmount || "-"} XMR</strong>
|
<strong>
|
||||||
|
{formattedDraftAmount || "-"}{" "}
|
||||||
|
{amountMode === "xmr" ? "XMR" : fiatCurrencyLabel}
|
||||||
|
</strong>
|
||||||
</p>
|
</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">
|
<p className="mt-2 text-sm text-ink">
|
||||||
Confirmations:{" "}
|
Confirmations:{" "}
|
||||||
<strong>{confirmationTarget || "-"}</strong>
|
<strong>{confirmationTarget || "-"}</strong>
|
||||||
@@ -866,6 +1031,7 @@ export default function InvoicePanel({
|
|||||||
onClick={() => setCreateStep(2)}
|
onClick={() => setCreateStep(2)}
|
||||||
disabled={
|
disabled={
|
||||||
!amount ||
|
!amount ||
|
||||||
|
(amountMode === "fiat" && !fiatCurrency) ||
|
||||||
!confirmationTarget ||
|
!confirmationTarget ||
|
||||||
!expiryIsValid ||
|
!expiryIsValid ||
|
||||||
(qrLogoMode === "custom" && !qrLogoDataUrl)
|
(qrLogoMode === "custom" && !qrLogoDataUrl)
|
||||||
@@ -883,7 +1049,11 @@ export default function InvoicePanel({
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
<button className={primaryButton} type="submit">
|
<button
|
||||||
|
className={primaryButton}
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFiatCurrencyValid}
|
||||||
|
>
|
||||||
Create invoice
|
Create invoice
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
};
|
||||||
Generated
+73
-22
@@ -6,10 +6,13 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "xmrcheckout-ui",
|
"name": "xmrcheckout-ui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"doctrine": "^3.0.0",
|
||||||
|
"esutils": "^2.0.3",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"tsconfig-paths": "^4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
@@ -2373,16 +2376,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esutils": "^2.0.2"
|
"esutils": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
@@ -2830,6 +2832,32 @@
|
|||||||
"ms": "^2.1.1"
|
"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": {
|
"node_modules/eslint-plugin-import/node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -2840,6 +2868,19 @@
|
|||||||
"semver": "bin/semver.js"
|
"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": {
|
"node_modules/eslint-plugin-jsx-a11y": {
|
||||||
"version": "6.10.2",
|
"version": "6.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
|
"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"
|
"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": {
|
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
||||||
"version": "2.0.0-next.5",
|
"version": "2.0.0-next.5",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||||
@@ -3104,7 +3158,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -4128,16 +4181,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "1.0.2",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"minimist": "^1.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
@@ -4303,7 +4355,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -5633,7 +5684,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
@@ -5914,16 +5964,17 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||||
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
|
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json5": "^0.0.29",
|
"json5": "^2.2.2",
|
||||||
"json5": "^1.0.2",
|
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"strip-bom": "^3.0.0"
|
"strip-bom": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
|
|||||||
+4
-1
@@ -9,10 +9,13 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"doctrine": "^3.0.0",
|
||||||
|
"esutils": "^2.0.3",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"tsconfig-paths": "^4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
|
|||||||
Reference in New Issue
Block a user