mirror of
https://github.com/Such-Software/wowcheckout.git
synced 2026-10-17 05:51:45 +02:00
Fork xmrcheckout for Wownero (WOW)
Minimal changes from upstream xmrcheckout to support Wownero: - Rate provider: Nonlogs WOW-BTC via Kraken BTC/USD - Subaddress derivation: Wownero network byte overrides (53/63/54) - Payment URI scheme: wownero: - Dockerfile: Wownero binaries from Codeberg - Display text: Monero→Wownero, XMR→WOW throughout UI - Default daemon: 192.168.88.235:34568 - Added wownero-logo.svg File and folder structure kept identical to upstream for easy sync.
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import BtcpayModalBridge from "../../../components/btcpay-modal-bridge";
|
||||
import BtcpayClassicCheckout from "../../../components/btcpay-classic-checkout";
|
||||
import InvoicePaymentDetails from "../../../components/invoice-payment-details";
|
||||
import InvoiceStatusAutoRefresh from "../../../components/invoice-status-auto-refresh";
|
||||
import { formatRelativeTime } from "../../../components/relative-time";
|
||||
import StatusRefreshButton from "../../../components/status-refresh-button";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const metadata: Metadata = {
|
||||
title: "Payment Request",
|
||||
};
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
type InvoiceStatus =
|
||||
| "pending"
|
||||
| "payment_detected"
|
||||
| "confirmed"
|
||||
| "expired"
|
||||
| "invalid";
|
||||
|
||||
type InvoiceStatusResponse = {
|
||||
id: string;
|
||||
address: string;
|
||||
subaddress_index: number | null;
|
||||
amount_xmr: string;
|
||||
amount_paid_xmr?: string | null;
|
||||
status: InvoiceStatus;
|
||||
confirmation_target: number;
|
||||
confirmations: number;
|
||||
checkout_continue_available?: boolean;
|
||||
created_at: string;
|
||||
expires_at: string | null;
|
||||
detected_at: string | null;
|
||||
confirmed_at: string | null;
|
||||
btcpay_amount?: string | null;
|
||||
btcpay_currency?: string | null;
|
||||
btcpay_checkout_style?: "standard" | "btcpay_classic" | null;
|
||||
btcpay_redirect_url?: string | null;
|
||||
btcpay_redirect_automatically?: boolean | null;
|
||||
btcpay_order_id?: string | null;
|
||||
btcpay_order_number?: string | null;
|
||||
qr_logo?: "monero" | "none" | "custom" | null;
|
||||
qr_logo_data_url?: string | null;
|
||||
quote?: {
|
||||
fiat_amount: string;
|
||||
fiat_currency: string;
|
||||
rate: string;
|
||||
source: string;
|
||||
quoted_at: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const formatStatus = (status: InvoiceStatus) => {
|
||||
if (status === "payment_detected") {
|
||||
return "Payment detected";
|
||||
}
|
||||
if (status === "pending") {
|
||||
return "Awaiting funds";
|
||||
}
|
||||
if (status === "confirmed") {
|
||||
return "Confirmed";
|
||||
}
|
||||
if (status === "invalid") {
|
||||
return "Invalid";
|
||||
}
|
||||
return "Expired";
|
||||
};
|
||||
|
||||
const formatTimestamp = (value: string | null) => {
|
||||
if (!value) {
|
||||
return { label: "-", relative: null };
|
||||
}
|
||||
return {
|
||||
label: new Date(value).toLocaleString(),
|
||||
relative: formatRelativeTime(value),
|
||||
};
|
||||
};
|
||||
|
||||
const statusPillStyles: Record<InvoiceStatus, string> = {
|
||||
pending: "bg-amber-100 text-amber-900",
|
||||
payment_detected: "bg-emerald-100 text-emerald-900",
|
||||
confirmed: "bg-ink/20 text-ink",
|
||||
expired: "bg-red-100 text-red-700",
|
||||
invalid: "bg-clay/15 text-clay",
|
||||
};
|
||||
|
||||
export default async function BtcpayModalInvoicePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ invoiceId: string }>;
|
||||
}) {
|
||||
const { invoiceId } = await params;
|
||||
const response = await fetch(
|
||||
`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
return (
|
||||
<main className="min-h-screen bg-cream px-6 py-6 text-ink">
|
||||
<BtcpayModalBridge invoiceId={invoiceId} status="invalid" />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-clay">
|
||||
Payment request
|
||||
</p>
|
||||
<h1 className="mt-2 font-serif text-2xl">Invoice not found</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-ink-soft">
|
||||
The invoice id does not match a known invoice. Check the id and try
|
||||
again.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return (
|
||||
<main className="min-h-screen bg-cream px-6 py-6 text-ink">
|
||||
<BtcpayModalBridge invoiceId={invoiceId} status="invalid" />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-clay">
|
||||
Payment request
|
||||
</p>
|
||||
<h1 className="mt-2 font-serif text-2xl">Status unavailable</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-ink-soft">
|
||||
We could not load this invoice status. Refresh the page or try again
|
||||
later.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const invoice = (await response.json()) as InvoiceStatusResponse;
|
||||
const statusLabel = formatStatus(invoice.status);
|
||||
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
||||
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
||||
const hasDetectedPayment =
|
||||
invoice.status === "payment_detected" || invoice.status === "confirmed";
|
||||
const createdTimestamp = formatTimestamp(invoice.created_at);
|
||||
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
||||
const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency);
|
||||
const useClassicCheckout =
|
||||
isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic";
|
||||
const shouldShowBtcpayActions = isBtcpayInvoice && invoice.status === "confirmed";
|
||||
const checkoutContinueAvailable =
|
||||
!isBtcpayInvoice &&
|
||||
invoice.status === "confirmed" &&
|
||||
Boolean(invoice.checkout_continue_available);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-cream px-6 py-6 text-ink">
|
||||
<InvoiceStatusAutoRefresh intervalMs={30000} />
|
||||
<BtcpayModalBridge invoiceId={invoiceId} status={invoice.status} />
|
||||
{useClassicCheckout ? (
|
||||
<div className="mx-auto grid max-w-md gap-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-clay">
|
||||
Payment request
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusRefreshButton label="Refresh" className="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<BtcpayClassicCheckout
|
||||
invoiceId={invoiceId}
|
||||
address={invoice.address}
|
||||
amountXmr={invoice.amount_xmr}
|
||||
amountPaidXmr={invoice.amount_paid_xmr ?? null}
|
||||
btcpayAmount={invoice.btcpay_amount ?? null}
|
||||
btcpayCurrency={invoice.btcpay_currency ?? null}
|
||||
quote={invoice.quote ?? null}
|
||||
status={invoice.status}
|
||||
confirmationTarget={confirmationTarget}
|
||||
redirectUrl={invoice.btcpay_redirect_url ?? null}
|
||||
redirectAutomatically={invoice.btcpay_redirect_automatically ?? null}
|
||||
orderId={invoice.btcpay_order_id ?? null}
|
||||
orderNumber={invoice.btcpay_order_number ?? null}
|
||||
qrLogoMode={invoice.qr_logo ?? "monero"}
|
||||
qrLogoDataUrl={invoice.qr_logo_data_url ?? null}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-clay">
|
||||
Payment request
|
||||
</p>
|
||||
<h1 className="mt-2 font-serif text-2xl">Payment request</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusRefreshButton label="Refresh" className="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-stroke bg-card p-5 shadow-card backdrop-blur">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="rounded-full bg-ink/10 px-3 py-1 text-[0.7rem] font-semibold uppercase tracking-[0.12em] text-ink">
|
||||
Status
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-[0.7rem] font-semibold uppercase tracking-[0.12em] ${statusPillStyles[invoice.status]}`}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||
Confirmations
|
||||
</p>
|
||||
<p className="mt-1 text-lg font-semibold">
|
||||
{confirmations}/{confirmationTarget}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||
Created
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-sm font-semibold"
|
||||
title={createdTimestamp.relative ?? undefined}
|
||||
>
|
||||
{createdTimestamp.label}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||
Expires
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-sm font-semibold"
|
||||
title={expiresTimestamp.relative ?? undefined}
|
||||
>
|
||||
{expiresTimestamp.label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{checkoutContinueAvailable ? (
|
||||
<a
|
||||
className="mt-5 inline-flex w-fit items-center justify-center rounded-full bg-ink px-5 py-2 text-sm font-semibold text-cream shadow-[0_16px_30px_rgba(16,18,23,0.18)] transition hover:-translate-y-0.5"
|
||||
href={`/api/core/public/invoice/${encodeURIComponent(invoiceId)}/continue`}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Continue to merchant
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<InvoicePaymentDetails
|
||||
address={invoice.address}
|
||||
amount={invoice.amount_xmr}
|
||||
hasDetectedPayment={hasDetectedPayment}
|
||||
status={invoice.status}
|
||||
confirmationTarget={confirmationTarget}
|
||||
qrLogoMode={invoice.qr_logo ?? "monero"}
|
||||
qrLogoDataUrl={invoice.qr_logo_data_url ?? null}
|
||||
/>
|
||||
</div>
|
||||
{shouldShowBtcpayActions ? (
|
||||
<div className="mt-6 grid gap-3">
|
||||
<a
|
||||
className="inline-flex items-center justify-center rounded-full bg-sage px-6 py-3 text-sm font-semibold text-cream shadow-[0_14px_22px_rgba(93,122,106,0.25)] transition hover:-translate-y-0.5"
|
||||
href={`/i/${encodeURIComponent(invoiceId)}/receipt`}
|
||||
>
|
||||
View receipt
|
||||
</a>
|
||||
{invoice.btcpay_redirect_url ? (
|
||||
<a
|
||||
className="inline-flex items-center justify-center rounded-full border border-stroke bg-white px-6 py-3 text-sm font-semibold text-sage transition hover:bg-cream"
|
||||
href={invoice.btcpay_redirect_url}
|
||||
target="_top"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Return to store
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user