From 64a0bba24244c10f90ad4c97b33a99e97b7cc565 Mon Sep 17 00:00:00 2001 From: Yash Dewasthale Date: Thu, 6 Aug 2026 18:19:28 +0530 Subject: [PATCH] refactor: streamline Dodo Payments integration and error handling: - Removed hardcoded checkout URL logic and replaced it with a fetch request to the billing API for session creation. - Enhanced error handling for checkout session failures, providing clearer feedback on Dodo product issues. - Consolidated Dodo SDK client initialization into a shared utility function to improve code maintainability. --- apps/supercode-cli/client/app/studio/page.tsx | 29 +++++++++++-------- apps/supercode-cli/server/package.json | 2 +- .../server/src/api/billing/checkout.ts | 19 +++++++----- .../server/src/api/billing/refund.ts | 8 +---- .../server/src/api/billing/status.ts | 8 +---- .../server/src/api/billing/webhook.ts | 11 ++----- apps/supercode-cli/server/src/lib/dodo.ts | 21 ++++++++++++++ 7 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 apps/supercode-cli/server/src/lib/dodo.ts diff --git a/apps/supercode-cli/client/app/studio/page.tsx b/apps/supercode-cli/client/app/studio/page.tsx index 282fcfe..016aeee 100644 --- a/apps/supercode-cli/client/app/studio/page.tsx +++ b/apps/supercode-cli/client/app/studio/page.tsx @@ -71,14 +71,6 @@ type CreditBalance = { resetAt: string | null } -const CHECKOUT_BASE = - process.env.NEXT_PUBLIC_DODO_CHECKOUT_BASE ?? "https://checkout.dodopayments.com/buy" - -function getCheckoutUrl(plan: Plan): string | null { - if (!plan.dodoProductId) return null - return `${CHECKOUT_BASE}/${plan.dodoProductId}?quantity=1` -} - const TIER_COLORS: Record = { spark: "text-emerald-400 border-emerald-500/30", "spark-premium": "text-cyan-400 border-cyan-500/30", @@ -293,12 +285,25 @@ function StudioPage() { const handleConfirmPayNow = useCallback(async () => { if (!confirmingPlan || !userId) return - const url = getCheckoutUrl(confirmingPlan) - if (url) { - window.open(url, "_blank", "noopener,noreferrer") - } else { + if (!confirmingPlan.dodoProductId) { setConfirmingPlan(null) toast.error("Checkout URL not available for this plan") + return + } + try { + const res = await fetch("/api/billing/checkout", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId, planId: confirmingPlan.id }), + }) + const data = await res.json() + if (!res.ok || !data.checkout_url) { + throw new Error(data.error ?? "Failed to create checkout session") + } + window.location.href = data.checkout_url as string + } catch (err) { + setConfirmingPlan(null) + toast.error(err instanceof Error ? err.message : "Checkout failed") } }, [confirmingPlan, userId]) diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index c2f2f11..716a419 100644 --- a/apps/supercode-cli/server/package.json +++ b/apps/supercode-cli/server/package.json @@ -1,6 +1,6 @@ { "name": "supercode-cli", - "version": "0.1.93", + "version": "0.1.94", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/api/billing/checkout.ts b/apps/supercode-cli/server/src/api/billing/checkout.ts index 8c33cff..3eeb16c 100644 --- a/apps/supercode-cli/server/src/api/billing/checkout.ts +++ b/apps/supercode-cli/server/src/api/billing/checkout.ts @@ -1,15 +1,9 @@ import { Router } from "express" import prisma from "../../lib/prisma" -import { DodoPayments } from "dodopayments" +import { getDodo, getDodoEnvironment } from "../../lib/dodo" const router = Router() -function getDodo(): DodoPayments | null { - const key = process.env.DODO_PAYMENTS_API_KEY - if (!key) return null - return new DodoPayments({ bearerToken: key }) -} - function studioUrl(path: string): string { const clientUrl = process.env.CLIENT_URL || "http://localhost:3000" return `${clientUrl.replace(/\/$/, "")}${path}` @@ -87,6 +81,17 @@ router.post("/", async (req, res) => { }) } catch (error) { console.error("[billing/checkout] Checkout creation failed:", error) + const message = error instanceof Error ? error.message : String(error) + const mode = getDodoEnvironment() + const looksMissing = + /not found|404|invalid product|product_id/i.test(message) + if (looksMissing) { + res.status(400).json({ + error: + `Dodo product not found in ${mode}. Re-seed plans with DODO_MODE=${mode === "test_mode" ? "test" : "live"} so dodoProductId matches this mode.`, + }) + return + } res.status(500).json({ error: "Failed to create checkout session" }) } }) diff --git a/apps/supercode-cli/server/src/api/billing/refund.ts b/apps/supercode-cli/server/src/api/billing/refund.ts index 51274e7..729cb63 100644 --- a/apps/supercode-cli/server/src/api/billing/refund.ts +++ b/apps/supercode-cli/server/src/api/billing/refund.ts @@ -1,15 +1,9 @@ import { Router } from "express" import prisma from "../../lib/prisma" -import { DodoPayments } from "dodopayments" +import { getDodo } from "../../lib/dodo" const router = Router() -function getDodo(): DodoPayments | null { - const key = process.env.DODO_PAYMENTS_API_KEY - if (!key) return null - return new DodoPayments({ bearerToken: key }) -} - router.post("/", async (req, res) => { try { const userId = req.body.userId as string | undefined diff --git a/apps/supercode-cli/server/src/api/billing/status.ts b/apps/supercode-cli/server/src/api/billing/status.ts index fb5f8ae..43f4fec 100644 --- a/apps/supercode-cli/server/src/api/billing/status.ts +++ b/apps/supercode-cli/server/src/api/billing/status.ts @@ -1,15 +1,9 @@ import { Router } from "express" import prisma from "../../lib/prisma" -import { DodoPayments } from "dodopayments" +import { getDodo } from "../../lib/dodo" const router = Router() -function getDodo(): DodoPayments | null { - const key = process.env.DODO_PAYMENTS_API_KEY - if (!key) return null - return new DodoPayments({ bearerToken: key }) -} - function studioUrl(path: string): string { const clientUrl = process.env.CLIENT_URL || "http://localhost:3000" return `${clientUrl.replace(/\/$/, "")}${path}` diff --git a/apps/supercode-cli/server/src/api/billing/webhook.ts b/apps/supercode-cli/server/src/api/billing/webhook.ts index 19c7595..b961ab7 100644 --- a/apps/supercode-cli/server/src/api/billing/webhook.ts +++ b/apps/supercode-cli/server/src/api/billing/webhook.ts @@ -1,18 +1,11 @@ import { Router } from "express" import prisma from "../../lib/prisma" -import { DodoPayments } from "dodopayments" +import { getDodo } from "../../lib/dodo" import { invalidateModelCache } from "../../lib/model-access" import type { Prisma } from "../../generated" const router = Router() -function getDodo(): DodoPayments | null { - const key = process.env.DODO_PAYMENTS_API_KEY - const webhookKey = process.env.DODO_PAYMENTS_WEBHOOK_KEY - if (!key) return null - return new DodoPayments({ bearerToken: key, webhookKey }) -} - // ── Event shapes (mirrors dodopayments SDK types) ── interface DodoSubscriptionEvent { @@ -361,7 +354,7 @@ router.post("/", async (req, res) => { try { const body = typeof req.body === "string" ? req.body : JSON.stringify(req.body) - const dodo = getDodo() + const dodo = getDodo({ webhookKey: true }) if (!dodo) { console.error("[webhook] Dodo keys not configured — cannot verify webhook") res.status(503).json({ received: false }) diff --git a/apps/supercode-cli/server/src/lib/dodo.ts b/apps/supercode-cli/server/src/lib/dodo.ts new file mode 100644 index 0000000..6eabd63 --- /dev/null +++ b/apps/supercode-cli/server/src/lib/dodo.ts @@ -0,0 +1,21 @@ +import { DodoPayments } from "dodopayments" + +export type DodoEnvironment = "test_mode" | "live_mode" + +export function getDodoEnvironment(): DodoEnvironment { + return process.env.DODO_MODE === "test" ? "test_mode" : "live_mode" +} + +/** Shared Dodo SDK client. Honors DODO_MODE so test keys hit the test API. */ +export function getDodo(options?: { webhookKey?: boolean }): DodoPayments | null { + const key = process.env.DODO_PAYMENTS_API_KEY + if (!key) return null + + return new DodoPayments({ + bearerToken: key, + environment: getDodoEnvironment(), + ...(options?.webhookKey + ? { webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY } + : {}), + }) +}