feat(rcart): add payment context, staging banner, and Transak amount pre-fill

- Payment page now shows recipient info and available payment methods
- Testnet chains show staging environment disclaimer
- Transak widget receives effective amount for editable-amount payments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 20:40:58 +00:00
parent 12f25edaf3
commit d6be2f2039
2 changed files with 34 additions and 4 deletions

View File

@ -140,10 +140,11 @@ class FolkPaymentPage extends HTMLElement {
this.render();
try {
const effectiveAmount = this.getEffectiveAmount();
const res = await fetch(`${this.getApiBase()}/api/payments/${this.paymentId}/transak-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: this.cardEmail }),
body: JSON.stringify({ email: this.cardEmail, ...(effectiveAmount ? { amount: effectiveAmount } : {}) }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create session');
@ -391,16 +392,38 @@ class FolkPaymentPage extends HTMLElement {
const chainNames: Record<number, string> = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' };
const chainName = chainNames[p.chainId] || `Chain ${p.chainId}`;
const isTestnet = p.chainId === 84532;
const showAmountInput = p.amountEditable && p.status === 'pending';
const displayAmount = (!p.amount || p.amount === '0') && p.amountEditable ? 'Any amount' : `${p.amount} ${p.token}`;
// Derive a short recipient label from creatorDid
const recipientLabel = p.creatorDid
? (p.creatorDid.startsWith('did:') ? p.creatorDid.split(':').pop()?.slice(0, 12) + '...' : p.creatorDid.slice(0, 16) + '...')
: p.recipientAddress.slice(0, 8) + '...' + p.recipientAddress.slice(-6);
// Build available methods description
const methods = p.enabledMethods || { card: true, wallet: true, encryptid: true };
const methodLabels: string[] = [];
if (methods.card) methodLabels.push('Credit Card (via Transak)');
if (methods.wallet) methodLabels.push('External Wallet (MetaMask, etc.)');
if (methods.encryptid) methodLabels.push('EncryptID Passkey');
return `
${isTestnet ? `<div class="staging-banner">
This is a staging environment for testing credit card to stablecoin and asset-backed CRDT token transactions. Your credit card will not be charged.
</div>` : ''}
<div class="header">
<h1 class="title">Payment Request</h1>
<div class="status-badge status-${p.status}">${p.status}</div>
</div>
<div class="recipient-info">
You are sending a payment to <strong>${this.esc(recipientLabel)}</strong>${p.description ? ` for <strong>${this.esc(p.description)}</strong>` : ''}.
${methodLabels.length > 0 ? `You can pay by: ${methodLabels.join(', ')}.` : ''}
</div>
<div class="amount-display">
${showAmountInput ? `
<div class="editable-amount">
@ -417,7 +440,7 @@ class FolkPaymentPage extends HTMLElement {
`}
</div>
<div class="description">${this.esc(p.description)}</div>
${p.description ? `<div class="description">${this.esc(p.description)}</div>` : ''}
${p.paymentType === 'subscription' ? '<div class="type-badge">Subscription</div>' : ''}
${p.paymentType === 'payer_choice' && p.status === 'pending' ? `
@ -633,6 +656,10 @@ class FolkPaymentPage extends HTMLElement {
.payment-page { }
.staging-banner { background: rgba(251,191,36,0.12); border: 1px solid rgba(251,191,36,0.3); color: #fbbf24; border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.8125rem; line-height: 1.5; margin-bottom: 1rem; text-align: center; }
.recipient-info { color: var(--rs-text-secondary); font-size: 0.875rem; line-height: 1.6; margin-bottom: 1.25rem; padding: 0.75rem 1rem; background: var(--rs-bg-surface); border-radius: 8px; border: 1px solid var(--rs-border); }
.recipient-info strong { color: var(--rs-text-primary); }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.title { color: var(--rs-text-primary); font-size: 1.25rem; font-weight: 700; margin: 0; }

View File

@ -1389,7 +1389,7 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
if (p.status !== 'pending') return c.json({ error: "Payment is no longer pending" }, 400);
if (p.enabledMethods && !p.enabledMethods.card) return c.json({ error: "Card payments are not enabled for this request" }, 400);
const { email } = await c.req.json();
const { email, amount: overrideAmount } = await c.req.json();
if (!email) return c.json({ error: "Required: email" }, 400);
const transakApiKey = getTransakApiKey();
@ -1399,6 +1399,9 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
const host = new URL(c.req.url).hostname;
// Use override amount for editable-amount payments, otherwise use preset amount
const effectiveAmount = (p.amountEditable && overrideAmount) ? String(overrideAmount) : p.amount;
const widgetParams: Record<string, string> = {
apiKey: transakApiKey,
referrerDomain: extractRootDomain(host),
@ -1407,7 +1410,7 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
defaultCryptoCurrency: p.token,
walletAddress: p.recipientAddress,
disableWalletAddressForm: 'true',
cryptoAmount: p.amount,
cryptoAmount: effectiveAmount,
partnerOrderId: `pay-${paymentId}`,
email,
themeColor: '6366f1',