import type { Metadata } from "next"; import { cookies } from "next/headers"; import Link from "next/link"; import { redirect } from "next/navigation"; import ApiKeySection from "../../../components/api-key-section"; import BtcpayCheckoutStyleSection from "../../../components/btcpay-checkout-style-section"; import DefaultConfirmationTargetSection from "../../../components/default-confirmation-target-section"; import DefaultQrLogoSection from "../../../components/default-qr-logo-section"; import InvoicePanel from "../../../components/invoice-panel"; import WebhookHistoryPanel from "../../../components/webhook-history-panel"; import WebhookSecretSection from "../../../components/webhook-secret-section"; import WebhookSection from "../../../components/webhook-section"; export const metadata: Metadata = { title: "Dashboard", }; const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:8000"; const tabs = ["overview", "invoices", "webhooks", "profile"] as const; const webhookTabs = ["settings", "history"] as const; type DashboardTab = (typeof tabs)[number]; type WebhookTab = (typeof webhookTabs)[number]; type DashboardSearchParams = Record; type InvoiceSummary = { id: string; address: string; subaddress_index?: number | null; amount_xmr: string; status: "pending" | "payment_detected" | "confirmed" | "expired" | "invalid"; confirmation_target: number; paid_after_expiry?: boolean; paid_after_expiry_at?: string | null; created_at: string; archived_at: string | null; detected_at: string | null; confirmed_at: string | null; expires_at: string | null; }; type WebhookSummary = { id: string; url: string; events: string[]; event_urls?: Record | null; active: boolean; created_at: string; }; type WebhookDeliverySummary = { id: string; webhook_id: string | null; event: string; url: string; invoice_id: string | null; invoice_address: string | null; invoice_subaddress_index: number | null; invoice_amount_xmr: string | null; invoice_status: | "pending" | "payment_detected" | "confirmed" | "expired" | "invalid" | null; http_status: number | null; error_message: string | null; created_at: string; }; type ProfileSummary = { id: string; payment_address: string; default_confirmation_target: number; default_qr_logo: "monero" | "none" | "custom"; default_qr_logo_data_url: string | null; btcpay_checkout_style?: "standard" | "btcpay_classic"; created_at: string; }; export default async function DashboardPage({ searchParams, }: { searchParams: Promise; }) { const apiKey = (await cookies()).get("xmrcheckout_api_key")?.value; const webhookSecret = (await cookies()).get("xmrcheckout_webhook_secret")?.value ?? null; if (!apiKey) { redirect("/?login=1"); } const resolvedSearchParams = await searchParams; const tabParam = resolvedSearchParams?.tab; const activeTabValue = Array.isArray(tabParam) ? tabParam[0] : tabParam; const activeTab: DashboardTab = tabs.includes(activeTabValue as DashboardTab) ? (activeTabValue as DashboardTab) : "overview"; const webhookTabParam = resolvedSearchParams?.webhook_tab; const activeWebhookTabValue = Array.isArray(webhookTabParam) ? webhookTabParam[0] : webhookTabParam; const activeWebhookTab: WebhookTab = webhookTabs.includes( activeWebhookTabValue as WebhookTab ) ? (activeWebhookTabValue as WebhookTab) : "settings"; const includeArchived = activeTab === "invoices" && (resolvedSearchParams?.archived === "1" || resolvedSearchParams?.archived === "true"); const qParam = resolvedSearchParams?.q; const queryValue = Array.isArray(qParam) ? qParam[0] : qParam; const invoiceSearchQuery = activeTab === "invoices" ? (queryValue ?? "") : ""; const sortParam = resolvedSearchParams?.sort; const sortValue = Array.isArray(sortParam) ? sortParam[0] : sortParam; const invoiceSort = activeTab === "invoices" && typeof sortValue === "string" && sortValue ? sortValue : "created_at"; const orderParam = resolvedSearchParams?.order; const orderValue = Array.isArray(orderParam) ? orderParam[0] : orderParam; const invoiceOrder = activeTab === "invoices" && typeof orderValue === "string" && orderValue ? orderValue : "desc"; // API key is already available from the cookie const apiKeyData = activeTab === "profile" ? { api_key: apiKey } : null; let webhooksData: WebhookSummary[] = []; let webhookHistory: WebhookDeliverySummary[] = []; const invoiceLimit = 100; let invoicesData: { items: InvoiceSummary[]; total: number } | null = null; let offset = 0; let total = 0; const items: InvoiceSummary[] = []; do { const invoiceUrl = new URL(`${apiBaseUrl}/api/core/invoices`); invoiceUrl.searchParams.set("limit", invoiceLimit.toString()); invoiceUrl.searchParams.set("offset", offset.toString()); if (includeArchived) { invoiceUrl.searchParams.set("include_archived", "true"); } if (invoiceSearchQuery) { invoiceUrl.searchParams.set("q", invoiceSearchQuery); } if (invoiceSort) { invoiceUrl.searchParams.set("sort", invoiceSort); } if (invoiceOrder) { invoiceUrl.searchParams.set("order", invoiceOrder); } const response = await fetch(invoiceUrl.toString(), { headers: { Authorization: `ApiKey ${apiKey}`, }, cache: "no-store", }); if (response.status === 401) { redirect("/?login=1"); } if (!response.ok) { break; } const data = (await response.json()) as { items: InvoiceSummary[]; total: number; }; total = data.total; items.push(...data.items); offset += invoiceLimit; } while (offset < total); invoicesData = { items, total }; if ( activeTab === "overview" || (activeTab === "webhooks" && activeWebhookTab === "settings") ) { const response = await fetch(`${apiBaseUrl}/api/core/webhooks`, { headers: { Authorization: `ApiKey ${apiKey}`, }, cache: "no-store", }); if (response.status === 401) { redirect("/?login=1"); } if (response.ok) { webhooksData = (await response.json()) as WebhookSummary[]; } } if (activeTab === "webhooks" && activeWebhookTab === "history") { const historyUrl = new URL(`${apiBaseUrl}/api/core/webhooks/history`); historyUrl.searchParams.set("limit", "50"); const response = await fetch(historyUrl.toString(), { headers: { Authorization: `ApiKey ${apiKey}`, }, cache: "no-store", }); if (response.status === 401) { redirect("/?login=1"); } if (response.ok) { webhookHistory = (await response.json()) as WebhookDeliverySummary[]; } } const allInvoices = invoicesData?.items ?? []; let profileData: ProfileSummary | null = null; if (activeTab === "overview" || activeTab === "profile" || activeTab === "invoices") { const response = await fetch(`${apiBaseUrl}/api/core/profile`, { headers: { Authorization: `ApiKey ${apiKey}`, }, cache: "no-store", }); if (response.status === 401) { redirect("/?login=1"); } if (response.ok) { profileData = (await response.json()) as ProfileSummary; } } const isToday = (value: string) => { const date = new Date(value); const today = new Date(); return ( date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate() ); }; const invoicesTodayCount = allInvoices.filter((invoice) => isToday(invoice.created_at) ).length; const awaitingConfirmationCount = allInvoices.filter( (invoice) => invoice.status === "payment_detected" ).length; const confirmedCount = allInvoices.filter( (invoice) => invoice.status === "confirmed" ).length; const tabBaseClass = "inline-flex items-center rounded-xl border border-stroke bg-white/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-ink-soft transition hover:-translate-y-0.5"; const tabActiveClass = "border-ink bg-ink text-cream shadow-[0_12px_24px_rgba(16,18,23,0.18)]"; const subTabBaseClass = "inline-flex items-center rounded-full border border-stroke bg-white/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-ink-soft transition hover:-translate-y-0.5"; const subTabActiveClass = "border-ink bg-ink text-cream shadow-[0_10px_18px_rgba(16,18,23,0.18)]"; return (
{activeTab === "overview" ? (

Operational overview.

Track invoice flow and confirmation progress at a glance.

Primary address

{profileData?.payment_address ?? "Unavailable"}

This is the wallet address used for your invoices.

{[ { title: "Invoices today", value: invoicesTodayCount.toString(), detail: "Totals will appear once invoices are created.", }, { title: "Invoices awaiting confirmation", value: awaitingConfirmationCount.toString(), detail: "Waiting for the configured confirmation target.", }, { title: "Confirmed invoices", value: confirmedCount.toString(), detail: "Confirmed on-chain invoices will display here.", }, ].map((item) => (

{item.title}

{item.value}

{item.detail}

))}

Webhook endpoints

{webhooksData.length === 0 ? (

No webhooks configured yet.{" "} Add a webhook endpoint.

) : (
{webhooksData.map((webhook) => (

{webhook.url}

{webhook.active ? "Active" : "Paused"}

Events: {webhook.events.join(", ")}

))}
)}
We never hold funds. All payments move from the customer to your wallet.
) : null} {activeTab === "invoices" ? ( ) : null} {activeTab === "profile" ? ( <> {apiKeyData ? (

Profile settings.

Manage credentials and delivery settings for your integration.

Store id

Single-store per primary address. Use this id in the BTCPay WooCommerce plugin store field.

{profileData?.id ?? "Unavailable"}

API key

Use this key with authenticated endpoints.

Webhook secret

Sent with each delivery as the{" "} X-Webhook-Secret header.

) : (

API key

Sign in again to reveal your API key.

)} ) : null} {activeTab === "webhooks" ? (

Webhook endpoints.

Relay invoice state updates to your systems. Optional overrides let you target different URLs per event.

{activeWebhookTab === "settings" ? (
) : null} {activeWebhookTab === "history" ? (
) : null}
) : null}
); }