fix(rflows): remove Coinbase onramp, use Transak only

Coinbase CDP integration was causing 500 errors ([object Object]).
Simplify to Transak-only: remove CoinbaseOnrampProvider import/init,
provider selection UI, and popup window branch. Also fix error handler
to properly stringify non-Error objects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-10 19:24:18 -07:00
parent 1c336c8616
commit 01a794b0f2
2 changed files with 30 additions and 126 deletions

View File

@ -3504,31 +3504,10 @@ class FolkFlowsApp extends HTMLElement {
* Prompt user for email via a modal dialog.
*/
private promptFundDetails(defaultAmount = 2): Promise<{ email: string; amount: number; label: string; provider: string } | null> {
return new Promise(async (resolve) => {
// Fetch available providers
let availableProviders: string[] = ["transak"];
let defaultProvider = "transak";
try {
const base = this.getApiBase();
const res = await fetch(`${base}/api/onramp/config`);
if (res.ok) {
const cfg = await res.json();
if (cfg.available?.length) availableProviders = cfg.available;
defaultProvider = cfg.provider || defaultProvider;
}
} catch { /* use defaults */ }
return new Promise((resolve) => {
const modal = document.createElement("div");
modal.style.cssText = `position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;`;
const inputStyle = `width:100%;padding:10px 12px;border:1px solid var(--rflows-modal-border);border-radius:8px;font-size:14px;box-sizing:border-box;background:var(--rs-bg-surface);color:var(--rs-text-primary)`;
const providerOptions = availableProviders.map((p) =>
`<option value="${p}"${p === defaultProvider ? ' selected' : ''}>${p === 'coinbase' ? 'Coinbase (0% fee)' : 'Transak (card)'}</option>`
).join("");
const providerPicker = availableProviders.length > 1 ? `
<label style="display:block;margin-bottom:12px">
<span style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;color:var(--rs-text-secondary)">Payment Provider</span>
<select id="fund-provider" style="${inputStyle}">${providerOptions}</select>
</label>` : `<input type="hidden" id="fund-provider" value="${defaultProvider}"/>`;
modal.innerHTML = `
<div style="background:var(--rs-bg-surface);border-radius:16px;padding:28px;width:400px;max-width:90vw">
@ -3543,7 +3522,7 @@ class FolkFlowsApp extends HTMLElement {
<span style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;color:var(--rs-text-secondary)">Recipient Email</span>
<input id="fund-email" type="email" placeholder="friend@example.com"
style="${inputStyle}"/>
</label>${providerPicker}
</label>
<label style="display:block;margin-bottom:20px">
<span style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;color:var(--rs-text-secondary)">Label (optional)</span>
<input id="fund-label" type="text" placeholder="Coffee Fund"
@ -3559,7 +3538,6 @@ class FolkFlowsApp extends HTMLElement {
const amountInput = modal.querySelector("#fund-amount") as HTMLInputElement;
const emailInput = modal.querySelector("#fund-email") as HTMLInputElement;
const labelInput = modal.querySelector("#fund-label") as HTMLInputElement;
const providerInput = modal.querySelector("#fund-provider") as HTMLInputElement | HTMLSelectElement;
emailInput.focus();
const cleanup = (value: { email: string; amount: number; label: string; provider: string } | null) => { modal.remove(); resolve(value); };
@ -3569,7 +3547,7 @@ class FolkFlowsApp extends HTMLElement {
const amount = parseFloat(amountInput.value) || 0;
if (!email || !email.includes("@")) { emailInput.style.borderColor = "red"; return; }
if (amount <= 0) { amountInput.style.borderColor = "red"; return; }
cleanup({ email, amount, label: labelInput.value.trim(), provider: providerInput.value });
cleanup({ email, amount, label: labelInput.value.trim(), provider: "transak" });
};
modal.querySelector("#fund-cancel")!.addEventListener("click", () => cleanup(null));
@ -3751,14 +3729,10 @@ class FolkFlowsApp extends HTMLElement {
}
/**
* Open on-ramp widget. Coinbase blocks iframing (CSP frame-ancestors),
* so we open it in a popup window. Transak allows iframing.
* Open Transak on-ramp widget in an iframe modal.
*/
private openWidgetModal(url: string) {
const isCoinbase = url.includes("pay.coinbase.com") || url.includes("coinbase");
const showClaimMessage = () => {
// Remove any existing modal first
document.getElementById("onramp-modal")?.remove();
const successModal = document.createElement("div");
successModal.id = "onramp-modal";
@ -3782,41 +3756,6 @@ class FolkFlowsApp extends HTMLElement {
successModal.querySelector("#onramp-done")!.addEventListener("click", () => successModal.remove());
};
if (isCoinbase) {
// Coinbase: open in popup window (CSP blocks iframing)
const popup = window.open(url, "coinbase-onramp", "width=460,height=700,left=200,top=100");
// Show a waiting modal
const modal = document.createElement("div");
modal.id = "onramp-modal";
modal.style.cssText = `position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;`;
modal.innerHTML = `
<div style="position:relative;width:450px;border-radius:16px;overflow:hidden;background:var(--rs-bg-surface);padding:40px;text-align:center">
<button id="onramp-close" style="position:absolute;top:8px;right:12px;background:none;border:none;color:var(--rs-text-secondary);font-size:24px;cursor:pointer">&times;</button>
<div style="font-size:48px;margin-bottom:16px">&#128179;</div>
<h2 style="color:var(--rs-text-primary);margin-bottom:12px;font-size:20px">Complete Payment</h2>
<p style="color:var(--rs-text-secondary);font-size:14px;line-height:1.6;margin-bottom:24px">
A Coinbase payment window has opened.<br>
Complete your purchase there, then return here.
</p>
<button id="onramp-done" style="padding:12px 32px;background:linear-gradient(90deg,#00d4ff,#7c3aed);color:#fff;border:none;border-radius:8px;font-weight:600;font-size:15px;cursor:pointer;width:100%">I've Completed Payment</button>
</div>`;
document.body.appendChild(modal);
modal.querySelector("#onramp-close")!.addEventListener("click", () => { popup?.close(); modal.remove(); });
modal.querySelector("#onramp-done")!.addEventListener("click", () => { popup?.close(); modal.remove(); showClaimMessage(); });
// Also detect if popup closes
const checkClosed = setInterval(() => {
if (popup?.closed) {
clearInterval(checkClosed);
modal.remove();
showClaimMessage();
}
}, 1000);
return;
}
// Transak: use iframe (they allow it)
const modal = document.createElement("div");
modal.id = "onramp-modal";
modal.style.cssText = `position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;`;
@ -3834,7 +3773,6 @@ class FolkFlowsApp extends HTMLElement {
modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });
const handler = (e: MessageEvent) => {
// Transak success event
if (e.data?.event_id === "TRANSAK_ORDER_SUCCESSFUL") {
console.log("[OnRamp] Transak order successful:", e.data.data);
window.removeEventListener("message", handler);

View File

@ -14,11 +14,9 @@ import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
import { demoNodes } from './lib/presets';
import { CoinbaseOnrampProvider } from './lib/coinbase-onramp';
import { OpenfortProvider } from './lib/openfort';
let _syncServer: SyncServer | null = null;
let _coinbaseOnramp: CoinbaseOnrampProvider | null = null;
let _openfort: OpenfortProvider | null = null;
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
@ -172,10 +170,7 @@ routes.post("/api/flows/user-onramp", async (c) => {
if (!_openfort) return c.json({ error: "Openfort not configured" }, 503);
// Determine provider: explicit request > env default > auto-detect
const provider = reqProvider
|| process.env.ONRAMP_PROVIDER
|| (_coinbaseOnramp ? 'coinbase' : 'transak');
const provider = 'transak';
// 1. Find or create Openfort smart wallet for this user (one wallet per email)
const wallet = await _openfort.findOrCreateWallet(`user:${email}`, {
@ -184,44 +179,30 @@ routes.post("/api/flows/user-onramp", async (c) => {
});
const sessionId = crypto.randomUUID();
let widgetUrl: string;
if (provider === 'coinbase') {
// 2a. Coinbase: server-side session → widget URL
if (!_coinbaseOnramp) return c.json({ error: "Coinbase Onramp not configured" }, 503);
const session = await _coinbaseOnramp.createSession({
walletAddress: wallet.address,
fiatAmount,
fiatCurrency,
partnerUserRef: `user-${sessionId}`,
redirectUrl: returnUrl,
});
widgetUrl = session.onrampUrl;
} else {
// 2b. Transak: build widget URL server-side
const transakApiKey = process.env.TRANSAK_API_KEY;
if (!transakApiKey) return c.json({ error: "Transak not configured" }, 503);
const transakEnv = process.env.TRANSAK_ENV || 'PRODUCTION';
const baseUrl = transakEnv === 'PRODUCTION'
? 'https://global.transak.com'
: 'https://global-stg.transak.com';
const params = new URLSearchParams({
apiKey: transakApiKey,
environment: transakEnv,
cryptoCurrencyCode: 'USDC',
network: 'base',
defaultCryptoCurrency: 'USDC',
walletAddress: wallet.address,
partnerOrderId: `user-${sessionId}`,
email,
themeColor: '6366f1',
hideMenu: 'true',
});
if (returnUrl) params.set('redirectURL', returnUrl);
widgetUrl = `${baseUrl}?${params}`;
}
// 2. Transak: build widget URL server-side
const transakApiKey = process.env.TRANSAK_API_KEY;
if (!transakApiKey) return c.json({ error: "Transak not configured" }, 503);
const transakEnv = process.env.TRANSAK_ENV || 'PRODUCTION';
const baseUrl = transakEnv === 'PRODUCTION'
? 'https://global.transak.com'
: 'https://global-stg.transak.com';
const params = new URLSearchParams({
apiKey: transakApiKey,
environment: transakEnv,
cryptoCurrencyCode: 'USDC',
network: 'base',
defaultCryptoCurrency: 'USDC',
walletAddress: wallet.address,
partnerOrderId: `user-${sessionId}`,
email,
themeColor: '6366f1',
hideMenu: 'true',
});
if (returnUrl) params.set('redirectURL', returnUrl);
const widgetUrl = `${baseUrl}?${params}`;
console.log(`[rflows] On-ramp session created: provider=${provider} session=${sessionId} wallet=${wallet.address}`);
console.log(`[rflows] On-ramp session created: provider=transak session=${sessionId} wallet=${wallet.address}`);
// Non-fatal side-effect: create fund claim → sends email via EncryptID
const encryptidServiceKey = process.env.ENCRYPTID_SERVICE_KEY;
@ -260,7 +241,7 @@ routes.post("/api/flows/user-onramp", async (c) => {
});
} catch (err) {
console.error("[rflows] user-onramp failed:", err);
const message = err instanceof Error ? err.message : "Unknown error";
const message = err instanceof Error ? err.message : String(err);
return c.json({ error: message }, 500);
}
});
@ -268,13 +249,9 @@ routes.post("/api/flows/user-onramp", async (c) => {
// ─── On-ramp config ──────────────────────────────────────
routes.get("/api/onramp/config", (c) => {
const available: string[] = [];
if (process.env.COINBASE_CDP_PROJECT_ID) available.push("coinbase");
if (process.env.TRANSAK_API_KEY || !process.env.COINBASE_CDP_PROJECT_ID) available.push("transak");
return c.json({
provider: process.env.ONRAMP_PROVIDER || (process.env.COINBASE_CDP_PROJECT_ID ? "coinbase" : "transak"),
available,
// Transak fields (only needed if provider=transak)
provider: "transak",
available: ["transak"],
apiKey: process.env.TRANSAK_API_KEY || "",
environment: process.env.TRANSAK_ENV || "PRODUCTION",
});
@ -490,17 +467,6 @@ export const flowsModule: RSpaceModule = {
async onInit(ctx) {
_syncServer = ctx.syncServer;
// Initialize on-ramp providers if env vars are set
if (process.env.COINBASE_CDP_KEY_ID && process.env.COINBASE_CDP_KEY_SECRET && process.env.COINBASE_CDP_PROJECT_ID) {
_coinbaseOnramp = new CoinbaseOnrampProvider({
apiKeyId: process.env.COINBASE_CDP_KEY_ID,
apiKeySecret: process.env.COINBASE_CDP_KEY_SECRET,
projectId: process.env.COINBASE_CDP_PROJECT_ID,
environment: (process.env.COINBASE_ENVIRONMENT as 'sandbox' | 'production') || 'production',
});
console.log('[rflows] Coinbase Onramp provider initialized');
}
if (process.env.OPENFORT_API_KEY && process.env.OPENFORT_PUBLISHABLE_KEY) {
_openfort = new OpenfortProvider({
apiKey: process.env.OPENFORT_API_KEY,