fix(rcart): Transak staging payment — popup instead of blocked iframe

Transak staging sets X-Frame-Options: sameorigin, blocking iframe embedding.
Server now uses x-forwarded-host header for correct referrerDomain behind
Traefik, and returns env (STAGING/PRODUCTION) in transak-session response.
Client opens a popup window for staging instead of iframe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-28 16:01:57 -07:00
parent bbbe14246c
commit 9edb6cccde
2 changed files with 45 additions and 2 deletions

View File

@ -23,6 +23,8 @@ class FolkPaymentPage extends HTMLElement {
private cardEmail = '';
private cardLoading = false;
private transakUrl = '';
private transakEnv: 'STAGING' | 'PRODUCTION' = 'PRODUCTION';
private transakPopup: Window | null = null;
// Wallet tab state
private walletProviders: any[] = [];
@ -64,6 +66,7 @@ class FolkPaymentPage extends HTMLElement {
disconnectedCallback() {
this.stopPolling();
this.walletDiscovery?.stop?.();
window.removeEventListener('message', this.handleTransakMessage);
}
private getApiBase(): string {
@ -148,10 +151,28 @@ class FolkPaymentPage extends HTMLElement {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create session');
this.transakEnv = data.env || 'PRODUCTION';
this.transakUrl = data.widgetUrl;
// Listen for Transak postMessage events
window.addEventListener('message', this.handleTransakMessage);
// Staging blocks iframes (X-Frame-Options: sameorigin) — use popup
if (this.transakEnv === 'STAGING') {
this.transakPopup = window.open(
data.widgetUrl,
'transak-payment',
'width=450,height=700,scrollbars=yes,resizable=yes',
);
// Poll for popup close (user cancelled)
const popupPoll = setInterval(() => {
if (this.transakPopup?.closed) {
clearInterval(popupPoll);
this.transakPopup = null;
}
}, 1000);
}
} catch (e) {
this.error = e instanceof Error ? e.message : String(e);
}
@ -549,6 +570,17 @@ class FolkPaymentPage extends HTMLElement {
private renderCardTab(): string {
if (this.transakUrl) {
// Staging: Transak blocks iframes, show popup status instead
if (this.transakEnv === 'STAGING') {
return `
<div class="tab-body" style="text-align:center">
<div class="transak-popup-status">
<p class="tab-desc">Transak payment opened in a new window.</p>
<p class="tab-desc" style="font-size:0.8125rem; color:var(--rs-text-muted)">Complete the payment in the popup window. This page will update automatically.</p>
<button class="btn" data-action="reopen-transak" style="margin-top:0.5rem">Re-open Payment Window</button>
</div>
</div>`;
}
return `
<div class="transak-container">
<iframe src="${this.transakUrl}" class="transak-iframe" allow="camera;microphone;payment" frameborder="0"></iframe>
@ -654,6 +686,15 @@ class FolkPaymentPage extends HTMLElement {
const emailInput = this.shadow.querySelector('[data-field="card-email"]') as HTMLInputElement;
emailInput?.addEventListener('input', () => { this.cardEmail = emailInput.value; });
this.shadow.querySelector('[data-action="start-transak"]')?.addEventListener('click', () => this.startTransak());
this.shadow.querySelector('[data-action="reopen-transak"]')?.addEventListener('click', () => {
if (this.transakUrl) {
this.transakPopup = window.open(
this.transakUrl,
'transak-payment',
'width=450,height=700,scrollbars=yes,resizable=yes',
);
}
});
// Wallet tab
this.shadow.querySelectorAll('[data-wallet-uuid]').forEach((el) => {

View File

@ -1759,7 +1759,8 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
const networkMap: Record<number, string> = { 8453: 'base', 84532: 'base', 1: 'ethereum' };
const host = new URL(c.req.url).hostname;
// Use forwarded host from reverse proxy, fall back to request URL
const host = c.req.header('x-forwarded-host') || c.req.header('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;
@ -1790,8 +1791,9 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
}
const widgetUrl = createTransakWidgetUrl(widgetParams);
const transakEnv = getTransakEnv();
return c.json({ widgetUrl });
return c.json({ widgetUrl, env: transakEnv });
});
// POST /api/payments/:id/share-email — Email payment link to recipients