fix(transak): use direct widget URL instead of broken gateway API

The Transak gateway session API consistently returns 401 despite valid
access tokens. Switch to direct URL construction (query params on
global.transak.com) which Transak still supports and is simpler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-11 20:32:23 -07:00
parent 1f4b28aee1
commit 96b77278d4
3 changed files with 34 additions and 99 deletions

View File

@ -1368,26 +1368,22 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
const networkMap: Record<number, string> = { 8453: 'base', 84532: 'base', 1: 'ethereum' };
try {
const widgetUrl = await createTransakWidgetUrl({
apiKey: transakApiKey,
referrerDomain: 'rspace.online',
cryptoCurrencyCode: p.token,
network: networkMap[p.chainId] || 'base',
defaultCryptoCurrency: p.token,
walletAddress: p.recipientAddress,
disableWalletAddressForm: 'true',
cryptoAmount: p.amount,
partnerOrderId: `pay-${paymentId}`,
email,
themeColor: '6366f1',
hideMenu: 'true',
});
const widgetUrl = createTransakWidgetUrl({
apiKey: transakApiKey,
referrerDomain: 'rspace.online',
cryptoCurrencyCode: p.token,
network: networkMap[p.chainId] || 'base',
defaultCryptoCurrency: p.token,
walletAddress: p.recipientAddress,
disableWalletAddressForm: 'true',
cryptoAmount: p.amount,
partnerOrderId: `pay-${paymentId}`,
email,
themeColor: '6366f1',
hideMenu: 'true',
});
return c.json({ widgetUrl });
} catch (err) {
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
}
return c.json({ widgetUrl });
});
function paymentToResponse(p: PaymentRequestMeta) {
@ -1465,7 +1461,7 @@ routes.post("/api/group-buys", async (c) => {
_syncServer!.setDoc(docId, doc);
const host = c.req.header('host') || 'rspace.online';
const shareUrl = `https://${host}/${space}/rcart/buy/${buyId}`;
const shareUrl = `https://${host}/${space}/rcart/group-buy/${buyId}`;
return c.json({ id: buyId, shareUrl }, 201);
});
@ -1575,7 +1571,7 @@ routes.post("/api/group-buys/:id/pledge", async (c) => {
});
// ── Page route: group buy page ──
routes.get("/buy/:id", (c) => {
routes.get("/group-buy/:id", (c) => {
const space = c.req.param("space") || "demo";
const buyId = c.req.param("id");
return c.html(renderShell({
@ -1728,7 +1724,9 @@ export const cartModule: RSpaceModule = {
acceptsFeeds: ["economic", "data"],
outputPaths: [
{ path: "carts", name: "Carts", icon: "🛒", description: "Group shopping carts" },
{ path: "products", name: "Products", icon: "🛍️", description: "Print-on-demand product catalog" },
{ path: "catalog", name: "Catalog", icon: "🛍️", description: "Print-on-demand product catalog" },
{ path: "orders", name: "Orders", icon: "📦", description: "Order history and fulfillment tracking" },
{ path: "payments", name: "Payments", icon: "💳", description: "Payment requests and invoices" },
{ path: "group-buys", name: "Group Buys", icon: "👥", description: "Volume discount group purchasing campaigns" },
],
};

View File

@ -11,7 +11,7 @@ export class TransakOnrampAdapter implements OnrampProvider {
name = 'Transak';
isAvailable(): boolean {
return !!(process.env.TRANSAK_API_KEY && process.env.TRANSAK_SECRET);
return !!process.env.TRANSAK_API_KEY;
}
async createSession(req: OnrampSessionRequest): Promise<OnrampSessionResult> {
@ -32,7 +32,7 @@ export class TransakOnrampAdapter implements OnrampProvider {
};
if (req.returnUrl) widgetParams.redirectURL = req.returnUrl;
const widgetUrl = await createTransakWidgetUrl(widgetParams);
const widgetUrl = createTransakWidgetUrl(widgetParams);
return { widgetUrl, provider: 'transak' };
}
}

View File

@ -1,87 +1,24 @@
/**
* Transak API utilities shared across rFlows and rCart.
*
* Handles access token management (cached 6 days) and
* widget URL generation via Transak's session API.
* Builds Transak widget URLs using direct query parameters.
* The gateway session API has auth issues, so we use the direct
* URL approach which Transak still supports.
*/
let _transakAccessToken: string | null = null;
let _transakTokenExpiry = 0;
export 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 TRANSAK_WIDGET_BASE = 'https://global.transak.com';
const TRANSAK_WIDGET_BASE_STG = 'https://global-stg.transak.com';
export function createTransakWidgetUrl(params: Record<string, string>): string {
const env = process.env.TRANSAK_ENV || 'PRODUCTION';
const baseUrl = env === 'PRODUCTION'
? 'https://api.transak.com'
: 'https://api-stg.transak.com';
const base = env === 'PRODUCTION' ? TRANSAK_WIDGET_BASE : TRANSAK_WIDGET_BASE_STG;
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('[transak] Access token refreshed');
return _transakAccessToken;
}
export 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;
const url = new URL(base);
for (const [key, value] of Object.entries(params)) {
if (value != null && value !== '') {
url.searchParams.set(key, value);
}
throw new Error(`Transak widget URL failed (${res.status}): ${text}`);
}
const data = await res.json() as any;
return data.data?.widgetUrl;
return url.toString();
}