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:
parent
1c336c8616
commit
01a794b0f2
|
|
@ -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">×</button>
|
||||
<div style="font-size:48px;margin-bottom:16px">💳</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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue