diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index dff08b8..38a3942 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -1368,26 +1368,22 @@ routes.post("/api/payments/:id/transak-session", async (c) => { const networkMap: Record = { 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" }, ], }; diff --git a/modules/rflows/lib/transak-onramp.ts b/modules/rflows/lib/transak-onramp.ts index fe55757..6895a93 100644 --- a/modules/rflows/lib/transak-onramp.ts +++ b/modules/rflows/lib/transak-onramp.ts @@ -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 { @@ -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' }; } } diff --git a/shared/transak.ts b/shared/transak.ts index 4ed52a5..5f304ce 100644 --- a/shared/transak.ts +++ b/shared/transak.ts @@ -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 { - 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 { 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): 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; + 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(); }