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:
parent
1f4b28aee1
commit
96b77278d4
|
|
@ -1368,26 +1368,22 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
|
||||||
|
|
||||||
const networkMap: Record<number, string> = { 8453: 'base', 84532: 'base', 1: 'ethereum' };
|
const networkMap: Record<number, string> = { 8453: 'base', 84532: 'base', 1: 'ethereum' };
|
||||||
|
|
||||||
try {
|
const widgetUrl = createTransakWidgetUrl({
|
||||||
const widgetUrl = await createTransakWidgetUrl({
|
apiKey: transakApiKey,
|
||||||
apiKey: transakApiKey,
|
referrerDomain: 'rspace.online',
|
||||||
referrerDomain: 'rspace.online',
|
cryptoCurrencyCode: p.token,
|
||||||
cryptoCurrencyCode: p.token,
|
network: networkMap[p.chainId] || 'base',
|
||||||
network: networkMap[p.chainId] || 'base',
|
defaultCryptoCurrency: p.token,
|
||||||
defaultCryptoCurrency: p.token,
|
walletAddress: p.recipientAddress,
|
||||||
walletAddress: p.recipientAddress,
|
disableWalletAddressForm: 'true',
|
||||||
disableWalletAddressForm: 'true',
|
cryptoAmount: p.amount,
|
||||||
cryptoAmount: p.amount,
|
partnerOrderId: `pay-${paymentId}`,
|
||||||
partnerOrderId: `pay-${paymentId}`,
|
email,
|
||||||
email,
|
themeColor: '6366f1',
|
||||||
themeColor: '6366f1',
|
hideMenu: 'true',
|
||||||
hideMenu: 'true',
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ widgetUrl });
|
return c.json({ widgetUrl });
|
||||||
} catch (err) {
|
|
||||||
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function paymentToResponse(p: PaymentRequestMeta) {
|
function paymentToResponse(p: PaymentRequestMeta) {
|
||||||
|
|
@ -1465,7 +1461,7 @@ routes.post("/api/group-buys", async (c) => {
|
||||||
_syncServer!.setDoc(docId, doc);
|
_syncServer!.setDoc(docId, doc);
|
||||||
|
|
||||||
const host = c.req.header('host') || 'rspace.online';
|
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);
|
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 ──
|
// ── Page route: group buy page ──
|
||||||
routes.get("/buy/:id", (c) => {
|
routes.get("/group-buy/:id", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
const buyId = c.req.param("id");
|
const buyId = c.req.param("id");
|
||||||
return c.html(renderShell({
|
return c.html(renderShell({
|
||||||
|
|
@ -1728,7 +1724,9 @@ export const cartModule: RSpaceModule = {
|
||||||
acceptsFeeds: ["economic", "data"],
|
acceptsFeeds: ["economic", "data"],
|
||||||
outputPaths: [
|
outputPaths: [
|
||||||
{ path: "carts", name: "Carts", icon: "🛒", description: "Group shopping carts" },
|
{ 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: "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" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export class TransakOnrampAdapter implements OnrampProvider {
|
||||||
name = 'Transak';
|
name = 'Transak';
|
||||||
|
|
||||||
isAvailable(): boolean {
|
isAvailable(): boolean {
|
||||||
return !!(process.env.TRANSAK_API_KEY && process.env.TRANSAK_SECRET);
|
return !!process.env.TRANSAK_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(req: OnrampSessionRequest): Promise<OnrampSessionResult> {
|
async createSession(req: OnrampSessionRequest): Promise<OnrampSessionResult> {
|
||||||
|
|
@ -32,7 +32,7 @@ export class TransakOnrampAdapter implements OnrampProvider {
|
||||||
};
|
};
|
||||||
if (req.returnUrl) widgetParams.redirectURL = req.returnUrl;
|
if (req.returnUrl) widgetParams.redirectURL = req.returnUrl;
|
||||||
|
|
||||||
const widgetUrl = await createTransakWidgetUrl(widgetParams);
|
const widgetUrl = createTransakWidgetUrl(widgetParams);
|
||||||
return { widgetUrl, provider: 'transak' };
|
return { widgetUrl, provider: 'transak' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,24 @@
|
||||||
/**
|
/**
|
||||||
* Transak API utilities — shared across rFlows and rCart.
|
* Transak API utilities — shared across rFlows and rCart.
|
||||||
*
|
*
|
||||||
* Handles access token management (cached 6 days) and
|
* Builds Transak widget URLs using direct query parameters.
|
||||||
* widget URL generation via Transak's session API.
|
* The gateway session API has auth issues, so we use the direct
|
||||||
|
* URL approach which Transak still supports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let _transakAccessToken: string | null = null;
|
const TRANSAK_WIDGET_BASE = 'https://global.transak.com';
|
||||||
let _transakTokenExpiry = 0;
|
const TRANSAK_WIDGET_BASE_STG = 'https://global-stg.transak.com';
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
|
export function createTransakWidgetUrl(params: Record<string, string>): string {
|
||||||
const env = process.env.TRANSAK_ENV || 'PRODUCTION';
|
const env = process.env.TRANSAK_ENV || 'PRODUCTION';
|
||||||
const baseUrl = env === 'PRODUCTION'
|
const base = env === 'PRODUCTION' ? TRANSAK_WIDGET_BASE : TRANSAK_WIDGET_BASE_STG;
|
||||||
? 'https://api.transak.com'
|
|
||||||
: 'https://api-stg.transak.com';
|
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/partners/api/v2/refresh-token`, {
|
const url = new URL(base);
|
||||||
method: 'POST',
|
for (const [key, value] of Object.entries(params)) {
|
||||||
headers: {
|
if (value != null && value !== '') {
|
||||||
'Content-Type': 'application/json',
|
url.searchParams.set(key, value);
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
throw new Error(`Transak widget URL failed (${res.status}): ${text}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json() as any;
|
return url.toString();
|
||||||
return data.data?.widgetUrl;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue