diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 2d40b002b..4a7f5fd46 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -109,7 +109,7 @@ export async function displayPostInstallInstructions( config.auth === "clerk" ? getClerkInstructions(config.backend, config.frontend ?? []) : ""; const polarInstructions = config.payments === "polar" && config.auth === "better-auth" - ? getPolarInstructions(backend) + ? getPolarInstructions(backend, packageManager) : ""; const alchemyDeployInstructions = getAlchemyDeployInstructions( runCmd, @@ -447,7 +447,21 @@ function getBetterAuthConvexInstructions(hasWeb: boolean, webPort: string, packa ); } -function getPolarInstructions(backend: Backend) { +function getPolarInstructions(backend: Backend, packageManager: string) { + if (backend === "convex") { + const cmd = packageManager === "npm" ? "npx" : packageManager; + return ( + `${pc.bold("Polar Payments Setup:")}\n` + + `${pc.cyan("•")} Create a Polar organization token, webhook secret, and product in ${pc.underline("https://sandbox.polar.sh/")}\n` + + `${pc.cyan("•")} Set the Convex env vars from ${pc.white("packages/backend")}:\n` + + `${pc.white(" cd packages/backend")}\n` + + `${pc.white(` ${cmd} convex env set POLAR_ORGANIZATION_TOKEN=your_polar_token`)}\n` + + `${pc.white(` ${cmd} convex env set POLAR_WEBHOOK_SECRET=your_polar_webhook_secret`)}\n` + + `${pc.white(` ${cmd} convex env set POLAR_PRODUCT_ID_PRO=your_polar_product_id`)}\n` + + `${pc.white(" Optional: set POLAR_SERVER=production when you go live")}\n` + + `${pc.cyan("•")} Configure a Polar webhook to ${pc.white("https:///polar/events")}` + ); + } const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env"; return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in ${envPath}`; } diff --git a/apps/cli/src/prompts/payments.ts b/apps/cli/src/prompts/payments.ts index 347e65d28..97af9ac2e 100644 --- a/apps/cli/src/prompts/payments.ts +++ b/apps/cli/src/prompts/payments.ts @@ -17,11 +17,8 @@ export async function getPaymentsChoice( return "none" as Payments; } - // Polar requires better-auth and non-convex backend const isPolarCompatible = - auth === "better-auth" && - backend !== "convex" && - (frontends?.length === 0 || splitFrontends(frontends).web.length > 0); + auth === "better-auth" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0); const options: Array<{ value: Payments; label: string; hint: string }> = []; diff --git a/apps/cli/test/auth.test.ts b/apps/cli/test/auth.test.ts index 53a1393a4..11a0d109a 100644 --- a/apps/cli/test/auth.test.ts +++ b/apps/cli/test/auth.test.ts @@ -187,6 +187,77 @@ describe("Authentication Configurations", () => { expectSuccess(result); }); + it("should scaffold Convex Better Auth with Polar payments", async () => { + const result = await runTRPCTest({ + projectName: "better-auth-convex-polar", + auth: "better-auth", + payments: "polar", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + if (!result.projectDir) { + throw new Error("Expected projectDir to be defined"); + } + + const convexConfigFile = await readFile( + join(result.projectDir, "packages/backend/convex/convex.config.ts"), + "utf8", + ); + const httpFile = await readFile( + join(result.projectDir, "packages/backend/convex/http.ts"), + "utf8", + ); + const polarFile = await readFile( + join(result.projectDir, "packages/backend/convex/polar.ts"), + "utf8", + ); + const dashboardFile = await readFile( + join(result.projectDir, "apps/web/src/routes/dashboard.tsx"), + "utf8", + ); + const backendPackageFile = await readFile( + join(result.projectDir, "packages/backend/package.json"), + "utf8", + ); + const webPackageFile = await readFile( + join(result.projectDir, "apps/web/package.json"), + "utf8", + ); + const convexEnvFile = await readFile( + join(result.projectDir, "packages/backend/.env.local"), + "utf8", + ); + + expect(convexConfigFile).toContain('import polar from "@convex-dev/polar/convex.config";'); + expect(convexConfigFile).toContain("app.use(polar);"); + expect(httpFile).toContain('import { polar } from "./polar";'); + expect(httpFile).toContain("polar.registerRoutes(http as any);"); + expect(polarFile).toContain('import { Polar } from "@convex-dev/polar";'); + expect(polarFile).toContain("POLAR_PRODUCT_ID_PRO"); + expect(dashboardFile).toContain('from "@convex-dev/polar/react";'); + expect(dashboardFile).toContain("api.polar.getConfiguredProducts"); + expect(dashboardFile).toContain("api.polar.getCurrentSubscription"); + expect(backendPackageFile).toContain('"@convex-dev/polar"'); + expect(backendPackageFile).toContain('"@polar-sh/sdk"'); + expect(webPackageFile).toContain('"@convex-dev/polar"'); + expect(webPackageFile).toContain('"@polar-sh/checkout"'); + expect(convexEnvFile).toContain("# npx convex env set POLAR_ORGANIZATION_TOKEN"); + expect(convexEnvFile).toContain("# npx convex env set POLAR_PRODUCT_ID_PRO"); + expect(convexEnvFile).toContain("POLAR_SERVER=sandbox"); + }); + const compatibleFrontends = [ "react-vite", "tanstack-router", diff --git a/packages/template-generator/src/processors/env-vars.ts b/packages/template-generator/src/processors/env-vars.ts index a79655598..b707f5c98 100644 --- a/packages/template-generator/src/processors/env-vars.ts +++ b/packages/template-generator/src/processors/env-vars.ts @@ -460,6 +460,7 @@ function buildNativeVars( function buildConvexBackendVars( frontend: string[], auth: ProjectConfig["auth"], + payments: ProjectConfig["payments"], examples: ProjectConfig["examples"], ): EnvVariable[] { const hasNextJs = frontend.includes("next"); @@ -516,12 +517,42 @@ function buildConvexBackendVars( } } + if (payments === "polar") { + vars.push( + { + key: "POLAR_ORGANIZATION_TOKEN", + value: "", + condition: true, + comment: "Polar organization token", + }, + { + key: "POLAR_WEBHOOK_SECRET", + value: "", + condition: true, + comment: "Polar webhook secret", + }, + { + key: "POLAR_PRODUCT_ID_PRO", + value: "", + condition: true, + comment: "Polar product ID for the default Pro plan", + }, + { + key: "POLAR_SERVER", + value: "sandbox", + condition: true, + comment: "Polar environment: sandbox or production", + }, + ); + } + return vars; } function buildConvexCommentBlocks( frontend: string[], auth: ProjectConfig["auth"], + payments: ProjectConfig["payments"], examples: ProjectConfig["examples"], ): string { const hasWeb = @@ -549,6 +580,18 @@ function buildConvexCommentBlocks( ${hasWeb ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}`; } + if (payments === "polar") { + commentBlocks += `# Set Polar environment variables +# npx convex env set POLAR_ORGANIZATION_TOKEN=your_polar_token +# npx convex env set POLAR_WEBHOOK_SECRET=your_polar_webhook_secret +# npx convex env set POLAR_PRODUCT_ID_PRO=your_polar_product_id +# Optional: npx convex env set POLAR_SERVER=sandbox +# Create a Polar webhook at https:///polar/events +# Enable: product.created, product.updated, subscription.created, subscription.updated + +`; + } + return commentBlocks; } @@ -1440,7 +1483,7 @@ export function processEnvVariables(vfs: VirtualFileSystem, config: ProjectConfi const envLocalPath = `${convexBackendDir}/.env.local`; // Write comment blocks first - const commentBlocks = buildConvexCommentBlocks(frontend, auth, examples); + const commentBlocks = buildConvexCommentBlocks(frontend, auth, payments, examples); if (commentBlocks) { let currentContent = ""; if (vfs.exists(envLocalPath)) { @@ -1450,7 +1493,7 @@ export function processEnvVariables(vfs: VirtualFileSystem, config: ProjectConfi } // Then add variables - const convexBackendVars = buildConvexBackendVars(frontend, auth, examples); + const convexBackendVars = buildConvexBackendVars(frontend, auth, payments, examples); if (convexBackendVars.length > 0) { let existingContent = ""; if (vfs.exists(envLocalPath)) { diff --git a/packages/template-generator/src/processors/payments-deps.ts b/packages/template-generator/src/processors/payments-deps.ts index c5c4f9f74..6918dce94 100644 --- a/packages/template-generator/src/processors/payments-deps.ts +++ b/packages/template-generator/src/processors/payments-deps.ts @@ -25,11 +25,37 @@ export function processPaymentsDeps(vfs: VirtualFileSystem, config: ProjectConfi const { payments, frontend, backend } = config; if (!payments || payments === "none") return; + const backendPath = "packages/backend/package.json"; const authPath = "packages/auth/package.json"; const webPath = getWebPackagePath(frontend, backend); const serverPath = getServerPackagePath(frontend, backend); if (payments === "polar") { + if (backend === "convex") { + if (vfs.exists(backendPath)) { + addPackageDependency({ + vfs, + packagePath: backendPath, + dependencies: ["@convex-dev/polar", "@polar-sh/sdk"], + }); + } + + if (vfs.exists(webPath)) { + const hasReactWebFrontend = frontend.some((f) => + ["react-router", "tanstack-router", "tanstack-start", "next"].includes(f), + ); + if (hasReactWebFrontend) { + addPackageDependency({ + vfs, + packagePath: webPath, + dependencies: ["@convex-dev/polar", "@polar-sh/checkout"], + }); + } + } + + return; + } + if (vfs.exists(authPath)) { addPackageDependency({ vfs, diff --git a/packages/template-generator/src/template-handlers/payments.ts b/packages/template-generator/src/template-handlers/payments.ts index 58ce97bcc..7e5dbff4a 100644 --- a/packages/template-generator/src/template-handlers/payments.ts +++ b/packages/template-generator/src/template-handlers/payments.ts @@ -10,7 +10,6 @@ export async function processPaymentsTemplates( config: ProjectConfig, ): Promise { if (!config.payments || config.payments === "none") return; - if (config.backend === "convex") return; const hasReactWeb = config.frontend.some((f) => ["tanstack-router", "react-router", "react-vite", "tanstack-start", "next"].includes(f), @@ -19,7 +18,15 @@ export async function processPaymentsTemplates( const hasSvelteWeb = config.frontend.includes("svelte"); const hasSolidWeb = config.frontend.includes("solid"); - if (config.backend !== "none") { + if (config.backend === "convex") { + processTemplatesFromPrefix( + vfs, + templates, + `payments/${config.payments}/convex/backend`, + "packages/backend", + config, + ); + } else if (config.backend !== "none") { processTemplatesFromPrefix( vfs, templates, @@ -43,6 +50,15 @@ export async function processPaymentsTemplates( "apps/web", config, ); + if (config.backend === "convex") { + processTemplatesFromPrefix( + vfs, + templates, + `payments/${config.payments}/convex/web/react/${reactFramework}`, + "apps/web", + config, + ); + } } } else if (hasNuxtWeb) { processTemplatesFromPrefix( diff --git a/packages/template-generator/src/utils/add-deps.ts b/packages/template-generator/src/utils/add-deps.ts index f586fad6c..a3add8494 100644 --- a/packages/template-generator/src/utils/add-deps.ts +++ b/packages/template-generator/src/utils/add-deps.ts @@ -228,6 +228,7 @@ export const dependencyVersionMap = { convex: "^1.34.1", "@convex-dev/react-query": "^0.1.0", "@convex-dev/agent": "^0.6.1", + "@convex-dev/polar": "^0.9.0", "convex-svelte": "^0.0.12", "convex-nuxt": "0.1.5", "convex-vue": "^0.1.5", @@ -306,6 +307,7 @@ export const dependencyVersionMap = { srvx: "^0.11.13", "@polar-sh/better-auth": "^1.8.3", + "@polar-sh/checkout": "^0.2.0", "@polar-sh/sdk": "^0.46.7", // Email diff --git a/packages/template-generator/templates/auth/better-auth/convex/backend/convex/http.ts.hbs b/packages/template-generator/templates/auth/better-auth/convex/backend/convex/http.ts.hbs index 705a23ef9..5633f449a 100644 --- a/packages/template-generator/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +++ b/packages/template-generator/templates/auth/better-auth/convex/backend/convex/http.ts.hbs @@ -1,6 +1,10 @@ import { httpRouter } from "convex/server"; import { authComponent, createAuth } from "./auth"; +{{#if (eq payments "polar")}} +import { polar } from "./polar"; +{{/if}} + const http = httpRouter(); {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}} @@ -8,5 +12,9 @@ authComponent.registerRoutes(http, createAuth, { cors: true }); {{else}} authComponent.registerRoutes(http, createAuth); {{/if}} +{{#if (eq payments "polar")}} + +polar.registerRoutes(http as any); +{{/if}} export default http; diff --git a/packages/template-generator/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs b/packages/template-generator/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs index 7e65c074d..e50e6acc6 100644 --- a/packages/template-generator/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs +++ b/packages/template-generator/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs @@ -3,6 +3,14 @@ import SignInForm from "@/components/sign-in-form"; import SignUpForm from "@/components/sign-up-form"; import UserMenu from "@/components/user-menu"; +{{#if (eq payments "polar")}} +import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react"; +{{#if (ne uiLibrary "none")}} +import { buttonVariants } from "@{{projectName}}/ui/components/button"; +{{else}} +import { buttonVariants } from "@/components/ui/button"; +{{/if}} +{{/if}} import { api } from "@{{projectName}}/backend/convex/_generated/api"; import { Authenticated, @@ -12,18 +20,58 @@ import { } from "convex/react"; import { useState } from "react"; +function DashboardContent() { + const privateData = useQuery(api.privateData.get); + {{#if (eq payments "polar")}} + const products = useQuery(api.polar.getConfiguredProducts); + const subscription = useQuery(api.polar.getCurrentSubscription); + + const proProduct = products?.pro; + const hasProSubscription = subscription?.productKey === "pro"; + {{/if}} + + return ( +
+

Dashboard

+

privateData: {privateData?.message}

+ {{#if (eq payments "polar")}} +

Plan: {hasProSubscription ? "Pro" : "Free"}

+ {products === undefined || subscription === undefined ? ( +

Loading subscription options...

+ ) : proProduct ? ( + hasProSubscription ? ( + + Manage Subscription + + ) : ( + + Upgrade to Pro + + ) + ) : ( +

Set POLAR_PRODUCT_ID_PRO in packages/backend/.env.local to enable checkout.

+ )} + {{/if}} + +
+ ); +} + export default function DashboardPage() { const [showSignIn, setShowSignIn] = useState(false); - const privateData = useQuery(api.privateData.get); return ( <> -
-

Dashboard

-

privateData: {privateData?.message}

- -
+
{showSignIn ? ( diff --git a/packages/template-generator/templates/auth/better-auth/convex/web/react/react-vite/src/routes/dashboard.tsx.hbs b/packages/template-generator/templates/auth/better-auth/convex/web/react/react-vite/src/routes/dashboard.tsx.hbs index 73e5c6bb0..96dda513d 100644 --- a/packages/template-generator/templates/auth/better-auth/convex/web/react/react-vite/src/routes/dashboard.tsx.hbs +++ b/packages/template-generator/templates/auth/better-auth/convex/web/react/react-vite/src/routes/dashboard.tsx.hbs @@ -1,22 +1,75 @@ import SignInForm from "@/components/sign-in-form"; import SignUpForm from "@/components/sign-up-form"; import UserMenu from "@/components/user-menu"; +{{#if (eq payments "polar")}} +import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react"; +{{#if (ne uiLibrary "none")}} +import { buttonVariants } from "@{{projectName}}/ui/components/button"; +{{else}} +import { buttonVariants } from "@/components/ui/button"; +{{/if}} +{{/if}} import { api } from "@{{projectName}}/backend/convex/_generated/api"; -import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react"; +import { + Authenticated, + AuthLoading, + Unauthenticated, + useQuery, +} from "convex/react"; import { useState } from "react"; +function PrivateDashboardContent() { + const privateData = useQuery(api.privateData.get); + {{#if (eq payments "polar")}} + const products = useQuery(api.polar.getConfiguredProducts); + const subscription = useQuery(api.polar.getCurrentSubscription); + + const proProduct = products?.pro; + const hasProSubscription = subscription?.productKey === "pro"; + {{/if}} + + return ( +
+

Dashboard

+

privateData: {privateData?.message}

+ {{#if (eq payments "polar")}} +

Plan: {hasProSubscription ? "Pro" : "Free"}

+ {products === undefined || subscription === undefined ? ( +

Loading subscription options...

+ ) : proProduct ? ( + hasProSubscription ? ( + + Manage Subscription + + ) : ( + + Upgrade to Pro + + ) + ) : ( +

Set POLAR_PRODUCT_ID_PRO in packages/backend/.env.local to enable checkout.

+ )} + {{/if}} + +
+ ); +} + export default function Dashboard() { const [showSignIn, setShowSignIn] = useState(false); - const privateData = useQuery(api.privateData.get); return ( <> -
-

Dashboard

-

privateData: {privateData?.message}

- -
+
{showSignIn ? ( diff --git a/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs b/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs index c5faeaf08..1ed01a709 100644 --- a/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +++ b/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs @@ -1,6 +1,14 @@ import SignInForm from "@/components/sign-in-form"; import SignUpForm from "@/components/sign-up-form"; import UserMenu from "@/components/user-menu"; +{{#if (eq payments "polar")}} +import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react"; +{{#if (ne uiLibrary "none")}} +import { buttonVariants } from "@{{projectName}}/ui/components/button"; +{{else}} +import { buttonVariants } from "@/components/ui/button"; +{{/if}} +{{/if}} import { api } from "@{{projectName}}/backend/convex/_generated/api"; import { createFileRoute } from "@tanstack/react-router"; import { @@ -15,18 +23,58 @@ export const Route = createFileRoute("/dashboard")({ component: RouteComponent, }); +function PrivateDashboardContent() { + const privateData = useQuery(api.privateData.get); + {{#if (eq payments "polar")}} + const products = useQuery(api.polar.getConfiguredProducts); + const subscription = useQuery(api.polar.getCurrentSubscription); + + const proProduct = products?.pro; + const hasProSubscription = subscription?.productKey === "pro"; + {{/if}} + + return ( +
+

Dashboard

+

privateData: {privateData?.message}

+ {{#if (eq payments "polar")}} +

Plan: {hasProSubscription ? "Pro" : "Free"}

+ {products === undefined || subscription === undefined ? ( +

Loading subscription options...

+ ) : proProduct ? ( + hasProSubscription ? ( + + Manage Subscription + + ) : ( + + Upgrade to Pro + + ) + ) : ( +

Set POLAR_PRODUCT_ID_PRO in packages/backend/.env.local to enable checkout.

+ )} + {{/if}} + +
+ ); +} + function RouteComponent() { const [showSignIn, setShowSignIn] = useState(false); - const privateData = useQuery(api.privateData.get); return ( <> -
-

Dashboard

-

privateData: {privateData?.message}

- -
+
{showSignIn ? ( diff --git a/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs b/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs index c5faeaf08..1ed01a709 100644 --- a/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +++ b/packages/template-generator/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs @@ -1,6 +1,14 @@ import SignInForm from "@/components/sign-in-form"; import SignUpForm from "@/components/sign-up-form"; import UserMenu from "@/components/user-menu"; +{{#if (eq payments "polar")}} +import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react"; +{{#if (ne uiLibrary "none")}} +import { buttonVariants } from "@{{projectName}}/ui/components/button"; +{{else}} +import { buttonVariants } from "@/components/ui/button"; +{{/if}} +{{/if}} import { api } from "@{{projectName}}/backend/convex/_generated/api"; import { createFileRoute } from "@tanstack/react-router"; import { @@ -15,18 +23,58 @@ export const Route = createFileRoute("/dashboard")({ component: RouteComponent, }); +function PrivateDashboardContent() { + const privateData = useQuery(api.privateData.get); + {{#if (eq payments "polar")}} + const products = useQuery(api.polar.getConfiguredProducts); + const subscription = useQuery(api.polar.getCurrentSubscription); + + const proProduct = products?.pro; + const hasProSubscription = subscription?.productKey === "pro"; + {{/if}} + + return ( +
+

Dashboard

+

privateData: {privateData?.message}

+ {{#if (eq payments "polar")}} +

Plan: {hasProSubscription ? "Pro" : "Free"}

+ {products === undefined || subscription === undefined ? ( +

Loading subscription options...

+ ) : proProduct ? ( + hasProSubscription ? ( + + Manage Subscription + + ) : ( + + Upgrade to Pro + + ) + ) : ( +

Set POLAR_PRODUCT_ID_PRO in packages/backend/.env.local to enable checkout.

+ )} + {{/if}} + +
+ ); +} + function RouteComponent() { const [showSignIn, setShowSignIn] = useState(false); - const privateData = useQuery(api.privateData.get); return ( <> -
-

Dashboard

-

privateData: {privateData?.message}

- -
+
{showSignIn ? ( diff --git a/packages/template-generator/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs b/packages/template-generator/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs index 12f69d403..0adb2df36 100644 --- a/packages/template-generator/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +++ b/packages/template-generator/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs @@ -2,6 +2,9 @@ import { defineApp } from "convex/server"; {{#if (eq auth "better-auth")}} import betterAuth from "@convex-dev/better-auth/convex.config"; {{/if}} +{{#if (eq payments "polar")}} +import polar from "@convex-dev/polar/convex.config"; +{{/if}} {{#if (includes examples "ai")}} import agent from "@convex-dev/agent/convex.config"; {{/if}} @@ -10,6 +13,9 @@ const app = defineApp(); {{#if (eq auth "better-auth")}} app.use(betterAuth); {{/if}} +{{#if (eq payments "polar")}} +app.use(polar); +{{/if}} {{#if (includes examples "ai")}} app.use(agent); {{/if}} diff --git a/packages/template-generator/templates/payments/polar/convex/backend/convex/polar.ts.hbs b/packages/template-generator/templates/payments/polar/convex/backend/convex/polar.ts.hbs new file mode 100644 index 000000000..49c91c470 --- /dev/null +++ b/packages/template-generator/templates/payments/polar/convex/backend/convex/polar.ts.hbs @@ -0,0 +1,60 @@ +import { Polar } from "@convex-dev/polar"; + +import { api, components } from "./_generated/api"; +import type { DataModel } from "./_generated/dataModel"; +import { action, query } from "./_generated/server"; + +export const polar = new Polar(components.polar, { + getUserInfo: async (ctx) => { + const user = await ctx.runQuery(api.auth.getCurrentUser); + + if (!user) { + throw new Error("Not authenticated"); + } + + if (!user.email) { + throw new Error("Authenticated user is missing an email address"); + } + + return { + userId: user._id, + email: user.email, + }; + }, + products: { + pro: process.env.POLAR_PRODUCT_ID_PRO || "your-product-id", + }, + server: (process.env.POLAR_SERVER as "sandbox" | "production" | undefined) ?? "sandbox", +}); + +export const { + changeCurrentSubscription, + cancelCurrentSubscription, + getConfiguredProducts, + listAllProducts, + listAllSubscriptions, + generateCheckoutLink, + generateCustomerPortalUrl, +} = polar.api(); + +export const getCurrentSubscription = query({ + args: {}, + handler: async (ctx) => { + const user = await ctx.runQuery(api.auth.getCurrentUser); + + if (!user) { + return null; + } + + return await polar.getCurrentSubscription(ctx, { + userId: user._id, + }); + }, +}); + +export const syncProducts = action({ + args: {}, + handler: async (ctx) => { + await polar.syncProducts(ctx); + }, +}); diff --git a/packages/template-generator/templates/payments/polar/convex/web/react/tanstack-start/src/functions/get-payment.ts.hbs b/packages/template-generator/templates/payments/polar/convex/web/react/tanstack-start/src/functions/get-payment.ts.hbs new file mode 100644 index 000000000..13fadacc5 --- /dev/null +++ b/packages/template-generator/templates/payments/polar/convex/web/react/tanstack-start/src/functions/get-payment.ts.hbs @@ -0,0 +1,8 @@ +import { createServerFn } from "@tanstack/react-start"; + +export const getPayment = createServerFn({ method: "GET" }).handler(async () => { + // For Convex projects, payment data is fetched using Convex queries on the + // client side via useQuery(api.polar.getCurrentSubscription). + // See the dashboard component for an example. + return null; +}); diff --git a/packages/types/src/compatibility.ts b/packages/types/src/compatibility.ts index 49ba74aa6..cede57874 100644 --- a/packages/types/src/compatibility.ts +++ b/packages/types/src/compatibility.ts @@ -922,14 +922,6 @@ export const analyzeStackCompatibility = ( message: "Payments set to 'None' (Polar requires Better Auth)", }); } - if (nextStack.backend === "convex") { - nextStack.payments = "none"; - changed = true; - changes.push({ - category: "payments", - message: "Payments set to 'None' (Polar incompatible with Convex)", - }); - } const hasWebFrontend = nextStack.webFrontend.some((f) => f !== "none"); if (!hasWebFrontend) { nextStack.payments = "none"; @@ -1306,9 +1298,6 @@ export const getDisabledReason = ( return `Convex AI example only supports React-based frontends including React + Vite (not ${frontendName})`; } } - if (category === "payments" && optionId === "polar") { - return "In Better-Fullstack, Polar is currently not available with the Convex backend"; - } } // ============================================