diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts
index 2b2f877..66b53af 100644
--- a/modules/rflows/components/folk-flows-app.ts
+++ b/modules/rflows/components/folk-flows-app.ts
@@ -3764,7 +3764,8 @@ class FolkFlowsApp extends HTMLElement {
+ allow="camera;microphone;payment"
+ referrerpolicy="strict-origin-when-cross-origin">
`;
document.body.appendChild(modal);
diff --git a/modules/rflows/mod.ts b/modules/rflows/mod.ts
index 2b525eb..f630c28 100644
--- a/modules/rflows/mod.ts
+++ b/modules/rflows/mod.ts
@@ -21,6 +21,88 @@ let _openfort: OpenfortProvider | null = null;
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
+// ─── Transak API-based widget URL (mandatory since migration) ────
+let _transakAccessToken: string | null = null;
+let _transakTokenExpiry = 0;
+
+async function getTransakAccessToken(): Promise {
+ if (_transakAccessToken && Date.now() < _transakTokenExpiry) return _transakAccessToken;
+
+ const apiKey = process.env.TRANSAK_API_KEY;
+ const apiSecret = process.env.TRANSAK_SECRET;
+ if (!apiKey || !apiSecret) throw new Error("Transak credentials not configured");
+
+ const env = process.env.TRANSAK_ENV || 'PRODUCTION';
+ const baseUrl = env === 'PRODUCTION'
+ ? 'https://api.transak.com'
+ : 'https://api-stg.transak.com';
+
+ const res = await fetch(`${baseUrl}/partners/api/v2/refresh-token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'api-secret': apiSecret,
+ },
+ body: JSON.stringify({ apiKey }),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Transak token refresh failed (${res.status}): ${text}`);
+ }
+
+ const data = await res.json() as any;
+ _transakAccessToken = data.data?.accessToken || data.accessToken;
+ if (!_transakAccessToken) throw new Error("No accessToken in Transak response");
+
+ // Cache for 6 days (tokens valid for 7)
+ _transakTokenExpiry = Date.now() + 6 * 24 * 60 * 60 * 1000;
+ console.log('[rflows] Transak access token refreshed');
+ return _transakAccessToken;
+}
+
+async function createTransakWidgetUrl(params: Record): Promise {
+ const accessToken = await getTransakAccessToken();
+ const env = process.env.TRANSAK_ENV || 'PRODUCTION';
+ const gatewayUrl = env === 'PRODUCTION'
+ ? 'https://api-gateway.transak.com'
+ : 'https://api-gateway-stg.transak.com';
+
+ const res = await fetch(`${gatewayUrl}/api/v2/auth/session`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'access-token': accessToken,
+ },
+ body: JSON.stringify({ widgetParams: params }),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ // If token expired, clear cache and retry once
+ if (res.status === 401 || res.status === 403) {
+ _transakAccessToken = null;
+ _transakTokenExpiry = 0;
+ const retryToken = await getTransakAccessToken();
+ const retry = await fetch(`${gatewayUrl}/api/v2/auth/session`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'access-token': retryToken,
+ },
+ body: JSON.stringify({ widgetParams: params }),
+ });
+ if (!retry.ok) throw new Error(`Transak widget URL failed on retry (${retry.status}): ${await retry.text()}`);
+ const retryData = await retry.json() as any;
+ return retryData.data?.widgetUrl;
+ }
+ throw new Error(`Transak widget URL failed (${res.status}): ${text}`);
+ }
+
+ const data = await res.json() as any;
+ return data.data?.widgetUrl;
+}
+
function ensureDoc(space: string): FlowsDoc {
const docId = flowsDocId(space);
let doc = _syncServer!.getDoc(docId);
@@ -180,16 +262,13 @@ routes.post("/api/flows/user-onramp", async (c) => {
const sessionId = crypto.randomUUID();
- // 2. Transak: build widget URL server-side
+ // 2. Transak: create widget URL via API
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({
+
+ const widgetParams: Record = {
apiKey: transakApiKey,
- environment: transakEnv,
+ referrerDomain: 'rspace.online',
cryptoCurrencyCode: 'USDC',
network: 'base',
defaultCryptoCurrency: 'USDC',
@@ -198,9 +277,9 @@ routes.post("/api/flows/user-onramp", async (c) => {
email,
themeColor: '6366f1',
hideMenu: 'true',
- });
- if (returnUrl) params.set('redirectURL', returnUrl);
- const widgetUrl = `${baseUrl}?${params}`;
+ };
+ if (returnUrl) widgetParams.redirectURL = returnUrl;
+ const widgetUrl = await createTransakWidgetUrl(widgetParams);
console.log(`[rflows] On-ramp session created: provider=transak session=${sessionId} wallet=${wallet.address}`);
@@ -255,15 +334,13 @@ routes.get("/api/onramp/config", (c) => {
return c.json({
provider: "transak",
available: ["transak"],
- apiKey: process.env.TRANSAK_API_KEY || "",
- environment: process.env.TRANSAK_ENV || "PRODUCTION",
});
});
// Legacy endpoint — keep for backwards compat
routes.get("/api/transak/config", (c) => {
return c.json({
- apiKey: process.env.TRANSAK_API_KEY || "",
+ provider: "transak",
environment: process.env.TRANSAK_ENV || "PRODUCTION",
});
});