Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@ export async function displayPostInstallInstructions(
: "";
const clerkInstructions =
config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
const polarInstructions =
config.payments === "polar" && config.auth === "better-auth"
? getPolarInstructions(backend)
: "";
const alchemyDeployInstructions = getAlchemyDeployInstructions(
runCmd,
webDeploy,
Expand All @@ -135,6 +131,10 @@ export async function displayPostInstallInstructions(
isConvex && config.auth === "better-auth"
? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager)
: "";
const polarInstructions =
config.payments === "polar" && config.auth === "better-auth"
? getPolarInstructions(backend, packageManager)
: "";

const bunWebNativeWarning =
packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
Expand Down Expand Up @@ -565,7 +565,22 @@ 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://<your-convex-site-url>/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}`;
}
Expand Down
4 changes: 1 addition & 3 deletions apps/cli/src/prompts/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ export async function getPaymentsChoice(
}

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);

if (!isPolarCompatible) {
return "none" as Payments;
Expand Down
71 changes: 71 additions & 0 deletions apps/cli/test/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,77 @@ describe("Authentication Configurations", () => {
expect(dashboardFile).toContain("Unauthenticated");
});

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 fs.readFile(
path.join(result.projectDir, "packages/backend/convex/convex.config.ts"),
"utf8",
);
const httpFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/convex/http.ts"),
"utf8",
);
const polarFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/convex/polar.ts"),
"utf8",
);
const dashboardFile = await fs.readFile(
path.join(result.projectDir, "apps/web/src/routes/dashboard.tsx"),
"utf8",
);
const backendPackageFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/package.json"),
"utf8",
);
const webPackageFile = await fs.readFile(
path.join(result.projectDir, "apps/web/package.json"),
"utf8",
);
const convexEnvFile = await fs.readFile(
path.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("# POLAR_PRODUCT_ID_PRO=");
expect(convexEnvFile).toContain("POLAR_SERVER=sandbox");
});

const convexUnsupportedFrontends = ["nuxt", "svelte", "solid", "astro"] as const;
for (const frontend of convexUnsupportedFrontends) {
it(`should fail with Convex Better Auth + ${frontend}`, async () => {
Expand Down
11 changes: 0 additions & 11 deletions apps/web/src/app/(home)/new/_components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,14 +598,6 @@ export const analyzeStackCompatibility = (stack: StackState): CompatibilityResul
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 hasAnyFrontend =
hasWebFrontend(nextStack.webFrontend) || hasNativeFrontend(nextStack.nativeFrontend);
if (!hasWebFrontend(nextStack.webFrontend) && hasAnyFrontend) {
Expand Down Expand Up @@ -795,9 +787,6 @@ export const getDisabledReason = (
return `Convex AI example only supports React-based frontends (not ${frontendName})`;
}
}
if (category === "payments" && optionId === "polar") {
return "Polar is not compatible with Convex";
}
}

// ============================================
Expand Down
47 changes: 45 additions & 2 deletions packages/template-generator/src/processors/env-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ function buildNativeVars(
function buildConvexBackendVars(
frontend: string[],
auth: ProjectConfig["auth"],
payments: ProjectConfig["payments"],
examples: ProjectConfig["examples"],
): EnvVariable[] {
const hasNextJs = frontend.includes("next");
Expand Down Expand Up @@ -325,12 +326,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 hasNative =
Expand Down Expand Up @@ -372,6 +403,18 @@ function buildConvexCommentBlocks(
${hasWeb || hasNative ? `# npx convex env set SITE_URL ${defaultSiteUrl}\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://<your-convex-site-url>/polar/events
# Enable: product.created, product.updated, subscription.created, subscription.updated

`;
}

return commentBlocks;
}

Expand Down Expand Up @@ -549,7 +592,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)) {
Expand All @@ -559,7 +602,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)) {
Expand Down
28 changes: 27 additions & 1 deletion packages/template-generator/src/processors/payments-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,39 @@ import type { VirtualFileSystem } from "../core/virtual-fs";
import { addPackageDependency } from "../utils/add-deps";

export function processPaymentsDeps(vfs: VirtualFileSystem, config: ProjectConfig): void {
const { payments, frontend } = config;
const { payments, frontend, backend } = config;
if (!payments || payments === "none") return;

const backendPath = "packages/backend/package.json";
const authPath = "packages/auth/package.json";
const webPath = "apps/web/package.json";

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,
Expand Down
21 changes: 19 additions & 2 deletions packages/template-generator/src/template-handlers/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export async function processPaymentsTemplates(
config: ProjectConfig,
): Promise<void> {
if (!config.payments || config.payments === "none") return;
if (config.backend === "convex") return;

const hasReactWeb = config.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
Expand All @@ -18,7 +17,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,
Expand All @@ -40,6 +47,16 @@ 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(
Expand Down
Loading
Loading