feat(rflows): migrate to Transak API-based widget URL
Transak deprecated direct query-parameter URLs. The new flow uses their Create Widget URL API with a Partner Access Token to generate one-time sessionId-based URLs server-side. Also stops exposing the API key in config endpoints and adds referrerpolicy to the iframe. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3f71b219bb
commit
e8a54f1eb6
|
|
@ -3764,7 +3764,8 @@ class FolkFlowsApp extends HTMLElement {
|
|||
<button id="onramp-close" style="position:absolute;top:8px;right:12px;z-index:10;
|
||||
background:none;border:none;color:white;font-size:24px;cursor:pointer">×</button>
|
||||
<iframe src="${url}" style="width:100%;height:100%;border:none"
|
||||
allow="camera;microphone;payment"></iframe>
|
||||
allow="camera;microphone;payment"
|
||||
referrerpolicy="strict-origin-when-cross-origin"></iframe>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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<string, string>): Promise<string> {
|
||||
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<FlowsDoc>(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<string, string> = {
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue