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.
|
* Prompt user for email via a modal dialog.
|
||||||
*/
|
*/
|
||||||
private promptFundDetails(defaultAmount = 2): Promise<{ email: string; amount: number; label: string; provider: string } | null> {
|
private promptFundDetails(defaultAmount = 2): Promise<{ email: string; amount: number; label: string; provider: string } | null> {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise((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 */ }
|
|
||||||
|
|
||||||
const modal = document.createElement("div");
|
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;`;
|
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 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 = `
|
modal.innerHTML = `
|
||||||
<div style="background:var(--rs-bg-surface);border-radius:16px;padding:28px;width:400px;max-width:90vw">
|
<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>
|
<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"
|
<input id="fund-email" type="email" placeholder="friend@example.com"
|
||||||
style="${inputStyle}"/>
|
style="${inputStyle}"/>
|
||||||
</label>${providerPicker}
|
</label>
|
||||||
<label style="display:block;margin-bottom:20px">
|
<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>
|
<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"
|
<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 amountInput = modal.querySelector("#fund-amount") as HTMLInputElement;
|
||||||
const emailInput = modal.querySelector("#fund-email") as HTMLInputElement;
|
const emailInput = modal.querySelector("#fund-email") as HTMLInputElement;
|
||||||
const labelInput = modal.querySelector("#fund-label") as HTMLInputElement;
|
const labelInput = modal.querySelector("#fund-label") as HTMLInputElement;
|
||||||
const providerInput = modal.querySelector("#fund-provider") as HTMLInputElement | HTMLSelectElement;
|
|
||||||
emailInput.focus();
|
emailInput.focus();
|
||||||
|
|
||||||
const cleanup = (value: { email: string; amount: number; label: string; provider: string } | null) => { modal.remove(); resolve(value); };
|
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;
|
const amount = parseFloat(amountInput.value) || 0;
|
||||||
if (!email || !email.includes("@")) { emailInput.style.borderColor = "red"; return; }
|
if (!email || !email.includes("@")) { emailInput.style.borderColor = "red"; return; }
|
||||||
if (amount <= 0) { amountInput.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));
|
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),
|
* Open Transak on-ramp widget in an iframe modal.
|
||||||
* so we open it in a popup window. Transak allows iframing.
|
|
||||||
*/
|
*/
|
||||||
private openWidgetModal(url: string) {
|
private openWidgetModal(url: string) {
|
||||||
const isCoinbase = url.includes("pay.coinbase.com") || url.includes("coinbase");
|
|
||||||
|
|
||||||
const showClaimMessage = () => {
|
const showClaimMessage = () => {
|
||||||
// Remove any existing modal first
|
|
||||||
document.getElementById("onramp-modal")?.remove();
|
document.getElementById("onramp-modal")?.remove();
|
||||||
const successModal = document.createElement("div");
|
const successModal = document.createElement("div");
|
||||||
successModal.id = "onramp-modal";
|
successModal.id = "onramp-modal";
|
||||||
|
|
@ -3782,41 +3756,6 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
successModal.querySelector("#onramp-done")!.addEventListener("click", () => successModal.remove());
|
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");
|
const modal = document.createElement("div");
|
||||||
modal.id = "onramp-modal";
|
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.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(); });
|
modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });
|
||||||
|
|
||||||
const handler = (e: MessageEvent) => {
|
const handler = (e: MessageEvent) => {
|
||||||
// Transak success event
|
|
||||||
if (e.data?.event_id === "TRANSAK_ORDER_SUCCESSFUL") {
|
if (e.data?.event_id === "TRANSAK_ORDER_SUCCESSFUL") {
|
||||||
console.log("[OnRamp] Transak order successful:", e.data.data);
|
console.log("[OnRamp] Transak order successful:", e.data.data);
|
||||||
window.removeEventListener("message", handler);
|
window.removeEventListener("message", handler);
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,9 @@ import { renderLanding } from "./landing";
|
||||||
import type { SyncServer } from '../../server/local-first/sync-server';
|
import type { SyncServer } from '../../server/local-first/sync-server';
|
||||||
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
|
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
|
||||||
import { demoNodes } from './lib/presets';
|
import { demoNodes } from './lib/presets';
|
||||||
import { CoinbaseOnrampProvider } from './lib/coinbase-onramp';
|
|
||||||
import { OpenfortProvider } from './lib/openfort';
|
import { OpenfortProvider } from './lib/openfort';
|
||||||
|
|
||||||
let _syncServer: SyncServer | null = null;
|
let _syncServer: SyncServer | null = null;
|
||||||
let _coinbaseOnramp: CoinbaseOnrampProvider | null = null;
|
|
||||||
let _openfort: OpenfortProvider | null = null;
|
let _openfort: OpenfortProvider | null = null;
|
||||||
|
|
||||||
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
|
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);
|
if (!_openfort) return c.json({ error: "Openfort not configured" }, 503);
|
||||||
|
|
||||||
// Determine provider: explicit request > env default > auto-detect
|
const provider = 'transak';
|
||||||
const provider = reqProvider
|
|
||||||
|| process.env.ONRAMP_PROVIDER
|
|
||||||
|| (_coinbaseOnramp ? 'coinbase' : 'transak');
|
|
||||||
|
|
||||||
// 1. Find or create Openfort smart wallet for this user (one wallet per email)
|
// 1. Find or create Openfort smart wallet for this user (one wallet per email)
|
||||||
const wallet = await _openfort.findOrCreateWallet(`user:${email}`, {
|
const wallet = await _openfort.findOrCreateWallet(`user:${email}`, {
|
||||||
|
|
@ -184,44 +179,30 @@ routes.post("/api/flows/user-onramp", async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionId = crypto.randomUUID();
|
const sessionId = crypto.randomUUID();
|
||||||
let widgetUrl: string;
|
|
||||||
|
|
||||||
if (provider === 'coinbase') {
|
// 2. Transak: build widget URL server-side
|
||||||
// 2a. Coinbase: server-side session → widget URL
|
const transakApiKey = process.env.TRANSAK_API_KEY;
|
||||||
if (!_coinbaseOnramp) return c.json({ error: "Coinbase Onramp not configured" }, 503);
|
if (!transakApiKey) return c.json({ error: "Transak not configured" }, 503);
|
||||||
const session = await _coinbaseOnramp.createSession({
|
const transakEnv = process.env.TRANSAK_ENV || 'PRODUCTION';
|
||||||
walletAddress: wallet.address,
|
const baseUrl = transakEnv === 'PRODUCTION'
|
||||||
fiatAmount,
|
? 'https://global.transak.com'
|
||||||
fiatCurrency,
|
: 'https://global-stg.transak.com';
|
||||||
partnerUserRef: `user-${sessionId}`,
|
const params = new URLSearchParams({
|
||||||
redirectUrl: returnUrl,
|
apiKey: transakApiKey,
|
||||||
});
|
environment: transakEnv,
|
||||||
widgetUrl = session.onrampUrl;
|
cryptoCurrencyCode: 'USDC',
|
||||||
} else {
|
network: 'base',
|
||||||
// 2b. Transak: build widget URL server-side
|
defaultCryptoCurrency: 'USDC',
|
||||||
const transakApiKey = process.env.TRANSAK_API_KEY;
|
walletAddress: wallet.address,
|
||||||
if (!transakApiKey) return c.json({ error: "Transak not configured" }, 503);
|
partnerOrderId: `user-${sessionId}`,
|
||||||
const transakEnv = process.env.TRANSAK_ENV || 'PRODUCTION';
|
email,
|
||||||
const baseUrl = transakEnv === 'PRODUCTION'
|
themeColor: '6366f1',
|
||||||
? 'https://global.transak.com'
|
hideMenu: 'true',
|
||||||
: 'https://global-stg.transak.com';
|
});
|
||||||
const params = new URLSearchParams({
|
if (returnUrl) params.set('redirectURL', returnUrl);
|
||||||
apiKey: transakApiKey,
|
const widgetUrl = `${baseUrl}?${params}`;
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// Non-fatal side-effect: create fund claim → sends email via EncryptID
|
||||||
const encryptidServiceKey = process.env.ENCRYPTID_SERVICE_KEY;
|
const encryptidServiceKey = process.env.ENCRYPTID_SERVICE_KEY;
|
||||||
|
|
@ -260,7 +241,7 @@ routes.post("/api/flows/user-onramp", async (c) => {
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[rflows] user-onramp failed:", 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);
|
return c.json({ error: message }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -268,13 +249,9 @@ routes.post("/api/flows/user-onramp", async (c) => {
|
||||||
// ─── On-ramp config ──────────────────────────────────────
|
// ─── On-ramp config ──────────────────────────────────────
|
||||||
|
|
||||||
routes.get("/api/onramp/config", (c) => {
|
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({
|
return c.json({
|
||||||
provider: process.env.ONRAMP_PROVIDER || (process.env.COINBASE_CDP_PROJECT_ID ? "coinbase" : "transak"),
|
provider: "transak",
|
||||||
available,
|
available: ["transak"],
|
||||||
// Transak fields (only needed if provider=transak)
|
|
||||||
apiKey: process.env.TRANSAK_API_KEY || "",
|
apiKey: process.env.TRANSAK_API_KEY || "",
|
||||||
environment: process.env.TRANSAK_ENV || "PRODUCTION",
|
environment: process.env.TRANSAK_ENV || "PRODUCTION",
|
||||||
});
|
});
|
||||||
|
|
@ -490,17 +467,6 @@ export const flowsModule: RSpaceModule = {
|
||||||
async onInit(ctx) {
|
async onInit(ctx) {
|
||||||
_syncServer = ctx.syncServer;
|
_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) {
|
if (process.env.OPENFORT_API_KEY && process.env.OPENFORT_PUBLISHABLE_KEY) {
|
||||||
_openfort = new OpenfortProvider({
|
_openfort = new OpenfortProvider({
|
||||||
apiKey: process.env.OPENFORT_API_KEY,
|
apiKey: process.env.OPENFORT_API_KEY,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue