Skip to content
Open
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ If Better Fullstack saves you time, consider supporting:
<tr>
<td align="center">
<a href="https://github.com/Divith123">
<img src="https://avatars.githubusercontent.com/u/106373840?v=4" width="80" style="border-radius:50%" alt="Divith123"/>
<br /><sub><b>Divith S</b></sub>
<br /><sub>Version channels, builder parity</sub>
<br /><sub><a href="https://github.com/Marve10s/Better-Fullstack/pull/104">#104</a></sub>
<br /><sub><code>+1,072</code> <code>−139</code></sub>
<img src="https://avatars.githubusercontent.com/u/106373840?v=4" width="80" style="border-radius:50%" alt="Divith123"/>
<br /><sub><b>Divith S</b></sub>
<br /><sub>Version channels, builder parity</sub>
<br /><sub><a href="https://github.com/Marve10s/Better-Fullstack/pull/104">#104</a></sub>
<br /><sub><code>+1,072</code> <code>−139</code></sub>
</a>
</td>
<td align="center">
Expand Down
18 changes: 16 additions & 2 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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://<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
5 changes: 1 addition & 4 deletions apps/cli/src/prompts/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = [];

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 @@ -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",
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 @@ -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");
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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://<your-convex-site-url>/polar/events
# Enable: product.created, product.updated, subscription.created, subscription.updated

`;
}

return commentBlocks;
}

Expand Down Expand Up @@ -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)) {
Expand All @@ -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)) {
Expand Down
26 changes: 26 additions & 0 deletions packages/template-generator/src/processors/payments-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions packages/template-generator/src/template-handlers/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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", "react-vite", "tanstack-start", "next"].includes(f),
Expand All @@ -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,
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/template-generator/src/utils/add-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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"))}}
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;
Loading
Loading