mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add polar as better-auth plugin (#578)
This commit is contained in:
@@ -13,6 +13,7 @@ export const DEFAULT_CONFIG_BASE = {
|
|||||||
database: "sqlite",
|
database: "sqlite",
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "none",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
git: true,
|
git: true,
|
||||||
@@ -39,8 +40,8 @@ export function getDefaultConfig() {
|
|||||||
export const DEFAULT_CONFIG = getDefaultConfig();
|
export const DEFAULT_CONFIG = getDefaultConfig();
|
||||||
|
|
||||||
export const dependencyVersionMap = {
|
export const dependencyVersionMap = {
|
||||||
"better-auth": "^1.3.9",
|
"better-auth": "^1.3.10",
|
||||||
"@better-auth/expo": "^1.3.9",
|
"@better-auth/expo": "^1.3.10",
|
||||||
|
|
||||||
"@clerk/nextjs": "^6.31.5",
|
"@clerk/nextjs": "^6.31.5",
|
||||||
"@clerk/clerk-react": "^5.45.0",
|
"@clerk/clerk-react": "^5.45.0",
|
||||||
@@ -139,9 +140,9 @@ export const dependencyVersionMap = {
|
|||||||
"@tanstack/react-query-devtools": "^5.85.5",
|
"@tanstack/react-query-devtools": "^5.85.5",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
|
|
||||||
"@tanstack/solid-query": "^5.75.0",
|
"@tanstack/solid-query": "^5.87.4",
|
||||||
"@tanstack/solid-query-devtools": "^5.75.0",
|
"@tanstack/solid-query-devtools": "^5.87.4",
|
||||||
"@tanstack/solid-router-devtools": "^1.131.25",
|
"@tanstack/solid-router-devtools": "^1.131.44",
|
||||||
|
|
||||||
wrangler: "^4.23.0",
|
wrangler: "^4.23.0",
|
||||||
"@cloudflare/vite-plugin": "^1.9.0",
|
"@cloudflare/vite-plugin": "^1.9.0",
|
||||||
@@ -155,6 +156,9 @@ export const dependencyVersionMap = {
|
|||||||
nitropack: "^2.12.4",
|
nitropack: "^2.12.4",
|
||||||
|
|
||||||
dotenv: "^17.2.1",
|
dotenv: "^17.2.1",
|
||||||
|
|
||||||
|
"@polar-sh/better-auth": "^1.1.3",
|
||||||
|
"@polar-sh/sdk": "^0.34.16",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import path from "node:path";
|
|||||||
import { log } from "@clack/prompts";
|
import { log } from "@clack/prompts";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { AddInput, Addons, ProjectConfig } from "../../types";
|
import type { AddInput, Addons, ProjectConfig } from "../../types";
|
||||||
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
|
|
||||||
import { updateBtsConfig } from "../../utils/bts-config";
|
import { updateBtsConfig } from "../../utils/bts-config";
|
||||||
|
import { validateAddonCompatibility } from "../../utils/compatibility-rules";
|
||||||
import { exitWithError } from "../../utils/errors";
|
import { exitWithError } from "../../utils/errors";
|
||||||
import { setupAddons } from "../addons/addons-setup";
|
import { setupAddons } from "../addons/addons-setup";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +45,7 @@ export async function addAddonsToProject(
|
|||||||
addons: input.addons,
|
addons: input.addons,
|
||||||
examples: detectedConfig.examples || [],
|
examples: detectedConfig.examples || [],
|
||||||
auth: detectedConfig.auth || "none",
|
auth: detectedConfig.auth || "none",
|
||||||
|
payments: detectedConfig.payments || "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager:
|
packageManager:
|
||||||
input.packageManager || detectedConfig.packageManager || "npm",
|
input.packageManager || detectedConfig.packageManager || "npm",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export async function addDeploymentToProject(
|
|||||||
addons: detectedConfig.addons || [],
|
addons: detectedConfig.addons || [],
|
||||||
examples: detectedConfig.examples || [],
|
examples: detectedConfig.examples || [],
|
||||||
auth: detectedConfig.auth || "none",
|
auth: detectedConfig.auth || "none",
|
||||||
|
payments: detectedConfig.payments || "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager:
|
packageManager:
|
||||||
input.packageManager || detectedConfig.packageManager || "npm",
|
input.packageManager || detectedConfig.packageManager || "npm",
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export async function createProjectHandler(
|
|||||||
addons: [],
|
addons: [],
|
||||||
examples: [],
|
examples: [],
|
||||||
auth: "none",
|
auth: "none",
|
||||||
|
payments: "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
install: false,
|
install: false,
|
||||||
@@ -272,6 +273,7 @@ export async function addAddonsHandler(input: AddInput) {
|
|||||||
const addonsPrompt = await getAddonsToAdd(
|
const addonsPrompt = await getAddonsToAdd(
|
||||||
detectedConfig.frontend || [],
|
detectedConfig.frontend || [],
|
||||||
detectedConfig.addons || [],
|
detectedConfig.addons || [],
|
||||||
|
detectedConfig.auth,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addonsPrompt.length > 0) {
|
if (addonsPrompt.length > 0) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { createReadme } from "./create-readme";
|
|||||||
import { setupEnvironmentVariables } from "./env-setup";
|
import { setupEnvironmentVariables } from "./env-setup";
|
||||||
import { initializeGit } from "./git";
|
import { initializeGit } from "./git";
|
||||||
import { installDependencies } from "./install-dependencies";
|
import { installDependencies } from "./install-dependencies";
|
||||||
|
import { setupPayments } from "./payments-setup";
|
||||||
import { displayPostInstallInstructions } from "./post-installation";
|
import { displayPostInstallInstructions } from "./post-installation";
|
||||||
import { updatePackageConfigurations } from "./project-config";
|
import { updatePackageConfigurations } from "./project-config";
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
setupDockerComposeTemplates,
|
setupDockerComposeTemplates,
|
||||||
setupExamplesTemplate,
|
setupExamplesTemplate,
|
||||||
setupFrontendTemplates,
|
setupFrontendTemplates,
|
||||||
|
setupPaymentsTemplate,
|
||||||
} from "./template-manager";
|
} from "./template-manager";
|
||||||
|
|
||||||
export async function createProject(
|
export async function createProject(
|
||||||
@@ -50,6 +52,9 @@ export async function createProject(
|
|||||||
await setupDockerComposeTemplates(projectDir, options);
|
await setupDockerComposeTemplates(projectDir, options);
|
||||||
}
|
}
|
||||||
await setupAuthTemplate(projectDir, options);
|
await setupAuthTemplate(projectDir, options);
|
||||||
|
if (options.payments && options.payments !== "none") {
|
||||||
|
await setupPaymentsTemplate(projectDir, options);
|
||||||
|
}
|
||||||
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
||||||
await setupExamplesTemplate(projectDir, options);
|
await setupExamplesTemplate(projectDir, options);
|
||||||
}
|
}
|
||||||
@@ -76,6 +81,10 @@ export async function createProject(
|
|||||||
await setupAuth(options);
|
await setupAuth(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.payments && options.payments !== "none") {
|
||||||
|
await setupPayments(options);
|
||||||
|
}
|
||||||
|
|
||||||
await handleExtras(projectDir, options);
|
await handleExtras(projectDir, options);
|
||||||
|
|
||||||
await setupEnvironmentVariables(options);
|
await setupEnvironmentVariables(options);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export async function detectProjectConfig(projectDir: string) {
|
|||||||
addons: btsConfig.addons,
|
addons: btsConfig.addons,
|
||||||
examples: btsConfig.examples,
|
examples: btsConfig.examples,
|
||||||
auth: btsConfig.auth,
|
auth: btsConfig.auth,
|
||||||
|
payments: btsConfig.payments,
|
||||||
packageManager: btsConfig.packageManager,
|
packageManager: btsConfig.packageManager,
|
||||||
dbSetup: btsConfig.dbSetup,
|
dbSetup: btsConfig.dbSetup,
|
||||||
api: btsConfig.api,
|
api: btsConfig.api,
|
||||||
|
|||||||
@@ -280,6 +280,16 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|||||||
value: "",
|
value: "",
|
||||||
condition: examples?.includes("ai") || false,
|
condition: examples?.includes("ai") || false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "POLAR_ACCESS_TOKEN",
|
||||||
|
value: "",
|
||||||
|
condition: config.payments === "polar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "POLAR_SUCCESS_URL",
|
||||||
|
value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
|
||||||
|
condition: config.payments === "polar",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await addEnvVariablesToFile(envPath, serverVars);
|
await addEnvVariablesToFile(envPath, serverVars);
|
||||||
|
|||||||
50
apps/cli/src/helpers/core/payments-setup.ts
Normal file
50
apps/cli/src/helpers/core/payments-setup.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import type { ProjectConfig } from "../../types";
|
||||||
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
|
export async function setupPayments(config: ProjectConfig) {
|
||||||
|
const { payments, projectDir, frontend } = config;
|
||||||
|
|
||||||
|
if (!payments || payments === "none") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
|
const serverDirExists = await fs.pathExists(serverDir);
|
||||||
|
const clientDirExists = await fs.pathExists(clientDir);
|
||||||
|
|
||||||
|
if (!serverDirExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payments === "polar") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@polar-sh/better-auth", "@polar-sh/sdk"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clientDirExists) {
|
||||||
|
const hasWebFrontend = frontend.some((f) =>
|
||||||
|
[
|
||||||
|
"react-router",
|
||||||
|
"tanstack-router",
|
||||||
|
"tanstack-start",
|
||||||
|
"next",
|
||||||
|
"nuxt",
|
||||||
|
"svelte",
|
||||||
|
"solid",
|
||||||
|
].includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasWebFrontend) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@polar-sh/better-auth"],
|
||||||
|
projectDir: clientDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,10 @@ export async function displayPostInstallInstructions(
|
|||||||
: "";
|
: "";
|
||||||
const clerkInstructions =
|
const clerkInstructions =
|
||||||
isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
|
isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
|
||||||
|
const polarInstructions =
|
||||||
|
config.payments === "polar" && config.auth === "better-auth"
|
||||||
|
? getPolarInstructions()
|
||||||
|
: "";
|
||||||
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
||||||
runCmd,
|
runCmd,
|
||||||
webDeploy,
|
webDeploy,
|
||||||
@@ -188,6 +192,7 @@ export async function displayPostInstallInstructions(
|
|||||||
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
||||||
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
||||||
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
|
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
|
||||||
|
if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
|
||||||
|
|
||||||
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
||||||
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
||||||
@@ -447,6 +452,10 @@ function getClerkInstructions() {
|
|||||||
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
|
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPolarInstructions() {
|
||||||
|
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 apps/server/.env`;
|
||||||
|
}
|
||||||
|
|
||||||
function getAlchemyDeployInstructions(
|
function getAlchemyDeployInstructions(
|
||||||
runCmd?: string,
|
runCmd?: string,
|
||||||
webDeploy?: string,
|
webDeploy?: string,
|
||||||
|
|||||||
@@ -607,6 +607,102 @@ export async function setupAuthTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setupPaymentsTemplate(
|
||||||
|
projectDir: string,
|
||||||
|
context: ProjectConfig,
|
||||||
|
) {
|
||||||
|
if (!context.payments || context.payments === "none") return;
|
||||||
|
|
||||||
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
|
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
||||||
|
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||||
|
|
||||||
|
if (serverAppDirExists && context.backend !== "convex") {
|
||||||
|
const paymentsServerSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/payments/${context.payments}/server/base`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(paymentsServerSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
paymentsServerSrc,
|
||||||
|
serverAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
|
);
|
||||||
|
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||||
|
const hasSvelteWeb = context.frontend.includes("svelte");
|
||||||
|
const hasSolidWeb = context.frontend.includes("solid");
|
||||||
|
|
||||||
|
if (
|
||||||
|
webAppDirExists &&
|
||||||
|
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb)
|
||||||
|
) {
|
||||||
|
if (hasReactWeb) {
|
||||||
|
const reactFramework = context.frontend.find((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||||
|
f,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (reactFramework) {
|
||||||
|
const paymentsWebSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/payments/${context.payments}/web/react/${reactFramework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(paymentsWebSrc)) {
|
||||||
|
await processAndCopyFiles("**/*", paymentsWebSrc, webAppDir, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasNuxtWeb) {
|
||||||
|
const paymentsWebNuxtSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/payments/${context.payments}/web/nuxt`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(paymentsWebNuxtSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
paymentsWebNuxtSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hasSvelteWeb) {
|
||||||
|
const paymentsWebSvelteSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/payments/${context.payments}/web/svelte`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(paymentsWebSvelteSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
paymentsWebSvelteSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hasSolidWeb) {
|
||||||
|
const paymentsWebSolidSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/payments/${context.payments}/web/solid`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(paymentsWebSolidSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
paymentsWebSolidSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupAddonsTemplate(
|
export async function setupAddonsTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
ORMSchema,
|
ORMSchema,
|
||||||
type PackageManager,
|
type PackageManager,
|
||||||
PackageManagerSchema,
|
PackageManagerSchema,
|
||||||
|
PaymentsSchema,
|
||||||
type ProjectConfig,
|
type ProjectConfig,
|
||||||
ProjectNameSchema,
|
ProjectNameSchema,
|
||||||
type Runtime,
|
type Runtime,
|
||||||
@@ -80,6 +81,7 @@ export const router = t.router({
|
|||||||
database: DatabaseSchema.optional(),
|
database: DatabaseSchema.optional(),
|
||||||
orm: ORMSchema.optional(),
|
orm: ORMSchema.optional(),
|
||||||
auth: AuthSchema.optional(),
|
auth: AuthSchema.optional(),
|
||||||
|
payments: PaymentsSchema.optional(),
|
||||||
frontend: z.array(FrontendSchema).optional(),
|
frontend: z.array(FrontendSchema).optional(),
|
||||||
addons: z.array(AddonsSchema).optional(),
|
addons: z.array(AddonsSchema).optional(),
|
||||||
examples: z.array(ExamplesSchema).optional(),
|
examples: z.array(ExamplesSchema).optional(),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { groupMultiselect, isCancel } from "@clack/prompts";
|
import { groupMultiselect, isCancel } from "@clack/prompts";
|
||||||
import { DEFAULT_CONFIG } from "../constants";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
import { type Addons, AddonsSchema, type Frontend } from "../types";
|
import { type Addons, AddonsSchema, type Auth, type Frontend } from "../types";
|
||||||
import {
|
import {
|
||||||
getCompatibleAddons,
|
getCompatibleAddons,
|
||||||
validateAddonCompatibility,
|
validateAddonCompatibility,
|
||||||
} from "../utils/addon-compatibility";
|
} from "../utils/compatibility-rules";
|
||||||
import { exitCancelled } from "../utils/errors";
|
import { exitCancelled } from "../utils/errors";
|
||||||
|
|
||||||
type AddonOption = {
|
type AddonOption = {
|
||||||
@@ -75,6 +75,7 @@ const ADDON_GROUPS = {
|
|||||||
export async function getAddonsChoice(
|
export async function getAddonsChoice(
|
||||||
addons?: Addons[],
|
addons?: Addons[],
|
||||||
frontends?: Frontend[],
|
frontends?: Frontend[],
|
||||||
|
auth?: Auth,
|
||||||
) {
|
) {
|
||||||
if (addons !== undefined) return addons;
|
if (addons !== undefined) return addons;
|
||||||
|
|
||||||
@@ -88,7 +89,11 @@ export async function getAddonsChoice(
|
|||||||
const frontendsArray = frontends || [];
|
const frontendsArray = frontends || [];
|
||||||
|
|
||||||
for (const addon of allAddons) {
|
for (const addon of allAddons) {
|
||||||
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
|
const { isCompatible } = validateAddonCompatibility(
|
||||||
|
addon,
|
||||||
|
frontendsArray,
|
||||||
|
auth,
|
||||||
|
);
|
||||||
if (!isCompatible) continue;
|
if (!isCompatible) continue;
|
||||||
|
|
||||||
const { label, hint } = getAddonDisplay(addon);
|
const { label, hint } = getAddonDisplay(addon);
|
||||||
@@ -131,6 +136,7 @@ export async function getAddonsChoice(
|
|||||||
export async function getAddonsToAdd(
|
export async function getAddonsToAdd(
|
||||||
frontend: Frontend[],
|
frontend: Frontend[],
|
||||||
existingAddons: Addons[] = [],
|
existingAddons: Addons[] = [],
|
||||||
|
auth?: Auth,
|
||||||
) {
|
) {
|
||||||
const groupedOptions: Record<string, AddonOption[]> = {
|
const groupedOptions: Record<string, AddonOption[]> = {
|
||||||
Documentation: [],
|
Documentation: [],
|
||||||
@@ -144,6 +150,7 @@ export async function getAddonsToAdd(
|
|||||||
AddonsSchema.options.filter((addon) => addon !== "none"),
|
AddonsSchema.options.filter((addon) => addon !== "none"),
|
||||||
frontendArray,
|
frontendArray,
|
||||||
existingAddons,
|
existingAddons,
|
||||||
|
auth,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const addon of compatibleAddons) {
|
for (const addon of compatibleAddons) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
Frontend,
|
Frontend,
|
||||||
ORM,
|
ORM,
|
||||||
PackageManager,
|
PackageManager,
|
||||||
|
Payments,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
Runtime,
|
Runtime,
|
||||||
ServerDeploy,
|
ServerDeploy,
|
||||||
@@ -28,6 +29,7 @@ import { getGitChoice } from "./git";
|
|||||||
import { getinstallChoice } from "./install";
|
import { getinstallChoice } from "./install";
|
||||||
import { getORMChoice } from "./orm";
|
import { getORMChoice } from "./orm";
|
||||||
import { getPackageManagerChoice } from "./package-manager";
|
import { getPackageManagerChoice } from "./package-manager";
|
||||||
|
import { getPaymentsChoice } from "./payments";
|
||||||
import { getRuntimeChoice } from "./runtime";
|
import { getRuntimeChoice } from "./runtime";
|
||||||
import { getServerDeploymentChoice } from "./server-deploy";
|
import { getServerDeploymentChoice } from "./server-deploy";
|
||||||
import { getDeploymentChoice } from "./web-deploy";
|
import { getDeploymentChoice } from "./web-deploy";
|
||||||
@@ -40,6 +42,7 @@ type PromptGroupResults = {
|
|||||||
orm: ORM;
|
orm: ORM;
|
||||||
api: API;
|
api: API;
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
|
payments: Payments;
|
||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
dbSetup: DatabaseSetup;
|
dbSetup: DatabaseSetup;
|
||||||
@@ -87,7 +90,15 @@ export async function gatherConfig(
|
|||||||
results.backend,
|
results.backend,
|
||||||
results.frontend,
|
results.frontend,
|
||||||
),
|
),
|
||||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
payments: ({ results }) =>
|
||||||
|
getPaymentsChoice(
|
||||||
|
flags.payments,
|
||||||
|
results.auth,
|
||||||
|
results.backend,
|
||||||
|
results.frontend,
|
||||||
|
),
|
||||||
|
addons: ({ results }) =>
|
||||||
|
getAddonsChoice(flags.addons, results.frontend, results.auth),
|
||||||
examples: ({ results }) =>
|
examples: ({ results }) =>
|
||||||
getExamplesChoice(
|
getExamplesChoice(
|
||||||
flags.examples,
|
flags.examples,
|
||||||
@@ -137,6 +148,7 @@ export async function gatherConfig(
|
|||||||
database: result.database,
|
database: result.database,
|
||||||
orm: result.orm,
|
orm: result.orm,
|
||||||
auth: result.auth,
|
auth: result.auth,
|
||||||
|
payments: result.payments,
|
||||||
addons: result.addons,
|
addons: result.addons,
|
||||||
examples: result.examples,
|
examples: result.examples,
|
||||||
git: result.git,
|
git: result.git,
|
||||||
|
|||||||
46
apps/cli/src/prompts/payments.ts
Normal file
46
apps/cli/src/prompts/payments.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { isCancel, select } from "@clack/prompts";
|
||||||
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
|
import type { Auth, Backend, Frontend, Payments } from "../types";
|
||||||
|
import { splitFrontends } from "../utils/compatibility-rules";
|
||||||
|
import { exitCancelled } from "../utils/errors";
|
||||||
|
|
||||||
|
export async function getPaymentsChoice(
|
||||||
|
payments?: Payments,
|
||||||
|
auth?: Auth,
|
||||||
|
backend?: Backend,
|
||||||
|
frontends?: Frontend[],
|
||||||
|
) {
|
||||||
|
if (payments !== undefined) return payments;
|
||||||
|
|
||||||
|
const isPolarCompatible =
|
||||||
|
auth === "better-auth" &&
|
||||||
|
backend !== "convex" &&
|
||||||
|
(frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
|
||||||
|
|
||||||
|
if (!isPolarCompatible) {
|
||||||
|
return "none" as Payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: "polar" as Payments,
|
||||||
|
label: "Polar",
|
||||||
|
hint: "Turn your software into a business. 6 lines of code.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "none" as Payments,
|
||||||
|
label: "None",
|
||||||
|
hint: "No payments integration",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await select<Payments>({
|
||||||
|
message: "Select payments provider",
|
||||||
|
options,
|
||||||
|
initialValue: DEFAULT_CONFIG.payments,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -86,6 +86,11 @@ export const AuthSchema = z
|
|||||||
.describe("Authentication provider");
|
.describe("Authentication provider");
|
||||||
export type Auth = z.infer<typeof AuthSchema>;
|
export type Auth = z.infer<typeof AuthSchema>;
|
||||||
|
|
||||||
|
export const PaymentsSchema = z
|
||||||
|
.enum(["polar", "none"])
|
||||||
|
.describe("Payments provider");
|
||||||
|
export type Payments = z.infer<typeof PaymentsSchema>;
|
||||||
|
|
||||||
export const ProjectNameSchema = z
|
export const ProjectNameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Project name cannot be empty")
|
.min(1, "Project name cannot be empty")
|
||||||
@@ -132,6 +137,7 @@ export type CreateInput = {
|
|||||||
database?: Database;
|
database?: Database;
|
||||||
orm?: ORM;
|
orm?: ORM;
|
||||||
auth?: Auth;
|
auth?: Auth;
|
||||||
|
payments?: Payments;
|
||||||
frontend?: Frontend[];
|
frontend?: Frontend[];
|
||||||
addons?: Addons[];
|
addons?: Addons[];
|
||||||
examples?: Examples[];
|
examples?: Examples[];
|
||||||
@@ -175,6 +181,7 @@ export interface ProjectConfig {
|
|||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
|
payments: Payments;
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
install: boolean;
|
install: boolean;
|
||||||
@@ -195,6 +202,7 @@ export interface BetterTStackConfig {
|
|||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
|
payments: Payments;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
dbSetup: DatabaseSetup;
|
dbSetup: DatabaseSetup;
|
||||||
api: API;
|
api: API;
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { ADDON_COMPATIBILITY } from "../constants";
|
|
||||||
import type { Addons, Frontend } from "../types";
|
|
||||||
|
|
||||||
export function validateAddonCompatibility(
|
|
||||||
addon: Addons,
|
|
||||||
frontend: Frontend[],
|
|
||||||
): { isCompatible: boolean; reason?: string } {
|
|
||||||
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
|
||||||
|
|
||||||
if (compatibleFrontends.length === 0) {
|
|
||||||
return { isCompatible: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCompatibleFrontend = frontend.some((f) =>
|
|
||||||
(compatibleFrontends as readonly string[]).includes(f),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasCompatibleFrontend) {
|
|
||||||
const frontendList = compatibleFrontends.join(", ");
|
|
||||||
return {
|
|
||||||
isCompatible: false,
|
|
||||||
reason: `${addon} addon requires one of these frontends: ${frontendList}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isCompatible: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCompatibleAddons(
|
|
||||||
allAddons: Addons[],
|
|
||||||
frontend: Frontend[],
|
|
||||||
existingAddons: Addons[] = [],
|
|
||||||
) {
|
|
||||||
return allAddons.filter((addon) => {
|
|
||||||
if (existingAddons.includes(addon)) return false;
|
|
||||||
|
|
||||||
if (addon === "none") return false;
|
|
||||||
|
|
||||||
const { isCompatible } = validateAddonCompatibility(addon, frontend);
|
|
||||||
return isCompatible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
|||||||
addons: projectConfig.addons,
|
addons: projectConfig.addons,
|
||||||
examples: projectConfig.examples,
|
examples: projectConfig.examples,
|
||||||
auth: projectConfig.auth,
|
auth: projectConfig.auth,
|
||||||
|
payments: projectConfig.payments,
|
||||||
packageManager: projectConfig.packageManager,
|
packageManager: projectConfig.packageManager,
|
||||||
dbSetup: projectConfig.dbSetup,
|
dbSetup: projectConfig.dbSetup,
|
||||||
api: projectConfig.api,
|
api: projectConfig.api,
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import { ADDON_COMPATIBILITY } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
Addons,
|
Addons,
|
||||||
API,
|
API,
|
||||||
|
Auth,
|
||||||
Backend,
|
Backend,
|
||||||
CLIInput,
|
CLIInput,
|
||||||
Frontend,
|
Frontend,
|
||||||
|
Payments,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ServerDeploy,
|
ServerDeploy,
|
||||||
WebDeploy,
|
WebDeploy,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
|
||||||
import { WEB_FRAMEWORKS } from "./compatibility";
|
import { WEB_FRAMEWORKS } from "./compatibility";
|
||||||
import { exitWithError } from "./errors";
|
import { exitWithError } from "./errors";
|
||||||
|
|
||||||
@@ -191,15 +193,57 @@ export function validateServerDeployRequiresBackend(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateAddonCompatibility(
|
||||||
|
addon: Addons,
|
||||||
|
frontend: Frontend[],
|
||||||
|
_auth?: Auth,
|
||||||
|
): { isCompatible: boolean; reason?: string } {
|
||||||
|
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
||||||
|
|
||||||
|
if (compatibleFrontends.length > 0) {
|
||||||
|
const hasCompatibleFrontend = frontend.some((f) =>
|
||||||
|
(compatibleFrontends as readonly string[]).includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasCompatibleFrontend) {
|
||||||
|
const frontendList = compatibleFrontends.join(", ");
|
||||||
|
return {
|
||||||
|
isCompatible: false,
|
||||||
|
reason: `${addon} addon requires one of these frontends: ${frontendList}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isCompatible: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompatibleAddons(
|
||||||
|
allAddons: Addons[],
|
||||||
|
frontend: Frontend[],
|
||||||
|
existingAddons: Addons[] = [],
|
||||||
|
auth?: Auth,
|
||||||
|
) {
|
||||||
|
return allAddons.filter((addon) => {
|
||||||
|
if (existingAddons.includes(addon)) return false;
|
||||||
|
|
||||||
|
if (addon === "none") return false;
|
||||||
|
|
||||||
|
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
|
||||||
|
return isCompatible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function validateAddonsAgainstFrontends(
|
export function validateAddonsAgainstFrontends(
|
||||||
addons: Addons[] = [],
|
addons: Addons[] = [],
|
||||||
frontends: Frontend[] = [],
|
frontends: Frontend[] = [],
|
||||||
|
auth?: Auth,
|
||||||
) {
|
) {
|
||||||
for (const addon of addons) {
|
for (const addon of addons) {
|
||||||
if (addon === "none") continue;
|
if (addon === "none") continue;
|
||||||
const { isCompatible, reason } = validateAddonCompatibility(
|
const { isCompatible, reason } = validateAddonCompatibility(
|
||||||
addon,
|
addon,
|
||||||
frontends,
|
frontends,
|
||||||
|
auth,
|
||||||
);
|
);
|
||||||
if (!isCompatible) {
|
if (!isCompatible) {
|
||||||
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
||||||
@@ -207,6 +251,36 @@ export function validateAddonsAgainstFrontends(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validatePaymentsCompatibility(
|
||||||
|
payments: Payments | undefined,
|
||||||
|
auth: Auth | undefined,
|
||||||
|
backend: Backend | undefined,
|
||||||
|
frontends: Frontend[] = [],
|
||||||
|
) {
|
||||||
|
if (!payments || payments === "none") return;
|
||||||
|
|
||||||
|
if (payments === "polar") {
|
||||||
|
if (!auth || auth === "none" || auth !== "better-auth") {
|
||||||
|
exitWithError(
|
||||||
|
"Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backend === "convex") {
|
||||||
|
exitWithError(
|
||||||
|
"Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { web } = splitFrontends(frontends);
|
||||||
|
if (web.length === 0 && frontends.length > 0) {
|
||||||
|
exitWithError(
|
||||||
|
"Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function validateExamplesCompatibility(
|
export function validateExamplesCompatibility(
|
||||||
examples: string[] | undefined,
|
examples: string[] | undefined,
|
||||||
backend: ProjectConfig["backend"] | undefined,
|
backend: ProjectConfig["backend"] | undefined,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
DatabaseSetup,
|
DatabaseSetup,
|
||||||
ORM,
|
ORM,
|
||||||
PackageManager,
|
PackageManager,
|
||||||
|
Payments,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
Runtime,
|
Runtime,
|
||||||
ServerDeploy,
|
ServerDeploy,
|
||||||
@@ -56,6 +57,10 @@ export function processFlags(options: CLIInput, projectName?: string) {
|
|||||||
config.auth = options.auth as Auth;
|
config.auth = options.auth as Auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.payments !== undefined) {
|
||||||
|
config.payments = options.payments as Payments;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.git !== undefined) {
|
if (options.git !== undefined) {
|
||||||
config.git = options.git;
|
config.git = options.git;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
validateAddonsAgainstFrontends,
|
validateAddonsAgainstFrontends,
|
||||||
validateApiFrontendCompatibility,
|
validateApiFrontendCompatibility,
|
||||||
validateExamplesCompatibility,
|
validateExamplesCompatibility,
|
||||||
|
validatePaymentsCompatibility,
|
||||||
validateServerDeployRequiresBackend,
|
validateServerDeployRequiresBackend,
|
||||||
validateWebDeployRequiresWebFrontend,
|
validateWebDeployRequiresWebFrontend,
|
||||||
validateWorkersCompatibility,
|
validateWorkersCompatibility,
|
||||||
@@ -417,7 +418,7 @@ export function validateFullConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons && config.addons.length > 0) {
|
if (config.addons && config.addons.length > 0) {
|
||||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
||||||
config.addons = [...new Set(config.addons)];
|
config.addons = [...new Set(config.addons)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,8 +442,19 @@ export function validateConfigForProgrammaticUse(
|
|||||||
|
|
||||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||||
|
|
||||||
|
validatePaymentsCompatibility(
|
||||||
|
config.payments,
|
||||||
|
config.auth,
|
||||||
|
config.backend,
|
||||||
|
config.frontend,
|
||||||
|
);
|
||||||
|
|
||||||
if (config.addons && config.addons.length > 0) {
|
if (config.addons && config.addons.length > 0) {
|
||||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
validateAddonsAgainstFrontends(
|
||||||
|
config.addons,
|
||||||
|
config.frontend,
|
||||||
|
config.auth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateExamplesCompatibility(
|
validateExamplesCompatibility(
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
|||||||
configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.payments !== undefined) {
|
||||||
|
configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.addons !== undefined) {
|
if (config.addons !== undefined) {
|
||||||
const addons = Array.isArray(config.addons)
|
const addons = Array.isArray(config.addons)
|
||||||
? config.addons
|
? config.addons
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function generateReproducibleCommand(config: ProjectConfig) {
|
|||||||
flags.push(`--orm ${config.orm}`);
|
flags.push(`--orm ${config.orm}`);
|
||||||
flags.push(`--api ${config.api}`);
|
flags.push(`--api ${config.api}`);
|
||||||
flags.push(`--auth ${config.auth}`);
|
flags.push(`--auth ${config.auth}`);
|
||||||
|
flags.push(`--payments ${config.payments}`);
|
||||||
|
|
||||||
if (config.addons && config.addons.length > 0) {
|
if (config.addons && config.addons.length > 0) {
|
||||||
flags.push(`--addons ${config.addons.join(" ")}`);
|
flags.push(`--addons ${config.addons.join(" ")}`);
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polar, checkout, portal } from "@polar-sh/better-auth";
|
||||||
|
import { polarClient } from "./payments";
|
||||||
|
{{/if}}
|
||||||
import prisma from "../db";
|
import prisma from "../db";
|
||||||
|
|
||||||
export const auth = betterAuth<BetterAuthOptions>({
|
export const auth = betterAuth<BetterAuthOptions>({
|
||||||
@@ -28,9 +32,35 @@ export const auth = betterAuth<BetterAuthOptions>({
|
|||||||
secure: true,
|
secure: true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [
|
||||||
|
polar({
|
||||||
|
client: polarClient,
|
||||||
|
createCustomerOnSignUp: true,
|
||||||
|
enableCustomerPortal: true,
|
||||||
|
use: [
|
||||||
|
checkout({
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
productId: "your-product-id",
|
||||||
|
slug: "pro",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
successUrl: process.env.POLAR_SUCCESS_URL,
|
||||||
|
authenticatedUsersOnly: true,
|
||||||
|
}),
|
||||||
|
portal(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
|
expo(),
|
||||||
|
{{/if}}
|
||||||
|
],
|
||||||
|
{{else}}
|
||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
, plugins: [expo()]
|
plugins: [expo()],
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -42,6 +72,10 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polar, checkout, portal } from "@polar-sh/better-auth";
|
||||||
|
import { polarClient } from "./payments";
|
||||||
|
{{/if}}
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema/auth";
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
@@ -68,9 +102,35 @@ export const auth = betterAuth<BetterAuthOptions>({
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [
|
||||||
|
polar({
|
||||||
|
client: polarClient,
|
||||||
|
createCustomerOnSignUp: true,
|
||||||
|
enableCustomerPortal: true,
|
||||||
|
use: [
|
||||||
|
checkout({
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
productId: "your-product-id",
|
||||||
|
slug: "pro",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
successUrl: process.env.POLAR_SUCCESS_URL,
|
||||||
|
authenticatedUsersOnly: true,
|
||||||
|
}),
|
||||||
|
portal(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
|
expo(),
|
||||||
|
{{/if}}
|
||||||
|
],
|
||||||
|
{{else}}
|
||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
plugins: [expo()],
|
plugins: [expo()],
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -80,6 +140,10 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polar, checkout, portal } from "@polar-sh/better-auth";
|
||||||
|
import { polarClient } from "./payments";
|
||||||
|
{{/if}}
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema/auth";
|
import * as schema from "../db/schema/auth";
|
||||||
import { env } from "cloudflare:workers";
|
import { env } from "cloudflare:workers";
|
||||||
@@ -109,9 +173,32 @@ export const auth = betterAuth<BetterAuthOptions>({
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [
|
||||||
|
polar({
|
||||||
|
client: polarClient,
|
||||||
|
createCustomerOnSignUp: true,
|
||||||
|
enableCustomerPortal: true,
|
||||||
|
use: [
|
||||||
|
checkout({
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
productId: "your-product-id",
|
||||||
|
slug: "pro",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
successUrl: env.POLAR_SUCCESS_URL,
|
||||||
|
authenticatedUsersOnly: true,
|
||||||
|
}),
|
||||||
|
portal(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{{else}}
|
||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
plugins: [expo()],
|
plugins: [expo()],
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -122,6 +209,10 @@ import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polar, checkout, portal } from "@polar-sh/better-auth";
|
||||||
|
import { polarClient } from "./payments";
|
||||||
|
{{/if}}
|
||||||
import { client } from "../db";
|
import { client } from "../db";
|
||||||
|
|
||||||
export const auth = betterAuth<BetterAuthOptions>({
|
export const auth = betterAuth<BetterAuthOptions>({
|
||||||
@@ -141,9 +232,35 @@ export const auth = betterAuth<BetterAuthOptions>({
|
|||||||
secure: true,
|
secure: true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [
|
||||||
|
polar({
|
||||||
|
client: polarClient,
|
||||||
|
createCustomerOnSignUp: true,
|
||||||
|
enableCustomerPortal: true,
|
||||||
|
use: [
|
||||||
|
checkout({
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
productId: "your-product-id",
|
||||||
|
slug: "pro",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
successUrl: process.env.POLAR_SUCCESS_URL,
|
||||||
|
authenticatedUsersOnly: true,
|
||||||
|
}),
|
||||||
|
portal(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
|
expo(),
|
||||||
|
{{/if}}
|
||||||
|
],
|
||||||
|
{{else}}
|
||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
, plugins: [expo()]
|
plugins: [expo()],
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -153,6 +270,10 @@ import { betterAuth, type BetterAuthOptions } from "better-auth";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polar, checkout, portal } from "@polar-sh/better-auth";
|
||||||
|
import { polarClient } from "./payments";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export const auth = betterAuth<BetterAuthOptions>({
|
export const auth = betterAuth<BetterAuthOptions>({
|
||||||
database: "", // Invalid configuration
|
database: "", // Invalid configuration
|
||||||
@@ -171,9 +292,35 @@ export const auth = betterAuth<BetterAuthOptions>({
|
|||||||
secure: true,
|
secure: true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [
|
||||||
|
polar({
|
||||||
|
client: polarClient,
|
||||||
|
createCustomerOnSignUp: true,
|
||||||
|
enableCustomerPortal: true,
|
||||||
|
use: [
|
||||||
|
checkout({
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
productId: "your-product-id",
|
||||||
|
slug: "pro",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
successUrl: process.env.POLAR_SUCCESS_URL,
|
||||||
|
authenticatedUsersOnly: true,
|
||||||
|
}),
|
||||||
|
portal(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
|
expo(),
|
||||||
|
{{/if}}
|
||||||
|
],
|
||||||
|
{{else}}
|
||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
, plugins: [expo()]
|
plugins: [expo()],
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -11,9 +11,28 @@ definePageMeta({
|
|||||||
const { $orpc } = useNuxtApp()
|
const { $orpc } = useNuxtApp()
|
||||||
|
|
||||||
const session = $authClient.useSession()
|
const session = $authClient.useSession()
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const customerState = ref<any>(null)
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
const privateData = useQuery($orpc.privateData.queryOptions())
|
const privateData = useQuery({
|
||||||
|
...$orpc.privateData.queryOptions(),
|
||||||
|
enabled: computed(() => !!session.value?.data?.user)
|
||||||
|
})
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
onMounted(async () => {
|
||||||
|
if (session.value?.data) {
|
||||||
|
const { data } = await $authClient.customer.state()
|
||||||
|
customerState.value = data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasProSubscription = computed(() =>
|
||||||
|
customerState.value?.activeSubscriptions?.length! > 0
|
||||||
|
)
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -27,7 +46,22 @@ const privateData = useQuery($orpc.privateData.queryOptions())
|
|||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
<div v-if="privateData.status.value === 'pending'">Loading private data...</div>
|
<div v-if="privateData.status.value === 'pending'">Loading private data...</div>
|
||||||
<div v-else-if="privateData.status.value === 'error'">Error loading private data: \{{ privateData.error.value?.message }}</div>
|
<div v-else-if="privateData.status.value === 'error'">Error loading private data: \{{ privateData.error.value?.message }}</div>
|
||||||
<p v-else-if="privateData.data.value">Private Data: \{{ privateData.data.value.message }}</p>
|
<p v-else-if="privateData.data.value">API: \{{ privateData.data.value.message }}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
<p class="mb-2">Plan: \{{ hasProSubscription ? "Pro" : "Free" }}</p>
|
||||||
|
<UButton
|
||||||
|
v-if="hasProSubscription"
|
||||||
|
@click="() => { $authClient.customer.portal() }"
|
||||||
|
>
|
||||||
|
Manage Subscription
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-else
|
||||||
|
@click="() => { $authClient.checkout({ slug: 'pro' }) }"
|
||||||
|
>
|
||||||
|
Upgrade to Pro
|
||||||
|
</UButton>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { createAuthClient } from "better-auth/vue";
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(nuxtApp => {
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const serverUrl = config.public.serverURL
|
|
||||||
|
|
||||||
const authClient = createAuthClient({
|
|
||||||
baseURL: serverUrl
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
authClient: authClient
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { createAuthClient } from "better-auth/vue";
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polarClient } from "@polar-sh/better-auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const serverUrl = config.public.serverURL;
|
||||||
|
|
||||||
|
const authClient = createAuthClient({
|
||||||
|
baseURL: serverUrl,
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [polarClient()],
|
||||||
|
{{/if}}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
authClient: authClient,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polarClient } from "@polar-sh/better-auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL:
|
baseURL:
|
||||||
@@ -7,4 +10,7 @@ export const authClient = createAuthClient({
|
|||||||
{{else}}
|
{{else}}
|
||||||
import.meta.env.VITE_SERVER_URL,
|
import.meta.env.VITE_SERVER_URL,
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [polarClient()]
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { orpc } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { trpc } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export default function Dashboard({
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
customerState,
|
||||||
|
{{/if}}
|
||||||
|
session
|
||||||
|
}: {
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
customerState: ReturnType<typeof authClient.customer.state>;
|
||||||
|
{{/if}}
|
||||||
|
session: typeof authClient.$Infer.Session;
|
||||||
|
}) {
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
const privateData = useQuery(orpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
const privateData = useQuery(trpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0;
|
||||||
|
console.log("Active subscriptions:", customerState?.activeSubscriptions);
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
<p>API: {privateData.data?.message}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
<p>API: {privateData.data?.message}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
|
||||||
|
{hasProSubscription ? (
|
||||||
|
<Button onClick={async () => await authClient.customer.portal()}>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
|
||||||
|
Upgrade to Pro
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{{/if}}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,47 +1,37 @@
|
|||||||
"use client"
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{#if (eq api "orpc")}}
|
import { redirect } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import Dashboard from "./dashboard";
|
||||||
import { orpc } from "@/utils/orpc";
|
import { headers } from "next/headers";
|
||||||
{{/if}}
|
|
||||||
{{#if (eq api "trpc")}}
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { trpc } from "@/utils/trpc";
|
|
||||||
{{/if}}
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default async function DashboardPage() {
|
||||||
const router = useRouter();
|
const session = await authClient.getSession({
|
||||||
const { data: session, isPending } = authClient.useSession();
|
fetchOptions: {
|
||||||
|
headers: await headers()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
{{#if (eq api "orpc")}}
|
if (!session.data) {
|
||||||
const privateData = useQuery(orpc.privateData.queryOptions());
|
redirect("/login");
|
||||||
{{/if}}
|
}
|
||||||
{{#if (eq api "trpc")}}
|
|
||||||
const privateData = useQuery(trpc.privateData.queryOptions());
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
useEffect(() => {
|
{{#if (eq payments "polar")}}
|
||||||
if (!session && !isPending) {
|
const { data: customerState, error } = await authClient.customer.state({
|
||||||
router.push("/login");
|
fetchOptions: {
|
||||||
}
|
headers: await headers()
|
||||||
}, [session, isPending]);
|
}
|
||||||
|
});
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
if (isPending) {
|
return (
|
||||||
return <div>Loading...</div>;
|
<div>
|
||||||
}
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome {session.data.user.name}</p>
|
||||||
return (
|
<Dashboard
|
||||||
<div>
|
session={session.data}
|
||||||
<h1>Dashboard</h1>
|
{{#if (eq payments "polar")}}
|
||||||
<p>Welcome {session?.user.name}</p>
|
customerState={customerState}
|
||||||
{{#if (eq api "orpc")}}
|
{{/if}}
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
/>
|
||||||
{{/if}}
|
</div>
|
||||||
{{#if (eq api "trpc")}}
|
);
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
import { orpc } from "@/utils/orpc";
|
import { orpc } from "@/utils/orpc";
|
||||||
@@ -8,12 +9,15 @@ import { trpc } from "@/utils/trpc";
|
|||||||
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { data: session, isPending } = authClient.useSession();
|
const { data: session, isPending } = authClient.useSession();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const [customerState, setCustomerState] = useState<any>(null);
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
const privateData = useQuery(orpc.privateData.queryOptions());
|
const privateData = useQuery(orpc.privateData.queryOptions());
|
||||||
@@ -26,18 +30,48 @@ export default function Dashboard() {
|
|||||||
if (!session && !isPending) {
|
if (!session && !isPending) {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
}, [session, isPending]);
|
}, [session, isPending, navigate]);
|
||||||
|
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCustomerState() {
|
||||||
|
if (session) {
|
||||||
|
const { data } = await authClient.customer.state();
|
||||||
|
setCustomerState(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCustomerState();
|
||||||
|
}, [session]);
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0;
|
||||||
|
console.log("Active subscriptions:", customerState?.activeSubscriptions);
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<p>Welcome {session?.user.name}</p>
|
<p>Welcome {session?.user.name}</p>
|
||||||
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
<p>API: {privateData.data?.message}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
|
||||||
|
{hasProSubscription ? (
|
||||||
|
<Button onClick={async () => await authClient.customer.portal()}>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
|
||||||
|
Upgrade to Pro
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
import { orpc } from "@/utils/orpc";
|
import { orpc } from "@/utils/orpc";
|
||||||
@@ -8,44 +9,61 @@ import { trpc } from "@/utils/trpc";
|
|||||||
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard")({
|
export const Route = createFileRoute("/dashboard")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const session = await authClient.getSession();
|
||||||
|
if (!session.data) {
|
||||||
|
redirect({
|
||||||
|
to: "/login",
|
||||||
|
throw: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const {data: customerState} = await authClient.customer.state()
|
||||||
|
return { session, customerState };
|
||||||
|
{{else}}
|
||||||
|
return { session };
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { data: session, isPending } = authClient.useSession();
|
const { session{{#if (eq payments "polar")}}, customerState{{/if}} } = Route.useRouteContext();
|
||||||
|
|
||||||
const navigate = Route.useNavigate();
|
{{#if (eq api "orpc")}}
|
||||||
|
const privateData = useQuery(orpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
const privateData = useQuery(trpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq payments "polar")}}
|
||||||
const privateData = useQuery(orpc.privateData.queryOptions());
|
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0
|
||||||
{{/if}}
|
console.log("Active subscriptions:", customerState?.activeSubscriptions)
|
||||||
{{#if (eq api "trpc")}}
|
{{/if}}
|
||||||
const privateData = useQuery(trpc.privateData.queryOptions());
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
if (!session && !isPending) {
|
<div>
|
||||||
navigate({
|
<h1>Dashboard</h1>
|
||||||
to: "/login",
|
<p>Welcome {session.data?.user.name}</p>
|
||||||
});
|
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
||||||
}
|
<p>API: {privateData.data?.message}</p>
|
||||||
}, [session, isPending]);
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
if (isPending) {
|
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
|
||||||
return <div>Loading...</div>;
|
{hasProSubscription ? (
|
||||||
}
|
<Button onClick={async () => await authClient.customer.portal()}>
|
||||||
|
Manage Subscription
|
||||||
return (
|
</Button>
|
||||||
<div>
|
) : (
|
||||||
<h1>Dashboard</h1>
|
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
|
||||||
<p>Welcome {session?.user.name}</p>
|
Upgrade to Pro
|
||||||
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
</Button>
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
)}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
import { useTRPC } from "@/utils/trpc";
|
import { useTRPC } from "@/utils/trpc";
|
||||||
@@ -7,48 +8,62 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { orpc } from "@/utils/orpc";
|
import { orpc } from "@/utils/orpc";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard")({
|
export const Route = createFileRoute("/dashboard")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const session = await authClient.getSession();
|
||||||
|
if (!session.data) {
|
||||||
|
redirect({
|
||||||
|
to: "/login",
|
||||||
|
throw: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const {data: customerState} = await authClient.customer.state()
|
||||||
|
return { session, customerState };
|
||||||
|
{{else}}
|
||||||
|
return { session };
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const navigate = Route.useNavigate();
|
const { session{{#if (eq payments "polar")}}, customerState{{/if}} } = Route.useRouteContext();
|
||||||
{{#if (eq api "trpc")}}
|
|
||||||
const trpc = useTRPC();
|
|
||||||
{{/if}}
|
|
||||||
{{#if (eq api "orpc")}}
|
|
||||||
{{/if}}
|
|
||||||
const { data: session, isPending } = authClient.useSession();
|
|
||||||
|
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
const privateData = useQuery(trpc.privateData.queryOptions());
|
const trpc = useTRPC();
|
||||||
{{/if}}
|
const privateData = useQuery(trpc.privateData.queryOptions());
|
||||||
{{#if (eq api "orpc")}}
|
{{/if}}
|
||||||
const privateData = useQuery(orpc.privateData.queryOptions());
|
{{#if (eq api "orpc")}}
|
||||||
{{/if}}
|
const privateData = useQuery(orpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
useEffect(() => {
|
{{#if (eq payments "polar")}}
|
||||||
if (!session && !isPending) {
|
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0
|
||||||
navigate({
|
console.log("Active subscriptions:", customerState?.activeSubscriptions)
|
||||||
to: "/login",
|
{{/if}}
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [session, isPending]);
|
|
||||||
|
|
||||||
if (isPending) {
|
return (
|
||||||
return <div>Loading...</div>;
|
<div>
|
||||||
}
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome {session.data?.user.name}</p>
|
||||||
return (
|
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
||||||
<div>
|
<p>API: {privateData.data?.message}</p>
|
||||||
<h1>Dashboard</h1>
|
{{/if}}
|
||||||
<p>Welcome {session?.user.name}</p>
|
{{#if (eq payments "polar")}}
|
||||||
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
|
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
{hasProSubscription ? (
|
||||||
{{/if}}
|
<Button onClick={async () => await authClient.customer.portal()}>
|
||||||
</div>
|
Manage Subscription
|
||||||
);
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
|
||||||
|
Upgrade to Pro
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createAuthClient } from "better-auth/solid";
|
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
|
||||||
baseURL: import.meta.env.VITE_SERVER_URL,
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createAuthClient } from "better-auth/solid";
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polarClient } from "@polar-sh/better-auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
baseURL: import.meta.env.VITE_SERVER_URL,
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [polarClient()]
|
||||||
|
{{/if}}
|
||||||
|
});
|
||||||
@@ -3,42 +3,65 @@ import { authClient } from "@/lib/auth-client";
|
|||||||
import { orpc } from "@/utils/orpc";
|
import { orpc } from "@/utils/orpc";
|
||||||
import { useQuery } from "@tanstack/solid-query";
|
import { useQuery } from "@tanstack/solid-query";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { createFileRoute } from "@tanstack/solid-router";
|
import { createFileRoute, redirect } from "@tanstack/solid-router";
|
||||||
import { createEffect, Show } from "solid-js";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard")({
|
export const Route = createFileRoute("/dashboard")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const session = await authClient.getSession();
|
||||||
|
if (!session.data) {
|
||||||
|
redirect({
|
||||||
|
to: "/login",
|
||||||
|
throw: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
const { data: customerState } = await authClient.customer.state();
|
||||||
|
return { session, customerState };
|
||||||
|
{{else}}
|
||||||
|
return { session };
|
||||||
|
{{/if}}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const session = authClient.useSession();
|
const context = Route.useRouteContext();
|
||||||
const navigate = Route.useNavigate();
|
|
||||||
|
|
||||||
{{#if (eq api "orpc")}}
|
const session = context().session;
|
||||||
const privateData = useQuery(() => orpc.privateData.queryOptions());
|
{{#if (eq payments "polar")}}
|
||||||
{{/if}}
|
const customerState = context().customerState;
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
createEffect(() => {
|
{{#if (eq api "orpc")}}
|
||||||
if (!session().data && !session().isPending) {
|
const privateData = useQuery(() => orpc.privateData.queryOptions());
|
||||||
navigate({
|
{{/if}}
|
||||||
to: "/login",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
{{#if (eq payments "polar")}}
|
||||||
<div>
|
const hasProSubscription = () =>
|
||||||
<Show when={session().isPending}>
|
customerState?.activeSubscriptions?.length! > 0;
|
||||||
<div>Loading...</div>
|
{{/if}}
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!session().isPending && session().data}>
|
return (
|
||||||
<h1>Dashboard</h1>
|
<div>
|
||||||
<p>Welcome {session().data?.user.name}</p>
|
<h1>Dashboard</h1>
|
||||||
{{#if (eq api "orpc")}}
|
<p>Welcome {session.data?.user.name}</p>
|
||||||
<p>privateData: {privateData.data?.message}</p>
|
{{#if (eq api "orpc")}}
|
||||||
{{/if}}
|
<p>API: {privateData.data?.message}</p>
|
||||||
</Show>
|
{{/if}}
|
||||||
</div>
|
{{#if (eq payments "polar")}}
|
||||||
);
|
<p>Plan: {hasProSubscription() ? "Pro" : "Free"}</p>
|
||||||
|
{hasProSubscription() ? (
|
||||||
|
<button onClick={async () => await authClient.customer.portal()}>
|
||||||
|
Manage Subscription
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={async () => await authClient.checkout({ slug: "pro" })}
|
||||||
|
>
|
||||||
|
Upgrade to Pro
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { PUBLIC_SERVER_URL } from "$env/static/public";
|
import { PUBLIC_SERVER_URL } from "$env/static/public";
|
||||||
import { createAuthClient } from "better-auth/svelte";
|
import { createAuthClient } from "better-auth/svelte";
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
import { polarClient } from "@polar-sh/better-auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: PUBLIC_SERVER_URL,
|
baseURL: PUBLIC_SERVER_URL,
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
plugins: [polarClient()]
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
import { orpc } from '$lib/orpc';
|
import { orpc } from '$lib/orpc';
|
||||||
import { createQuery } from '@tanstack/svelte-query';
|
import { createQuery } from '@tanstack/svelte-query';
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { get } from 'svelte/store';
|
{{#if (eq payments "polar")}}
|
||||||
|
let customerState: any = null;
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
const sessionQuery = authClient.useSession();
|
const sessionQuery = authClient.useSession();
|
||||||
|
|
||||||
@@ -15,10 +17,17 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const { data: session, isPending } = get(sessionQuery);
|
const { data: session, isPending } = $sessionQuery;
|
||||||
if (!session && !isPending) {
|
if (!session && !isPending) {
|
||||||
goto('/login');
|
goto('/login');
|
||||||
}
|
}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
if (session) {
|
||||||
|
authClient.customer.state().then(({ data }) => {
|
||||||
|
customerState = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,7 +39,19 @@
|
|||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<p>Welcome {$sessionQuery.data.user.name}</p>
|
<p>Welcome {$sessionQuery.data.user.name}</p>
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
<p>privateData: {$privateDataQuery.data?.message}</p>
|
<p>API: {$privateDataQuery.data?.message}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq payments "polar")}}
|
||||||
|
<p>Plan: {customerState?.activeSubscriptions?.length > 0 ? "Pro" : "Free"}</p>
|
||||||
|
{#if customerState?.activeSubscriptions?.length > 0}
|
||||||
|
<button onclick={async () => await authClient.customer.portal()}>
|
||||||
|
Manage Subscription
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button onclick={async () => await authClient.checkout({ slug: "pro" })}>
|
||||||
|
Upgrade to Pro
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
],
|
],
|
||||||
{{/if}}
|
{{/if}}
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "^0.14.1",
|
"tsdown": "^0.15.1",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
[install]
|
[install]
|
||||||
|
{{#if (includes frontend "nuxt")}}
|
||||||
|
# linker = "isolated" # Commented out for Nuxt compatibility
|
||||||
|
{{else}}
|
||||||
linker = "isolated"
|
linker = "isolated"
|
||||||
|
{{/if}}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import appCss from "../index.css?url";
|
|||||||
import type { QueryClient } from "@tanstack/react-query";
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import type { ConvexQueryClient } from "@convex-dev/react-query";
|
import type { ConvexQueryClient } from "@convex-dev/react-query";
|
||||||
import type { ConvexReactClient } from "convex/react";
|
import type { ConvexReactClient } from "convex/react";
|
||||||
|
{{else}}
|
||||||
|
{{#if (or (eq api "trpc") (eq api "orpc"))}}
|
||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import Loader from "@/components/loader";
|
import Loader from "@/components/loader";
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,18 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/router-plugin": "^1.109.2",
|
"@tanstack/router-plugin": "^1.131.44",
|
||||||
"@tanstack/solid-form": "^1.9.0",
|
"@tanstack/solid-form": "^1.20.0",
|
||||||
"@tanstack/solid-router": "^1.110.0",
|
"@tanstack/solid-router": "^1.131.44",
|
||||||
"lucide-solid": "^0.507.0",
|
"lucide-solid": "^0.544.0",
|
||||||
"solid-js": "^1.9.4",
|
"solid-js": "^1.9.9",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.1.13",
|
||||||
"zod": "^4.0.2"
|
"zod": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.0.2",
|
"vite": "^7.1.5",
|
||||||
"vite-plugin-solid": "^2.11.2"
|
"vite-plugin-solid": "^2.11.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Polar } from "@polar-sh/sdk";
|
||||||
|
|
||||||
|
export const polarClient = new Polar({
|
||||||
|
accessToken: process.env.POLAR_ACCESS_TOKEN,
|
||||||
|
server: "sandbox",
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const checkout_id = route.query.checkout_id as string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Payment Successful!</h1>
|
||||||
|
<p v-if="checkout_id">Checkout ID: \{{ checkout_id }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default async function SuccessPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ checkout_id: string }>
|
||||||
|
}) {
|
||||||
|
const params = await searchParams;
|
||||||
|
const checkout_id = params.checkout_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useSearchParams } from "react-router";
|
||||||
|
|
||||||
|
export default function SuccessPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const checkout_id = searchParams.get("checkout_id");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/success")({
|
||||||
|
component: SuccessPage,
|
||||||
|
validateSearch: (search) => ({
|
||||||
|
checkout_id: search.checkout_id as string,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function SuccessPage() {
|
||||||
|
const { checkout_id } = useSearch({ from: "/success" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/success")({
|
||||||
|
component: SuccessPage,
|
||||||
|
validateSearch: (search) => ({
|
||||||
|
checkout_id: search.checkout_id as string,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function SuccessPage() {
|
||||||
|
const { checkout_id } = useSearch({ from: "/success" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/solid-router";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/success")({
|
||||||
|
component: SuccessPage,
|
||||||
|
validateSearch: (search) => ({
|
||||||
|
checkout_id: search.checkout_id as string,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function SuccessPage() {
|
||||||
|
const searchParams = Route.useSearch();
|
||||||
|
const checkout_id = searchParams().checkout_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
<Show when={checkout_id}>
|
||||||
|
<p>Checkout ID: {checkout_id}</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
const checkout_id = $page.url.searchParams.get('checkout_id');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1>Payment Successful!</h1>
|
||||||
|
{#if checkout_id}
|
||||||
|
<p>Checkout ID: {checkout_id}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -60,54 +60,54 @@ describe("Database and ORM Combinations", () => {
|
|||||||
orm: ORM;
|
orm: ORM;
|
||||||
error: string;
|
error: string;
|
||||||
}> = [
|
}> = [
|
||||||
// MongoDB with Drizzle (not supported)
|
// MongoDB with Drizzle (not supported)
|
||||||
{
|
{
|
||||||
database: "mongodb" as Database,
|
database: "mongodb" as Database,
|
||||||
orm: "drizzle" as ORM,
|
orm: "drizzle" as ORM,
|
||||||
error: "Drizzle ORM does not support MongoDB",
|
error: "Drizzle ORM does not support MongoDB",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mongoose with non-MongoDB
|
// Mongoose with non-MongoDB
|
||||||
{
|
{
|
||||||
database: "sqlite" as Database,
|
database: "sqlite" as Database,
|
||||||
orm: "mongoose" as ORM,
|
orm: "mongoose" as ORM,
|
||||||
error: "Mongoose ORM requires MongoDB database",
|
error: "Mongoose ORM requires MongoDB database",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
database: "postgres" as Database,
|
database: "postgres" as Database,
|
||||||
orm: "mongoose" as ORM,
|
orm: "mongoose" as ORM,
|
||||||
error: "Mongoose ORM requires MongoDB database",
|
error: "Mongoose ORM requires MongoDB database",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
database: "mysql" as Database,
|
database: "mysql" as Database,
|
||||||
orm: "mongoose" as ORM,
|
orm: "mongoose" as ORM,
|
||||||
error: "Mongoose ORM requires MongoDB database",
|
error: "Mongoose ORM requires MongoDB database",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Database without ORM
|
// Database without ORM
|
||||||
{
|
{
|
||||||
database: "sqlite" as Database,
|
database: "sqlite" as Database,
|
||||||
orm: "none" as ORM,
|
orm: "none" as ORM,
|
||||||
error: "Database selection requires an ORM",
|
error: "Database selection requires an ORM",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
database: "postgres" as Database,
|
database: "postgres" as Database,
|
||||||
orm: "none" as ORM,
|
orm: "none" as ORM,
|
||||||
error: "Database selection requires an ORM",
|
error: "Database selection requires an ORM",
|
||||||
},
|
},
|
||||||
|
|
||||||
// ORM without database
|
// ORM without database
|
||||||
{
|
{
|
||||||
database: "none" as Database,
|
database: "none" as Database,
|
||||||
orm: "drizzle" as ORM,
|
orm: "drizzle" as ORM,
|
||||||
error: "ORM selection requires a database",
|
error: "ORM selection requires a database",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
database: "none" as Database,
|
database: "none" as Database,
|
||||||
orm: "prisma" as ORM,
|
orm: "prisma" as ORM,
|
||||||
error: "ORM selection requires a database",
|
error: "ORM selection requires a database",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { database, orm, error } of invalidCombinations) {
|
for (const { database, orm, error } of invalidCombinations) {
|
||||||
it(`should fail with ${database} + ${orm}`, async () => {
|
it(`should fail with ${database} + ${orm}`, async () => {
|
||||||
|
|||||||
@@ -456,16 +456,16 @@ describe("Deployment Configurations", () => {
|
|||||||
webDeploy: TestConfig["webDeploy"];
|
webDeploy: TestConfig["webDeploy"];
|
||||||
serverDeploy: TestConfig["serverDeploy"];
|
serverDeploy: TestConfig["serverDeploy"];
|
||||||
}> = [
|
}> = [
|
||||||
{ webDeploy: "wrangler", serverDeploy: "wrangler" },
|
{ webDeploy: "wrangler", serverDeploy: "wrangler" },
|
||||||
{ webDeploy: "wrangler", serverDeploy: "alchemy" },
|
{ webDeploy: "wrangler", serverDeploy: "alchemy" },
|
||||||
{ webDeploy: "alchemy", serverDeploy: "wrangler" },
|
{ webDeploy: "alchemy", serverDeploy: "wrangler" },
|
||||||
{ webDeploy: "alchemy", serverDeploy: "alchemy" },
|
{ webDeploy: "alchemy", serverDeploy: "alchemy" },
|
||||||
{ webDeploy: "wrangler", serverDeploy: "none" },
|
{ webDeploy: "wrangler", serverDeploy: "none" },
|
||||||
{ webDeploy: "alchemy", serverDeploy: "none" },
|
{ webDeploy: "alchemy", serverDeploy: "none" },
|
||||||
{ webDeploy: "none", serverDeploy: "wrangler" },
|
{ webDeploy: "none", serverDeploy: "wrangler" },
|
||||||
{ webDeploy: "none", serverDeploy: "alchemy" },
|
{ webDeploy: "none", serverDeploy: "alchemy" },
|
||||||
{ webDeploy: "none", serverDeploy: "none" },
|
{ webDeploy: "none", serverDeploy: "none" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { webDeploy, serverDeploy } of deployOptions) {
|
for (const { webDeploy, serverDeploy } of deployOptions) {
|
||||||
it(`should work with webDeploy: ${webDeploy}, serverDeploy: ${serverDeploy}`, async () => {
|
it(`should work with webDeploy: ${webDeploy}, serverDeploy: ${serverDeploy}`, async () => {
|
||||||
|
|||||||
@@ -98,19 +98,19 @@ export async function runTRPCTest(config: TestConfig): Promise<TestResult> {
|
|||||||
const coreStackDefaults = willUseYesFlag
|
const coreStackDefaults = willUseYesFlag
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
frontend: ["tanstack-router"] as Frontend[],
|
frontend: ["tanstack-router"] as Frontend[],
|
||||||
backend: "hono" as Backend,
|
backend: "hono" as Backend,
|
||||||
runtime: "bun" as Runtime,
|
runtime: "bun" as Runtime,
|
||||||
api: "trpc" as API,
|
api: "trpc" as API,
|
||||||
database: "sqlite" as Database,
|
database: "sqlite" as Database,
|
||||||
orm: "drizzle" as ORM,
|
orm: "drizzle" as ORM,
|
||||||
auth: "none" as Auth,
|
auth: "none" as Auth,
|
||||||
addons: ["none"] as Addons[],
|
addons: ["none"] as Addons[],
|
||||||
examples: ["none"] as Examples[],
|
examples: ["none"] as Examples[],
|
||||||
dbSetup: "none" as DatabaseSetup,
|
dbSetup: "none" as DatabaseSetup,
|
||||||
webDeploy: "none" as WebDeploy,
|
webDeploy: "none" as WebDeploy,
|
||||||
serverDeploy: "none" as ServerDeploy,
|
serverDeploy: "none" as ServerDeploy,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build options object - let the CLI handle all validation
|
// Build options object - let the CLI handle all validation
|
||||||
const options: CreateInput = {
|
const options: CreateInput = {
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export function TechIcon({
|
|||||||
icon.includes("prisma") ||
|
icon.includes("prisma") ||
|
||||||
icon.includes("express") ||
|
icon.includes("express") ||
|
||||||
icon.includes("clerk") ||
|
icon.includes("clerk") ||
|
||||||
icon.includes("planetscale"))
|
icon.includes("planetscale") ||
|
||||||
|
icon.includes("polar"))
|
||||||
) {
|
) {
|
||||||
iconSrc = icon.replace(".svg", "-light.svg");
|
iconSrc = icon.replace(".svg", "-light.svg");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -818,6 +818,64 @@ export const analyzeStackCompatibility = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextStack.payments === "polar") {
|
||||||
|
if (nextStack.auth !== "better-auth") {
|
||||||
|
notes.payments.notes.push(
|
||||||
|
"Polar payments requires Better Auth. Payments will be set to 'None'.",
|
||||||
|
);
|
||||||
|
notes.auth.notes.push(
|
||||||
|
"Polar payments requires Better Auth. Payments will be disabled.",
|
||||||
|
);
|
||||||
|
notes.payments.hasIssue = true;
|
||||||
|
notes.auth.hasIssue = true;
|
||||||
|
nextStack.payments = "none";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "payments",
|
||||||
|
message: "Payments set to 'None' (Polar requires Better Auth)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStack.backend === "convex") {
|
||||||
|
notes.payments.notes.push(
|
||||||
|
"Polar payments is not compatible with Convex backend. Payments will be set to 'None'.",
|
||||||
|
);
|
||||||
|
notes.backend.notes.push(
|
||||||
|
"Polar payments is not compatible with Convex backend. Payments will be disabled.",
|
||||||
|
);
|
||||||
|
notes.payments.hasIssue = true;
|
||||||
|
notes.backend.hasIssue = true;
|
||||||
|
nextStack.payments = "none";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "payments",
|
||||||
|
message:
|
||||||
|
"Payments set to 'None' (Polar not compatible with Convex backend)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasWebFrontend = nextStack.webFrontend.some((f) => f !== "none");
|
||||||
|
if (
|
||||||
|
!hasWebFrontend &&
|
||||||
|
nextStack.nativeFrontend.some((f) => f !== "none")
|
||||||
|
) {
|
||||||
|
notes.payments.notes.push(
|
||||||
|
"Polar payments requires a web frontend. Payments will be set to 'None'.",
|
||||||
|
);
|
||||||
|
notes.webFrontend.notes.push(
|
||||||
|
"Polar payments requires a web frontend. Payments will be disabled.",
|
||||||
|
);
|
||||||
|
notes.payments.hasIssue = true;
|
||||||
|
notes.webFrontend.hasIssue = true;
|
||||||
|
nextStack.payments = "none";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "payments",
|
||||||
|
message: "Payments set to 'None' (Polar requires web frontend)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const incompatibleAddons: string[] = [];
|
const incompatibleAddons: string[] = [];
|
||||||
const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend);
|
const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend);
|
||||||
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend);
|
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend);
|
||||||
@@ -1536,6 +1594,22 @@ export const getDisabledReason = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (category === "payments" && optionId === "polar") {
|
||||||
|
if (finalStack.auth !== "better-auth") {
|
||||||
|
return "Polar payments requires Better Auth. Select Better Auth first.";
|
||||||
|
}
|
||||||
|
if (finalStack.backend === "convex") {
|
||||||
|
return "Polar payments is not compatible with Convex backend. Try Hono, Express, Fastify, or Elysia.";
|
||||||
|
}
|
||||||
|
const hasWebFrontend = finalStack.webFrontend.some((f) => f !== "none");
|
||||||
|
if (
|
||||||
|
!hasWebFrontend &&
|
||||||
|
finalStack.nativeFrontend.some((f) => f !== "none")
|
||||||
|
) {
|
||||||
|
return "Polar payments requires a web frontend. Select a web frontend first.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (category === "dbSetup" && optionId === "planetscale") {
|
if (category === "dbSetup" && optionId === "planetscale") {
|
||||||
if (finalStack.database !== "postgres" && finalStack.database !== "mysql") {
|
if (finalStack.database !== "postgres" && finalStack.database !== "mysql") {
|
||||||
return "PlanetScale requires PostgreSQL or MySQL database. Select PostgreSQL or MySQL first.";
|
return "PlanetScale requires PostgreSQL or MySQL database. Select PostgreSQL or MySQL first.";
|
||||||
|
|||||||
@@ -427,6 +427,24 @@ export const TECH_OPTIONS: Record<
|
|||||||
color: "from-red-400 to-red-600",
|
color: "from-red-400 to-red-600",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
payments: [
|
||||||
|
{
|
||||||
|
id: "polar",
|
||||||
|
name: "Polar",
|
||||||
|
description: "Turn your software into a business. 6 lines of code.",
|
||||||
|
icon: `${ICON_BASE_URL}/polar.svg`,
|
||||||
|
color: "from-purple-400 to-purple-600",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "none",
|
||||||
|
name: "No Payments",
|
||||||
|
description: "Skip payments integration",
|
||||||
|
icon: "",
|
||||||
|
color: "from-gray-400 to-gray-600",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
packageManager: [
|
packageManager: [
|
||||||
{
|
{
|
||||||
id: "npm",
|
id: "npm",
|
||||||
@@ -604,6 +622,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "none",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
@@ -629,6 +648,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
orm: "none",
|
orm: "none",
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
auth: "none",
|
auth: "none",
|
||||||
|
payments: "none",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: ["todo"],
|
examples: ["todo"],
|
||||||
@@ -654,6 +674,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "none",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
@@ -679,6 +700,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "none",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
@@ -704,6 +726,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
dbSetup: "turso",
|
dbSetup: "turso",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "polar",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["pwa", "biome", "husky", "tauri", "starlight", "turborepo"],
|
addons: ["pwa", "biome", "husky", "tauri", "starlight", "turborepo"],
|
||||||
examples: ["todo", "ai"],
|
examples: ["todo", "ai"],
|
||||||
@@ -727,6 +750,7 @@ export type StackState = {
|
|||||||
orm: string;
|
orm: string;
|
||||||
dbSetup: string;
|
dbSetup: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
|
payments: string;
|
||||||
packageManager: string;
|
packageManager: string;
|
||||||
addons: string[];
|
addons: string[];
|
||||||
examples: string[];
|
examples: string[];
|
||||||
@@ -748,6 +772,7 @@ export const DEFAULT_STACK: StackState = {
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
auth: "better-auth",
|
auth: "better-auth",
|
||||||
|
payments: "none",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const stackUrlKeys: UrlKeys<Record<keyof StackState, unknown>> = {
|
|||||||
orm: "orm",
|
orm: "orm",
|
||||||
dbSetup: "dbs",
|
dbSetup: "dbs",
|
||||||
auth: "au",
|
auth: "au",
|
||||||
|
payments: "pay",
|
||||||
packageManager: "pm",
|
packageManager: "pm",
|
||||||
addons: "add",
|
addons: "add",
|
||||||
examples: "ex",
|
examples: "ex",
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export const stackParsers = {
|
|||||||
auth: parseAsStringEnum<StackState["auth"]>(getValidIds("auth")).withDefault(
|
auth: parseAsStringEnum<StackState["auth"]>(getValidIds("auth")).withDefault(
|
||||||
DEFAULT_STACK.auth,
|
DEFAULT_STACK.auth,
|
||||||
),
|
),
|
||||||
|
payments: parseAsStringEnum<StackState["payments"]>(
|
||||||
|
getValidIds("payments"),
|
||||||
|
).withDefault(DEFAULT_STACK.payments),
|
||||||
packageManager: parseAsStringEnum<StackState["packageManager"]>(
|
packageManager: parseAsStringEnum<StackState["packageManager"]>(
|
||||||
getValidIds("packageManager"),
|
getValidIds("packageManager"),
|
||||||
).withDefault(DEFAULT_STACK.packageManager),
|
).withDefault(DEFAULT_STACK.packageManager),
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ const serverStackParsers = {
|
|||||||
auth: parseAsStringEnumServer<StackState["auth"]>(
|
auth: parseAsStringEnumServer<StackState["auth"]>(
|
||||||
getValidIds("auth"),
|
getValidIds("auth"),
|
||||||
).withDefault(DEFAULT_STACK.auth),
|
).withDefault(DEFAULT_STACK.auth),
|
||||||
|
payments: parseAsStringEnumServer<StackState["payments"]>(
|
||||||
|
getValidIds("payments"),
|
||||||
|
).withDefault(DEFAULT_STACK.payments),
|
||||||
packageManager: parseAsStringEnumServer<StackState["packageManager"]>(
|
packageManager: parseAsStringEnumServer<StackState["packageManager"]>(
|
||||||
getValidIds("packageManager"),
|
getValidIds("packageManager"),
|
||||||
).withDefault(DEFAULT_STACK.packageManager),
|
).withDefault(DEFAULT_STACK.packageManager),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
|
|||||||
"webDeploy",
|
"webDeploy",
|
||||||
"serverDeploy",
|
"serverDeploy",
|
||||||
"auth",
|
"auth",
|
||||||
|
"payments",
|
||||||
"packageManager",
|
"packageManager",
|
||||||
"addons",
|
"addons",
|
||||||
"examples",
|
"examples",
|
||||||
@@ -88,6 +89,7 @@ export function generateStackCommand(stack: StackState) {
|
|||||||
`--runtime ${stack.runtime}`,
|
`--runtime ${stack.runtime}`,
|
||||||
`--api ${stack.api}`,
|
`--api ${stack.api}`,
|
||||||
`--auth ${stack.auth}`,
|
`--auth ${stack.auth}`,
|
||||||
|
`--payments ${stack.payments}`,
|
||||||
`--database ${stack.database}`,
|
`--database ${stack.database}`,
|
||||||
`--orm ${stack.orm}`,
|
`--orm ${stack.orm}`,
|
||||||
`--db-setup ${stack.dbSetup}`,
|
`--db-setup ${stack.dbSetup}`,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TechCategory =
|
|||||||
| "webDeploy"
|
| "webDeploy"
|
||||||
| "serverDeploy"
|
| "serverDeploy"
|
||||||
| "auth"
|
| "auth"
|
||||||
|
| "payments"
|
||||||
| "packageManager"
|
| "packageManager"
|
||||||
| "addons"
|
| "addons"
|
||||||
| "examples"
|
| "examples"
|
||||||
|
|||||||
Reference in New Issue
Block a user