fix(rcart): graceful server-side page for paid/expired/cancelled payments

When a payment request is in a terminal state (paid, confirmed, expired,
cancelled, filled), the /pay/:id route now renders a static HTML page
with a clear message instead of loading the full JS component. Prevents
"corrupted content error" and shows a friendly "already paid" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-03 14:58:35 -07:00
parent f9dc06394c
commit 55067729b1
1 changed files with 43 additions and 0 deletions

View File

@ -2535,6 +2535,49 @@ routes.get("/request", (c) => {
routes.get("/pay/:id", (c) => {
const space = c.req.param("space") || "demo";
const paymentId = c.req.param("id");
// Check payment status server-side for graceful terminal-state messages
const docId = paymentRequestDocId(space, paymentId);
const doc = _syncServer?.getDoc<PaymentRequestDoc>(docId);
if (doc) {
const p = doc.payment;
const terminalStates: Record<string, { title: string; msg: string; icon: string }> = {
paid: { title: 'Payment Complete', msg: 'This payment request has already been paid.', icon: '&#10003;' },
confirmed: { title: 'Payment Confirmed', msg: 'This payment has been confirmed on-chain.', icon: '&#10003;' },
expired: { title: 'Payment Expired', msg: 'This payment request has expired and is no longer accepting payments.', icon: '&#9202;' },
cancelled: { title: 'Payment Cancelled', msg: 'This payment request has been cancelled by the creator.', icon: '&#10007;' },
filled: { title: 'Payment Limit Reached', msg: 'This payment request has reached its maximum number of payments.', icon: '&#10003;' },
};
const info = terminalStates[p.status];
if (info) {
const chainNames: Record<number, string> = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' };
const explorerBase: Record<number, string> = { 8453: 'https://basescan.org/tx/', 84532: 'https://sepolia.basescan.org/tx/', 1: 'https://etherscan.io/tx/' };
const txLink = p.txHash && explorerBase[p.chainId]
? `<a href="${explorerBase[p.chainId]}${p.txHash}" target="_blank" rel="noopener" style="color:#60a5fa;text-decoration:underline">${p.txHash.slice(0, 10)}...${p.txHash.slice(-8)}</a>`
: '';
return c.html(renderShell({
title: `${info.title} | rCart`,
moduleId: "rcart",
spaceSlug: space,
spaceVisibility: "public",
modules: getModuleInfoList(),
theme: "dark",
body: `
<div style="max-width:480px;margin:60px auto;padding:32px;text-align:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#e2e8f0">
<div style="font-size:48px;margin-bottom:16px;color:${p.status === 'paid' || p.status === 'confirmed' || p.status === 'filled' ? '#4ade80' : p.status === 'expired' ? '#fbbf24' : '#f87171'}">${info.icon}</div>
<h1 style="font-size:24px;font-weight:600;margin:0 0 12px">${info.title}</h1>
<p style="color:#94a3b8;font-size:15px;line-height:1.6;margin:0 0 24px">${info.msg}</p>
${p.amount && p.amount !== '0' ? `<div style="font-size:20px;font-weight:600;margin-bottom:8px">${p.amount} ${p.token}</div>` : ''}
${p.fiatAmount ? `<div style="color:#94a3b8;font-size:14px;margin-bottom:16px">&asymp; $${p.fiatAmount} ${p.fiatCurrency || 'USD'}</div>` : ''}
${chainNames[p.chainId] ? `<div style="color:#64748b;font-size:13px;margin-bottom:8px">Network: ${chainNames[p.chainId]}</div>` : ''}
${txLink ? `<div style="font-size:13px;margin-bottom:8px">Tx: ${txLink}</div>` : ''}
${p.paidAt ? `<div style="color:#64748b;font-size:13px">Paid: ${new Date(p.paidAt).toLocaleString()}</div>` : ''}
</div>`,
styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
}));
}
}
return c.html(renderShell({
title: `Payment | rCart`,
moduleId: "rcart",