From 9edb6cccdee49efecb87a0fe614c5035d147c354 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Mar 2026 16:01:57 -0700 Subject: [PATCH] =?UTF-8?q?fix(rcart):=20Transak=20staging=20payment=20?= =?UTF-8?q?=E2=80=94=20popup=20instead=20of=20blocked=20iframe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- modules/rcart/components/folk-payment-page.ts | 41 +++++++++++++++++++ modules/rcart/mod.ts | 6 ++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/modules/rcart/components/folk-payment-page.ts b/modules/rcart/components/folk-payment-page.ts index 504f820..64fcdd3 100644 --- a/modules/rcart/components/folk-payment-page.ts +++ b/modules/rcart/components/folk-payment-page.ts @@ -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 ` +
+
+

Transak payment opened in a new window.

+

Complete the payment in the popup window. This page will update automatically.

+ +
+
`; + } return `
@@ -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) => { diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index 6bb2595..589796e 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -1759,7 +1759,8 @@ routes.post("/api/payments/:id/transak-session", async (c) => { const networkMap: Record = { 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