@better-t-stack/template-generator 3.28.3 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -694,8 +694,8 @@ function renameDevScriptsForAlchemy(vfs, config) {
694
694
  //#region src/utils/add-deps.ts
695
695
  const dependencyVersionMap = {
696
696
  typescript: "^6",
697
- "better-auth": "1.6.9",
698
- "@better-auth/expo": "1.6.9",
697
+ "better-auth": "1.6.11",
698
+ "@better-auth/expo": "1.6.11",
699
699
  "@clerk/backend": "^3.2.1",
700
700
  "@clerk/express": "^2.0.5",
701
701
  "@clerk/fastify": "^3.1.3",
@@ -715,16 +715,17 @@ const dependencyVersionMap = {
715
715
  "@types/ws": "^8.18.1",
716
716
  ws: "^8.18.3",
717
717
  mysql2: "^3.14.0",
718
- "@prisma/client": "^7.7.0",
719
- prisma: "^7.7.0",
720
- "@prisma/adapter-d1": "^7.7.0",
721
- "@prisma/adapter-neon": "^7.7.0",
722
- "@prisma/adapter-mariadb": "^7.7.0",
723
- "@prisma/adapter-libsql": "^7.7.0",
724
- "@prisma/adapter-better-sqlite3": "^7.7.0",
725
- "@prisma/adapter-pg": "^7.7.0",
726
- "@prisma/adapter-planetscale": "^7.7.0",
727
- mongoose: "^8.14.0",
718
+ "@prisma/client": "^7.8.0",
719
+ prisma: "^7.8.0",
720
+ "@prisma/adapter-d1": "^7.8.0",
721
+ "@prisma/adapter-neon": "^7.8.0",
722
+ "@prisma/adapter-mariadb": "^7.8.0",
723
+ "@prisma/adapter-libsql": "^7.8.0",
724
+ "@prisma/adapter-better-sqlite3": "^7.8.0",
725
+ "@prisma/adapter-pg": "^7.8.0",
726
+ "@prisma/adapter-planetscale": "^7.8.0",
727
+ mongoose: "^9.6.2",
728
+ mongodb: "^7.2.0",
728
729
  "vite-plugin-pwa": "^1.2.0",
729
730
  "@vite-pwa/assets-generator": "^1.0.2",
730
731
  "@tauri-apps/cli": "^2.4.0",
@@ -774,10 +775,11 @@ const dependencyVersionMap = {
774
775
  convex: "^1.33.1",
775
776
  "@convex-dev/react-query": "^0.1.0",
776
777
  "@convex-dev/agent": "^0.3.2",
778
+ "@convex-dev/polar": "^0.9.1",
777
779
  "convex-svelte": "^0.0.12",
778
780
  "convex-nuxt": "0.1.5",
779
781
  "convex-vue": "^0.1.5",
780
- "@convex-dev/better-auth": "^0.12.1",
782
+ "@convex-dev/better-auth": "^0.12.2",
781
783
  "@tanstack/svelte-query": "^5.85.3",
782
784
  "@tanstack/svelte-query-devtools": "^5.85.3",
783
785
  "@tanstack/vue-query-devtools": "^6.1.5",
@@ -806,8 +808,11 @@ const dependencyVersionMap = {
806
808
  "@t3-oss/env-core": "^0.13.1",
807
809
  "@t3-oss/env-nextjs": "^0.13.1",
808
810
  "@t3-oss/env-nuxt": "^0.13.1",
809
- "@polar-sh/better-auth": "^1.8.3",
810
- "@polar-sh/sdk": "^0.42.2",
811
+ "@polar-sh/better-auth": "^1.8.4",
812
+ "@polar-sh/checkout": "^0.2.1",
813
+ "@polar-sh/sdk": "^0.47.1",
814
+ "@stripe/react-stripe-js": "^4.0.2",
815
+ "@stripe/stripe-js": "^7.9.0",
811
816
  evlog: "^2.14.1"
812
817
  };
813
818
  /**
@@ -1488,7 +1493,7 @@ function addConvexDeps(vfs, frontend, frontendType) {
1488
1493
  }
1489
1494
  //#endregion
1490
1495
  //#region src/processors/auth-deps.ts
1491
- const CONVEX_BETTER_AUTH_VERSION = "1.6.9";
1496
+ const CONVEX_BETTER_AUTH_VERSION = "~1.6.9";
1492
1497
  function processAuthDeps(vfs, config) {
1493
1498
  const { auth, backend } = config;
1494
1499
  if (!auth || auth === "none") return;
@@ -1599,7 +1604,7 @@ function processConvexAuthDeps(vfs, config) {
1599
1604
  }
1600
1605
  }
1601
1606
  function processStandardAuthDeps(vfs, config) {
1602
- const { auth, backend, frontend } = config;
1607
+ const { auth, backend, frontend, orm } = config;
1603
1608
  const authPath = "packages/auth/package.json";
1604
1609
  const apiPath = "packages/api/package.json";
1605
1610
  const webPath = "apps/web/package.json";
@@ -1696,10 +1701,12 @@ function processStandardAuthDeps(vfs, config) {
1696
1701
  }
1697
1702
  } else if (auth === "better-auth") {
1698
1703
  if (authExists) {
1704
+ const authDependencies = ["better-auth"];
1705
+ if (orm === "mongoose") authDependencies.push("mongodb");
1699
1706
  addPackageDependency({
1700
1707
  vfs,
1701
1708
  packagePath: authPath,
1702
- dependencies: ["better-auth"]
1709
+ dependencies: authDependencies
1703
1710
  });
1704
1711
  if (hasNative) addPackageDependency({
1705
1712
  vfs,
@@ -1871,13 +1878,13 @@ function processPrismaDeps(vfs, config, dbPkgPath, webPkgPath, webExists) {
1871
1878
  addPackageDependency({
1872
1879
  vfs,
1873
1880
  packagePath: dbPkgPath,
1874
- customDependencies: { "@prisma/client": "6.19.0" },
1875
- customDevDependencies: { prisma: "6.19.0" }
1881
+ customDependencies: { "@prisma/client": "6.19.3" },
1882
+ customDevDependencies: { prisma: "6.19.3" }
1876
1883
  });
1877
1884
  if (webExists) addPackageDependency({
1878
1885
  vfs,
1879
1886
  packagePath: webPkgPath,
1880
- customDependencies: { "@prisma/client": "6.19.0" }
1887
+ customDependencies: { "@prisma/client": "6.19.3" }
1881
1888
  });
1882
1889
  return;
1883
1890
  }
@@ -2207,7 +2214,9 @@ function buildNativeVars(frontend, backend, auth) {
2207
2214
  });
2208
2215
  return vars;
2209
2216
  }
2210
- function buildConvexBackendVars(frontend, auth, examples) {
2217
+ function buildConvexBackendVars(frontend, auth, payments, examples) {
2218
+ const hasReactRouter = frontend.includes("react-router");
2219
+ const hasTanStackRouter = frontend.includes("tanstack-router");
2211
2220
  const hasNextJs = frontend.includes("next");
2212
2221
  const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2213
2222
  const hasWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || hasNextJs || frontend.includes("nuxt") || frontend.includes("solid") || frontend.includes("svelte") || frontend.includes("astro");
@@ -2220,6 +2229,12 @@ function buildConvexBackendVars(frontend, auth, examples) {
2220
2229
  comment: "Google AI API key for AI agent"
2221
2230
  });
2222
2231
  if (auth === "better-auth") {
2232
+ if (hasReactRouter || hasTanStackRouter) vars.push({
2233
+ key: "CONVEX_SITE_URL",
2234
+ value: "",
2235
+ condition: true,
2236
+ comment: "Same as CONVEX_URL but ends in .site"
2237
+ });
2223
2238
  if (hasNative) vars.push({
2224
2239
  key: "EXPO_PUBLIC_CONVEX_SITE_URL",
2225
2240
  value: "",
@@ -2244,9 +2259,26 @@ function buildConvexBackendVars(frontend, auth, examples) {
2244
2259
  comment: "Web app URL for authentication (for Expo web support)"
2245
2260
  });
2246
2261
  }
2262
+ if (payments === "polar") vars.push({
2263
+ key: "POLAR_ORGANIZATION_TOKEN",
2264
+ value: "",
2265
+ condition: true,
2266
+ comment: "Polar organization token"
2267
+ }, {
2268
+ key: "POLAR_WEBHOOK_SECRET",
2269
+ value: "",
2270
+ condition: true,
2271
+ comment: "Polar webhook secret"
2272
+ }, {
2273
+ key: "POLAR_SERVER",
2274
+ value: "sandbox",
2275
+ condition: true,
2276
+ comment: "Polar environment: sandbox or production"
2277
+ });
2247
2278
  return vars;
2248
2279
  }
2249
- function buildConvexCommentBlocks(frontend, auth, examples) {
2280
+ function buildConvexCommentBlocks(frontend, auth, payments, examples) {
2281
+ const needsConvexSiteUrl = frontend.includes("react-router") || frontend.includes("tanstack-router");
2250
2282
  const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2251
2283
  const hasWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next") || frontend.includes("nuxt") || frontend.includes("solid") || frontend.includes("svelte") || frontend.includes("astro");
2252
2284
  const defaultSiteUrl = hasNative && !hasWeb ? "http://localhost:8081" : frontend.includes("react-router") || frontend.includes("svelte") ? "http://localhost:5173" : frontend.includes("astro") ? "http://localhost:4321" : "http://localhost:3001";
@@ -2257,16 +2289,28 @@ function buildConvexCommentBlocks(frontend, auth, examples) {
2257
2289
  `;
2258
2290
  if (auth === "better-auth") commentBlocks += `# Set Convex environment variables
2259
2291
  # npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
2260
- ${hasWeb || hasNative ? `# npx convex env set SITE_URL ${defaultSiteUrl}\n` : ""}`;
2292
+ ${needsConvexSiteUrl ? "# npx convex env set CONVEX_SITE_URL https://<YOUR_CONVEX_SITE_URL>\n" : ""}${hasWeb || hasNative ? `# npx convex env set SITE_URL ${defaultSiteUrl}\n` : ""}`;
2293
+ if (payments === "polar") commentBlocks += `# Set Polar environment variables
2294
+ # npx convex env set POLAR_ORGANIZATION_TOKEN your_polar_token
2295
+ # npx convex env set POLAR_WEBHOOK_SECRET your_polar_webhook_secret
2296
+ # Optional: npx convex env set POLAR_SERVER production
2297
+ # Create a Polar webhook at https://<your-convex-site-url>/polar/events
2298
+ # Enable: product.created, product.updated, subscription.created, subscription.updated
2299
+
2300
+ `;
2261
2301
  return commentBlocks;
2262
2302
  }
2263
- function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtime, webDeploy, serverDeploy, payments, examples) {
2303
+ function buildServerVars(backend, frontend, projectName, auth, api, database, dbSetup, runtime, webDeploy, serverDeploy, payments, examples) {
2264
2304
  const hasReactRouter = frontend.includes("react-router");
2265
2305
  const hasSvelte = frontend.includes("svelte");
2266
2306
  const hasAstro = frontend.includes("astro");
2307
+ const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2308
+ const hasWeb = hasReactRouter || hasSvelte || hasAstro || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next") || frontend.includes("nuxt") || frontend.includes("solid");
2267
2309
  let corsOrigin = "http://localhost:3001";
2268
2310
  if (hasAstro) corsOrigin = "http://localhost:4321";
2269
2311
  else if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173";
2312
+ const betterAuthUrl = backend === "self" ? hasSvelte ? "http://localhost:5173" : hasAstro ? "http://localhost:4321" : "http://localhost:3001" : "http://localhost:3000";
2313
+ const polarSuccessUrl = hasNative && !hasWeb ? `${betterAuthUrl}/polar/success` : `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`;
2270
2314
  let databaseUrl = null;
2271
2315
  if (database !== "none" && dbSetup === "none") switch (database) {
2272
2316
  case "postgres":
@@ -2298,7 +2342,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2298
2342
  },
2299
2343
  {
2300
2344
  key: "BETTER_AUTH_URL",
2301
- value: backend === "self" ? hasSvelte ? "http://localhost:5173" : hasAstro ? "http://localhost:4321" : "http://localhost:3001" : "http://localhost:3000",
2345
+ value: betterAuthUrl,
2302
2346
  condition: hasBetterAuth
2303
2347
  },
2304
2348
  {
@@ -2318,7 +2362,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2318
2362
  },
2319
2363
  {
2320
2364
  key: "POLAR_SUCCESS_URL",
2321
- value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
2365
+ value: polarSuccessUrl,
2322
2366
  condition: payments === "polar"
2323
2367
  },
2324
2368
  {
@@ -2339,7 +2383,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2339
2383
  ];
2340
2384
  }
2341
2385
  function processEnvVariables(vfs, config) {
2342
- const { backend, frontend, database, auth, api, examples, dbSetup, webDeploy, serverDeploy, runtime, payments } = config;
2386
+ const { backend, frontend, projectName, database, auth, api, examples, dbSetup, webDeploy, serverDeploy, runtime, payments } = config;
2343
2387
  const hasReactRouter = frontend.includes("react-router");
2344
2388
  const hasTanStackRouter = frontend.includes("tanstack-router");
2345
2389
  const hasTanStackStart = frontend.includes("tanstack-start");
@@ -2360,13 +2404,13 @@ function processEnvVariables(vfs, config) {
2360
2404
  const convexBackendDir = "packages/backend";
2361
2405
  if (vfs.directoryExists(convexBackendDir)) {
2362
2406
  const envLocalPath = `${convexBackendDir}/.env.local`;
2363
- const commentBlocks = buildConvexCommentBlocks(frontend, auth, examples);
2407
+ const commentBlocks = buildConvexCommentBlocks(frontend, auth, payments, examples);
2364
2408
  if (commentBlocks) {
2365
2409
  let currentContent = "";
2366
2410
  if (vfs.exists(envLocalPath)) currentContent = vfs.readFile(envLocalPath) || "";
2367
2411
  vfs.writeFile(envLocalPath, commentBlocks + currentContent);
2368
2412
  }
2369
- const convexBackendVars = buildConvexBackendVars(frontend, auth, examples);
2413
+ const convexBackendVars = buildConvexBackendVars(frontend, auth, payments, examples);
2370
2414
  if (convexBackendVars.length > 0) {
2371
2415
  let existingContent = "";
2372
2416
  if (vfs.exists(envLocalPath)) existingContent = vfs.readFile(envLocalPath) || "";
@@ -2376,7 +2420,7 @@ function processEnvVariables(vfs, config) {
2376
2420
  }
2377
2421
  return;
2378
2422
  }
2379
- const serverVars = buildServerVars(backend, frontend, auth, api, database, dbSetup, runtime, webDeploy, serverDeploy, payments, examples);
2423
+ const serverVars = buildServerVars(backend, frontend, projectName, auth, api, database, dbSetup, runtime, webDeploy, serverDeploy, payments, examples);
2380
2424
  if (backend === "self") {
2381
2425
  const webDir = "apps/web";
2382
2426
  if (vfs.directoryExists(webDir)) writeEnvFile(vfs, `${webDir}/.env`, serverVars);
@@ -2597,11 +2641,37 @@ function getDeployTargets() {
2597
2641
  //#endregion
2598
2642
  //#region src/processors/payments-deps.ts
2599
2643
  function processPaymentsDeps(vfs, config) {
2600
- const { payments, frontend } = config;
2644
+ const { payments, frontend, backend } = config;
2601
2645
  if (!payments || payments === "none") return;
2646
+ const backendPath = "packages/backend/package.json";
2602
2647
  const authPath = "packages/auth/package.json";
2603
2648
  const webPath = "apps/web/package.json";
2604
2649
  if (payments === "polar") {
2650
+ if (backend === "convex") {
2651
+ if (vfs.exists(backendPath)) addPackageDependency({
2652
+ vfs,
2653
+ packagePath: backendPath,
2654
+ dependencies: ["@convex-dev/polar", "@polar-sh/sdk"]
2655
+ });
2656
+ if (vfs.exists(webPath)) {
2657
+ if (frontend.some((f) => [
2658
+ "react-router",
2659
+ "tanstack-router",
2660
+ "tanstack-start",
2661
+ "next"
2662
+ ].includes(f))) addPackageDependency({
2663
+ vfs,
2664
+ packagePath: webPath,
2665
+ dependencies: [
2666
+ "@convex-dev/polar",
2667
+ "@polar-sh/checkout",
2668
+ "@stripe/react-stripe-js",
2669
+ "@stripe/stripe-js"
2670
+ ]
2671
+ });
2672
+ }
2673
+ return;
2674
+ }
2605
2675
  if (vfs.exists(authPath)) addPackageDependency({
2606
2676
  vfs,
2607
2677
  packagePath: authPath,
@@ -3275,7 +3345,6 @@ function getDeployTasks() {
3275
3345
  }
3276
3346
  //#endregion
3277
3347
  //#region src/processors/workspace-deps.ts
3278
- const NATIVE_TYPESCRIPT_VERSION = "~5.9.2";
3279
3348
  function processWorkspaceDeps(vfs, config) {
3280
3349
  const { projectName, packageManager, runtime, backend, database, auth, api, serverDeploy, webDeploy } = config;
3281
3350
  const workspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
@@ -3411,10 +3480,7 @@ function processWorkspaceDeps(vfs, config) {
3411
3480
  packagePath: "apps/native/package.json",
3412
3481
  dependencies: commonDeps,
3413
3482
  customDependencies: nativeDeps,
3414
- customDevDependencies: {
3415
- ...configDep,
3416
- typescript: NATIVE_TYPESCRIPT_VERSION
3417
- }
3483
+ customDevDependencies: configDep
3418
3484
  });
3419
3485
  }
3420
3486
  }
@@ -3445,11 +3511,6 @@ function processDependencies(vfs, config) {
3445
3511
  }
3446
3512
  //#endregion
3447
3513
  //#region src/template-handlers/utils.ts
3448
- function hasTemplatesWithPrefix(templates, prefix) {
3449
- const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
3450
- for (const path of templates.keys()) if (path.startsWith(normalizedPrefix)) return true;
3451
- return false;
3452
- }
3453
3514
  function processSingleTemplate(vfs, templates, templatePath, destPath, config) {
3454
3515
  const templateKey = templatePath.endsWith(".hbs") ? templatePath : `${templatePath}.hbs`;
3455
3516
  const content = templates.get(templateKey);
@@ -3725,7 +3786,6 @@ async function processAuthTemplates(vfs, templates, config) {
3725
3786
  //#region src/template-handlers/payments.ts
3726
3787
  async function processPaymentsTemplates(vfs, templates, config) {
3727
3788
  if (!config.payments || config.payments === "none") return;
3728
- if (config.backend === "convex") return;
3729
3789
  const hasReactWeb = config.frontend.some((f) => [
3730
3790
  "tanstack-router",
3731
3791
  "react-router",
@@ -3735,7 +3795,10 @@ async function processPaymentsTemplates(vfs, templates, config) {
3735
3795
  const hasNuxtWeb = config.frontend.includes("nuxt");
3736
3796
  const hasSvelteWeb = config.frontend.includes("svelte");
3737
3797
  const hasSolidWeb = config.frontend.includes("solid");
3738
- if (config.backend !== "none") processTemplatesFromPrefix(vfs, templates, `payments/${config.payments}/server/base`, "packages/auth", config);
3798
+ if (config.backend === "convex") {
3799
+ processTemplatesFromPrefix(vfs, templates, `payments/${config.payments}/convex/backend`, "packages/backend", config);
3800
+ return;
3801
+ } else if (config.backend !== "none") processTemplatesFromPrefix(vfs, templates, `payments/${config.payments}/server/base`, "packages/auth", config);
3739
3802
  if (hasReactWeb) {
3740
3803
  const reactFramework = config.frontend.find((f) => [
3741
3804
  "tanstack-router",
@@ -3829,11 +3892,9 @@ async function processExtrasTemplates(vfs, templates, config) {
3829
3892
  "native-unistyles"
3830
3893
  ].includes(f));
3831
3894
  const hasNuxt = config.frontend.includes("nuxt");
3832
- if (config.packageManager === "pnpm") {
3833
- if (hasTemplatesWithPrefix(templates, "extras")) processTemplatesFromPrefix(vfs, templates, "extras/pnpm-workspace.yaml", "", config);
3834
- }
3835
- if (config.packageManager === "bun") processTemplatesFromPrefix(vfs, templates, "extras/bunfig.toml", "", config);
3836
- if (config.packageManager === "pnpm" && (hasNative || hasNuxt)) processTemplatesFromPrefix(vfs, templates, "extras/_npmrc", "", config);
3895
+ if (config.packageManager === "pnpm") processSingleTemplate(vfs, templates, "extras/pnpm-workspace.yaml", "pnpm-workspace.yaml", config);
3896
+ if (config.packageManager === "bun") processSingleTemplate(vfs, templates, "extras/bunfig.toml", "bunfig.toml", config);
3897
+ if (config.packageManager === "pnpm" && (hasNative || hasNuxt)) processSingleTemplate(vfs, templates, "extras/_npmrc", ".npmrc", config);
3837
3898
  if (config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare") processSingleTemplate(vfs, templates, "extras/env.d.ts", "packages/env/env.d.ts", config);
3838
3899
  }
3839
3900
  //#endregion
@@ -4792,7 +4853,7 @@ import { auth } from "@{{projectName}}/auth";
4792
4853
  {{/if}}
4793
4854
  {{/if}}
4794
4855
 
4795
- export async function createContext(req: NextRequest){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4856
+ export async function createContext({{#if (eq auth "none")}}_req{{else}}req{{/if}}: NextRequest){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4796
4857
  {{#if (eq auth "better-auth")}}
4797
4858
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
4798
4859
  headers: req.headers,
@@ -4824,7 +4885,7 @@ import { auth } from "@{{projectName}}/auth";
4824
4885
  {{/if}}
4825
4886
  {{/if}}
4826
4887
 
4827
- export async function createContext({ req }: { req: Request }){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4888
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ req }{{/if}}: { req: Request }){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4828
4889
  {{#if (eq auth "better-auth")}}
4829
4890
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
4830
4891
  headers: req.headers,
@@ -4860,7 +4921,7 @@ export type CreateContextOptions = {
4860
4921
  headers: Headers;
4861
4922
  };
4862
4923
 
4863
- export async function createContext({ headers }: CreateContextOptions) {
4924
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ headers }{{/if}}: CreateContextOptions) {
4864
4925
  {{#if (eq auth "better-auth")}}
4865
4926
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({ headers });
4866
4927
  return {
@@ -4894,7 +4955,7 @@ export type CreateContextOptions = {
4894
4955
  {{/if}}
4895
4956
  };
4896
4957
 
4897
- export async function createContext({ headers{{#if (eq webDeploy "cloudflare")}}, env{{/if}} }: CreateContextOptions) {
4958
+ export async function createContext({{#if (eq auth "none")}}{{#if (eq webDeploy "cloudflare")}}{ env: _env }{{else}}_options{{/if}}{{else}}{ headers{{#if (eq webDeploy "cloudflare")}}, env{{/if}} }{{/if}}: CreateContextOptions) {
4898
4959
  {{#if (eq auth "better-auth")}}
4899
4960
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth({{#if (eq webDeploy "cloudflare")}}env{{/if}}){{else}}auth{{/if}}.api.getSession({ headers });
4900
4961
  return {
@@ -4928,7 +4989,7 @@ export type CreateContextOptions = {
4928
4989
  headers: Headers;
4929
4990
  };
4930
4991
 
4931
- export async function createContext({ headers }: CreateContextOptions) {
4992
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ headers }{{/if}}: CreateContextOptions) {
4932
4993
  {{#if (eq auth "better-auth")}}
4933
4994
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({ headers });
4934
4995
  return {
@@ -4957,7 +5018,7 @@ export type CreateContextOptions = {
4957
5018
  context: HonoContext;
4958
5019
  };
4959
5020
 
4960
- export async function createContext({ context }: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5021
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ context }{{/if}}: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4961
5022
  {{#if (eq auth "better-auth")}}
4962
5023
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
4963
5024
  headers: context.req.raw.headers,
@@ -4990,7 +5051,7 @@ export type CreateContextOptions = {
4990
5051
  context: ElysiaContext;
4991
5052
  };
4992
5053
 
4993
- export async function createContext({ context }: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5054
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ context }{{/if}}: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
4994
5055
  {{#if (eq auth "better-auth")}}
4995
5056
  const session = await auth.api.getSession({
4996
5057
  headers: context.request.headers,
@@ -5026,7 +5087,7 @@ interface CreateContextOptions {
5026
5087
  req: Request;
5027
5088
  }
5028
5089
 
5029
- export async function createContext(opts: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5090
+ export async function createContext({{#if (eq auth "none")}}_opts{{else}}opts{{/if}}: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5030
5091
  {{#if (eq auth "better-auth")}}
5031
5092
  const session = await auth.api.getSession({
5032
5093
  headers: fromNodeHeaders(opts.req.headers),
@@ -5076,6 +5137,7 @@ export async function createContext(req: {{#if (eq auth "clerk")}}Parameters<typ
5076
5137
  session: null,
5077
5138
  };
5078
5139
  {{else}}
5140
+ void req;
5079
5141
  return {
5080
5142
  auth: null,
5081
5143
  session: null,
@@ -5094,7 +5156,7 @@ export async function createContext() {
5094
5156
 
5095
5157
  export type Context = Awaited<ReturnType<typeof createContext>>;
5096
5158
  `],
5097
- ["api/orpc/server/src/index.ts.hbs", `import { ORPCError, os } from "@orpc/server";
5159
+ ["api/orpc/server/src/index.ts.hbs", `import { {{#if (or (eq auth "better-auth") (eq auth "clerk"))}}ORPCError, {{/if}}os } from "@orpc/server";
5098
5160
  import type { Context } from "./context";
5099
5161
 
5100
5162
  export const o = os.$context<Context>();
@@ -5716,7 +5778,7 @@ import { auth } from "@{{projectName}}/auth";
5716
5778
  {{/if}}
5717
5779
  {{/if}}
5718
5780
 
5719
- export async function createContext(req: NextRequest){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5781
+ export async function createContext({{#if (eq auth "none")}}_req{{else}}req{{/if}}: NextRequest){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5720
5782
  {{#if (eq auth "better-auth")}}
5721
5783
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
5722
5784
  headers: req.headers,
@@ -5748,7 +5810,7 @@ import { auth } from "@{{projectName}}/auth";
5748
5810
  {{/if}}
5749
5811
  {{/if}}
5750
5812
 
5751
- export async function createContext({ req }: { req: Request }){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5813
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ req }{{/if}}: { req: Request }){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5752
5814
  {{#if (eq auth "better-auth")}}
5753
5815
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
5754
5816
  headers: req.headers,
@@ -5785,7 +5847,7 @@ export type CreateContextOptions = {
5785
5847
  context: HonoContext;
5786
5848
  };
5787
5849
 
5788
- export async function createContext({ context }: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5850
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ context }{{/if}}: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5789
5851
  {{#if (eq auth "better-auth")}}
5790
5852
  const session = await {{#if (or (eq runtime "workers") (eq serverDeploy "cloudflare") (and (eq backend "self") (eq webDeploy "cloudflare")))}}createAuth(){{else}}auth{{/if}}.api.getSession({
5791
5853
  headers: context.req.raw.headers,
@@ -5818,7 +5880,7 @@ export type CreateContextOptions = {
5818
5880
  context: ElysiaContext;
5819
5881
  };
5820
5882
 
5821
- export async function createContext({ context }: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5883
+ export async function createContext({{#if (eq auth "none")}}_options{{else}}{ context }{{/if}}: CreateContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5822
5884
  {{#if (eq auth "better-auth")}}
5823
5885
  const session = await auth.api.getSession({
5824
5886
  headers: context.request.headers,
@@ -5850,7 +5912,7 @@ import { auth } from "@{{projectName}}/auth";
5850
5912
  import { getAuth } from "@clerk/express";
5851
5913
  {{/if}}
5852
5914
 
5853
- export async function createContext(opts: CreateExpressContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5915
+ export async function createContext({{#if (eq auth "none")}}_opts{{else}}opts{{/if}}: CreateExpressContextOptions){{#if (eq auth "clerk")}}: Promise<ClerkRequestContext>{{/if}} {
5854
5916
  {{#if (eq auth "better-auth")}}
5855
5917
  const session = await auth.api.getSession({
5856
5918
  headers: fromNodeHeaders(opts.req.headers),
@@ -5898,6 +5960,7 @@ export async function createContext({ req }: CreateFastifyContextOptions){{#if (
5898
5960
  session: null,
5899
5961
  };
5900
5962
  {{else}}
5963
+ void req;
5901
5964
  return {
5902
5965
  auth: null,
5903
5966
  session: null,
@@ -5916,7 +5979,7 @@ export async function createContext() {
5916
5979
 
5917
5980
  export type Context = Awaited<ReturnType<typeof createContext>>;
5918
5981
  `],
5919
- ["api/trpc/server/src/index.ts.hbs", `import { initTRPC, TRPCError } from "@trpc/server";
5982
+ ["api/trpc/server/src/index.ts.hbs", `import { initTRPC{{#if (or (eq auth "better-auth") (eq auth "clerk"))}}, TRPCError{{/if}} } from "@trpc/server";
5920
5983
  import type { Context } from "./context";
5921
5984
 
5922
5985
  export const t = initTRPC.context<Context>().create();
@@ -6184,11 +6247,14 @@ export const authComponent = createClient<DataModel>(components.betterAuth);
6184
6247
 
6185
6248
  function createAuth(ctx: GenericCtx<DataModel>) {
6186
6249
  return betterAuth({
6250
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router"))}}
6251
+ baseURL: process.env.CONVEX_SITE_URL,
6252
+ {{/if}}
6187
6253
  {{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
6188
6254
  baseURL: siteUrl,
6189
6255
  {{/if}}
6190
6256
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
6191
- trustedOrigins: [siteUrl, nativeAppUrl, ...(process.env.NODE_ENV === "development" ? ["exp://", "exp://**", "exp://192.168.*.*:*/**"] : [])],
6257
+ trustedOrigins: [siteUrl, nativeAppUrl, "exp://"],
6192
6258
  {{else if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
6193
6259
  trustedOrigins: [siteUrl],
6194
6260
  {{else if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
@@ -6225,21 +6291,56 @@ export const getCurrentUser = query({
6225
6291
  `],
6226
6292
  ["auth/better-auth/convex/backend/convex/http.ts.hbs", `import { httpRouter } from "convex/server";
6227
6293
  import { authComponent, createAuth } from "./auth";
6294
+ {{#if (and (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
6295
+ import { httpAction } from "./_generated/server";
6296
+ {{/if}}
6297
+ {{#if (eq payments "polar")}}
6298
+ import { polar } from "./polar";
6299
+ {{/if}}
6228
6300
 
6229
6301
  const http = httpRouter();
6230
6302
 
6231
- {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles") (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
6232
- {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
6233
- authComponent.registerRoutesLazy(http, createAuth, {
6234
- cors: true,
6235
- trustedOrigins: [process.env.SITE_URL!],
6303
+ {{#if (and (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
6304
+ const nativeAppUrl = process.env.NATIVE_APP_URL || "{{projectName}}://";
6305
+ const allowedNativeProtocols = new Set(["exp:", new URL(nativeAppUrl).protocol]);
6306
+
6307
+ http.route({
6308
+ path: "/polar/success",
6309
+ method: "GET",
6310
+ handler: httpAction(async (_ctx, request) => {
6311
+ const requestUrl = new URL(request.url);
6312
+ const returnUrl = requestUrl.searchParams.get("returnUrl") || nativeAppUrl;
6313
+
6314
+ let redirectUrl: URL;
6315
+ try {
6316
+ redirectUrl = new URL(returnUrl);
6317
+ } catch {
6318
+ return new Response("Invalid return URL", { status: 400 });
6319
+ }
6320
+
6321
+ if (!allowedNativeProtocols.has(redirectUrl.protocol)) {
6322
+ return new Response("Invalid return URL", { status: 400 });
6323
+ }
6324
+
6325
+ return new Response(null, {
6326
+ status: 302,
6327
+ headers: {
6328
+ Location: redirectUrl.toString(),
6329
+ },
6330
+ });
6331
+ }),
6236
6332
  });
6237
- {{else}}
6238
- authComponent.registerRoutes(http, createAuth, { cors: true });
6333
+
6239
6334
  {{/if}}
6335
+ {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles") (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
6336
+ authComponent.registerRoutes(http, createAuth, { cors: true });
6240
6337
  {{else}}
6241
6338
  authComponent.registerRoutes(http, createAuth);
6242
6339
  {{/if}}
6340
+ {{#if (eq payments "polar")}}
6341
+
6342
+ polar.registerRoutes(http);
6343
+ {{/if}}
6243
6344
 
6244
6345
  export default http;
6245
6346
  `],
@@ -7560,6 +7661,10 @@ export const { GET, POST } = handler;
7560
7661
  import SignInForm from "@/components/sign-in-form";
7561
7662
  import SignUpForm from "@/components/sign-up-form";
7562
7663
  import UserMenu from "@/components/user-menu";
7664
+ {{#if (eq payments "polar")}}
7665
+ import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
7666
+ import { buttonVariants } from "@{{projectName}}/ui/components/button";
7667
+ {{/if}}
7563
7668
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
7564
7669
  import {
7565
7670
  Authenticated,
@@ -7569,18 +7674,58 @@ import {
7569
7674
  } from "convex/react";
7570
7675
  import { useState } from "react";
7571
7676
 
7677
+ function DashboardContent() {
7678
+ const privateData = useQuery(api.privateData.get);
7679
+ {{#if (eq payments "polar")}}
7680
+ const products = useQuery(api.polar.listAllProducts);
7681
+ const subscription = useQuery(api.polar.getCurrentSubscription);
7682
+
7683
+ const product = products?.find((product: { isRecurring?: boolean }) => product.isRecurring);
7684
+ const hasActiveSubscription = Boolean(subscription);
7685
+ {{/if}}
7686
+
7687
+ return (
7688
+ <div>
7689
+ <h1>Dashboard</h1>
7690
+ <p>privateData: {privateData?.message}</p>
7691
+ {{#if (eq payments "polar")}}
7692
+ <p>Plan: {hasActiveSubscription ? "Active" : "Free"}</p>
7693
+ {subscription === undefined ? (
7694
+ <p>Loading subscription options...</p>
7695
+ ) : hasActiveSubscription ? (
7696
+ <CustomerPortalLink
7697
+ polarApi={api.polar}
7698
+ className={buttonVariants({ variant: "outline" })}
7699
+ >
7700
+ Manage Subscription
7701
+ </CustomerPortalLink>
7702
+ ) : products === undefined ? (
7703
+ <p>Loading subscription options...</p>
7704
+ ) : product ? (
7705
+ <CheckoutLink
7706
+ polarApi={api.polar}
7707
+ productIds={[product.id]}
7708
+ embed={false}
7709
+ className={buttonVariants({ variant: "default" })}
7710
+ >
7711
+ Upgrade
7712
+ </CheckoutLink>
7713
+ ) : (
7714
+ <p>No recurring plans available.</p>
7715
+ )}
7716
+ {{/if}}
7717
+ <UserMenu />
7718
+ </div>
7719
+ );
7720
+ }
7721
+
7572
7722
  export default function DashboardPage() {
7573
7723
  const [showSignIn, setShowSignIn] = useState(false);
7574
- const privateData = useQuery(api.privateData.get);
7575
7724
 
7576
7725
  return (
7577
7726
  <>
7578
7727
  <Authenticated>
7579
- <div>
7580
- <h1>Dashboard</h1>
7581
- <p>privateData: {privateData?.message}</p>
7582
- <UserMenu />
7583
- </div>
7728
+ <DashboardContent />
7584
7729
  </Authenticated>
7585
7730
  <Unauthenticated>
7586
7731
  {showSignIn ? (
@@ -8300,12 +8445,16 @@ import { env } from "@{{projectName}}/env/web";
8300
8445
 
8301
8446
  export const authClient = createAuthClient({
8302
8447
  baseURL: env.VITE_CONVEX_SITE_URL,
8303
- plugins: [crossDomainClient(), convexClient()],
8448
+ plugins: [convexClient(), crossDomainClient()],
8304
8449
  });
8305
8450
  `],
8306
8451
  ["auth/better-auth/convex/web/react/react-router/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
8307
8452
  import SignUpForm from "@/components/sign-up-form";
8308
8453
  import UserMenu from "@/components/user-menu";
8454
+ {{#if (eq payments "polar")}}
8455
+ import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
8456
+ import { buttonVariants } from "@{{projectName}}/ui/components/button";
8457
+ {{/if}}
8309
8458
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
8310
8459
  import {
8311
8460
  Authenticated,
@@ -8317,11 +8466,44 @@ import { useState } from "react";
8317
8466
 
8318
8467
  function PrivateDashboardContent() {
8319
8468
  const privateData = useQuery(api.privateData.get);
8469
+ {{#if (eq payments "polar")}}
8470
+ const products = useQuery(api.polar.listAllProducts);
8471
+ const subscription = useQuery(api.polar.getCurrentSubscription);
8472
+
8473
+ const product = products?.find((product: { isRecurring?: boolean }) => product.isRecurring);
8474
+ const hasActiveSubscription = Boolean(subscription);
8475
+ {{/if}}
8320
8476
 
8321
8477
  return (
8322
8478
  <div>
8323
8479
  <h1>Dashboard</h1>
8324
8480
  <p>privateData: {privateData?.message}</p>
8481
+ {{#if (eq payments "polar")}}
8482
+ <p>Plan: {hasActiveSubscription ? "Active" : "Free"}</p>
8483
+ {subscription === undefined ? (
8484
+ <p>Loading subscription options...</p>
8485
+ ) : hasActiveSubscription ? (
8486
+ <CustomerPortalLink
8487
+ polarApi={api.polar}
8488
+ className={buttonVariants({ variant: "outline" })}
8489
+ >
8490
+ Manage Subscription
8491
+ </CustomerPortalLink>
8492
+ ) : products === undefined ? (
8493
+ <p>Loading subscription options...</p>
8494
+ ) : product ? (
8495
+ <CheckoutLink
8496
+ polarApi={api.polar}
8497
+ productIds={[product.id]}
8498
+ embed={false}
8499
+ className={buttonVariants({ variant: "default" })}
8500
+ >
8501
+ Upgrade
8502
+ </CheckoutLink>
8503
+ ) : (
8504
+ <p>No recurring plans available.</p>
8505
+ )}
8506
+ {{/if}}
8325
8507
  <UserMenu />
8326
8508
  </div>
8327
8509
  );
@@ -8704,12 +8886,16 @@ import { env } from "@{{projectName}}/env/web";
8704
8886
 
8705
8887
  export const authClient = createAuthClient({
8706
8888
  baseURL: env.VITE_CONVEX_SITE_URL,
8707
- plugins: [crossDomainClient(), convexClient()],
8889
+ plugins: [convexClient(), crossDomainClient()],
8708
8890
  });
8709
8891
  `],
8710
8892
  ["auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
8711
8893
  import SignUpForm from "@/components/sign-up-form";
8712
8894
  import UserMenu from "@/components/user-menu";
8895
+ {{#if (eq payments "polar")}}
8896
+ import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
8897
+ import { buttonVariants } from "@{{projectName}}/ui/components/button";
8898
+ {{/if}}
8713
8899
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
8714
8900
  import { createFileRoute } from "@tanstack/react-router";
8715
8901
  import {
@@ -8726,11 +8912,44 @@ export const Route = createFileRoute("/dashboard")({
8726
8912
 
8727
8913
  function PrivateDashboardContent() {
8728
8914
  const privateData = useQuery(api.privateData.get);
8915
+ {{#if (eq payments "polar")}}
8916
+ const products = useQuery(api.polar.listAllProducts);
8917
+ const subscription = useQuery(api.polar.getCurrentSubscription);
8918
+
8919
+ const product = products?.find((product: { isRecurring?: boolean }) => product.isRecurring);
8920
+ const hasActiveSubscription = Boolean(subscription);
8921
+ {{/if}}
8729
8922
 
8730
8923
  return (
8731
8924
  <div>
8732
8925
  <h1>Dashboard</h1>
8733
8926
  <p>privateData: {privateData?.message}</p>
8927
+ {{#if (eq payments "polar")}}
8928
+ <p>Plan: {hasActiveSubscription ? "Active" : "Free"}</p>
8929
+ {subscription === undefined ? (
8930
+ <p>Loading subscription options...</p>
8931
+ ) : hasActiveSubscription ? (
8932
+ <CustomerPortalLink
8933
+ polarApi={api.polar}
8934
+ className={buttonVariants({ variant: "outline" })}
8935
+ >
8936
+ Manage Subscription
8937
+ </CustomerPortalLink>
8938
+ ) : products === undefined ? (
8939
+ <p>Loading subscription options...</p>
8940
+ ) : product ? (
8941
+ <CheckoutLink
8942
+ polarApi={api.polar}
8943
+ productIds={[product.id]}
8944
+ embed={false}
8945
+ className={buttonVariants({ variant: "default" })}
8946
+ >
8947
+ Upgrade
8948
+ </CheckoutLink>
8949
+ ) : (
8950
+ <p>No recurring plans available.</p>
8951
+ )}
8952
+ {{/if}}
8734
8953
  <UserMenu />
8735
8954
  </div>
8736
8955
  );
@@ -9134,6 +9353,10 @@ export const Route = createFileRoute("/api/auth/$")({
9134
9353
  ["auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
9135
9354
  import SignUpForm from "@/components/sign-up-form";
9136
9355
  import UserMenu from "@/components/user-menu";
9356
+ {{#if (eq payments "polar")}}
9357
+ import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
9358
+ import { buttonVariants } from "@{{projectName}}/ui/components/button";
9359
+ {{/if}}
9137
9360
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
9138
9361
  import { createFileRoute } from "@tanstack/react-router";
9139
9362
  import {
@@ -9148,18 +9371,58 @@ export const Route = createFileRoute("/dashboard")({
9148
9371
  component: RouteComponent,
9149
9372
  });
9150
9373
 
9374
+ function PrivateDashboardContent() {
9375
+ const privateData = useQuery(api.privateData.get);
9376
+ {{#if (eq payments "polar")}}
9377
+ const products = useQuery(api.polar.listAllProducts);
9378
+ const subscription = useQuery(api.polar.getCurrentSubscription);
9379
+
9380
+ const product = products?.find((product: { isRecurring?: boolean }) => product.isRecurring);
9381
+ const hasActiveSubscription = Boolean(subscription);
9382
+ {{/if}}
9383
+
9384
+ return (
9385
+ <div>
9386
+ <h1>Dashboard</h1>
9387
+ <p>privateData: {privateData?.message}</p>
9388
+ {{#if (eq payments "polar")}}
9389
+ <p>Plan: {hasActiveSubscription ? "Active" : "Free"}</p>
9390
+ {subscription === undefined ? (
9391
+ <p>Loading subscription options...</p>
9392
+ ) : hasActiveSubscription ? (
9393
+ <CustomerPortalLink
9394
+ polarApi={api.polar}
9395
+ className={buttonVariants({ variant: "outline" })}
9396
+ >
9397
+ Manage Subscription
9398
+ </CustomerPortalLink>
9399
+ ) : products === undefined ? (
9400
+ <p>Loading subscription options...</p>
9401
+ ) : product ? (
9402
+ <CheckoutLink
9403
+ polarApi={api.polar}
9404
+ productIds={[product.id]}
9405
+ embed={false}
9406
+ className={buttonVariants({ variant: "default" })}
9407
+ >
9408
+ Upgrade
9409
+ </CheckoutLink>
9410
+ ) : (
9411
+ <p>No recurring plans available.</p>
9412
+ )}
9413
+ {{/if}}
9414
+ <UserMenu />
9415
+ </div>
9416
+ );
9417
+ }
9418
+
9151
9419
  function RouteComponent() {
9152
9420
  const [showSignIn, setShowSignIn] = useState(false);
9153
- const privateData = useQuery(api.privateData.get);
9154
9421
 
9155
9422
  return (
9156
9423
  <>
9157
9424
  <Authenticated>
9158
- <div>
9159
- <h1>Dashboard</h1>
9160
- <p>privateData: {privateData?.message}</p>
9161
- <UserMenu />
9162
- </div>
9425
+ <PrivateDashboardContent />
9163
9426
  </Authenticated>
9164
9427
  <Unauthenticated>
9165
9428
  {showSignIn ? (
@@ -9321,11 +9584,17 @@ export const Route = createFileRoute('/api/auth/$')({
9321
9584
  },
9322
9585
  })
9323
9586
  `],
9324
- ["auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs", `import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
9587
+ ["auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs", `import { Button, Column, Host, Text as ExpoUIText } from "@expo/ui";
9588
+ import { View, ScrollView, StyleSheet{{#if (eq payments "polar")}}, Alert{{/if}} } from "react-native";
9589
+ {{#if (eq payments "polar")}}
9590
+ import * as Linking from "expo-linking";
9591
+ import * as WebBrowser from "expo-web-browser";
9592
+ import { env } from "@{{projectName}}/env/native";
9593
+ {{/if}}
9325
9594
  import { Container } from "@/components/container";
9326
9595
  import { useColorScheme } from "@/lib/use-color-scheme";
9327
9596
  import { NAV_THEME } from "@/lib/constants";
9328
- import { authClient } from "@/lib/auth-client";
9597
+ import { authClient{{#if (eq payments "polar")}}, polarNativeClient{{/if}} } from "@/lib/auth-client";
9329
9598
  import { SignIn } from "@/components/sign-in";
9330
9599
  import { SignUp } from "@/components/sign-up";
9331
9600
  {{#if (eq api "orpc")}}
@@ -9353,70 +9622,158 @@ const isConnected = healthCheck?.data === "OK";
9353
9622
  const isLoading = healthCheck?.isLoading;
9354
9623
  {{/if}}
9355
9624
  const { data: session } = authClient.useSession();
9625
+ {{#if (eq payments "polar")}}
9626
+
9627
+ const openPolarLink = async (url: string, returnUrl: string) => {
9628
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
9629
+ };
9630
+
9631
+ const getPolarReturnUrl = (returnUrl: string) => {
9632
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_SERVER_URL);
9633
+ url.searchParams.set("returnUrl", returnUrl);
9634
+ return url.toString();
9635
+ };
9636
+
9637
+ const handlePolarCheckout = async () => {
9638
+ const returnUrl = Linking.createURL("/");
9639
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
9640
+ const { data, error } = await polarNativeClient.checkout({
9641
+ slug: "pro",
9642
+ redirect: false,
9643
+ successUrl: polarReturnUrl,
9644
+ returnUrl: polarReturnUrl,
9645
+ });
9646
+
9647
+ if (error || !data?.url) {
9648
+ Alert.alert("Checkout unavailable", error?.message ?? "Unable to create a checkout session.");
9649
+ return;
9650
+ }
9651
+
9652
+ await openPolarLink(data.url, returnUrl);
9653
+ };
9654
+
9655
+ const handlePolarPortal = async () => {
9656
+ const returnUrl = Linking.createURL("/");
9657
+ const { data, error } = await polarNativeClient.customer.portal({ redirect: false });
9658
+
9659
+ if (error || !data?.url) {
9660
+ Alert.alert("Portal unavailable", error?.message ?? "Unable to open the customer portal.");
9661
+ return;
9662
+ }
9663
+
9664
+ await openPolarLink(data.url, returnUrl);
9665
+ };
9666
+ {{/if}}
9356
9667
 
9357
9668
  return (
9358
9669
  <Container>
9359
- <ScrollView style={styles.scrollView}>
9670
+ <ScrollView style={styles.scrollView} contentInsetAdjustmentBehavior="never">
9360
9671
  <View style={styles.content}>
9361
- <Text style={[styles.title, { color: theme.text }]}>
9362
- BETTER T STACK
9363
- </Text>
9672
+ <Host style={styles.titleHost}>
9673
+ <ExpoUIText
9674
+ textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold", textAlign: "center" }}
9675
+ >
9676
+ BETTER T STACK
9677
+ </ExpoUIText>
9678
+ </Host>
9364
9679
 
9365
9680
  {session?.user ? (
9366
9681
  <View style={[styles.userCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
9367
- <View style={styles.userHeader}>
9368
- <Text style={[styles.userText, { color: theme.text }]}>
9369
- Welcome, <Text style={styles.userName}>{session.user.name}</Text>
9370
- </Text>
9371
- </View>
9372
- <Text style={[styles.userEmail, { color: theme.text, opacity: 0.7 }]}>
9373
- {session.user.email}
9374
- </Text>
9375
- <TouchableOpacity style={[styles.signOutButton, { backgroundColor: theme.notification }]} onPress={()=> {
9376
- authClient.signOut();
9377
- {{#if (eq api "orpc")}}
9378
- queryClient.invalidateQueries();
9379
- {{/if}}
9380
- {{#if (eq api "trpc")}}
9381
- queryClient.invalidateQueries();
9382
- {{/if}}
9383
- }}
9384
- >
9385
- <Text style={styles.signOutText}>Sign Out</Text>
9386
- </TouchableOpacity>
9682
+ <Host style={styles.userHeader} matchContents=\\{{ vertical: true }}>
9683
+ <Column spacing={8}>
9684
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 16 }}>
9685
+ {\`Welcome, \${session.user.name}\`}
9686
+ </ExpoUIText>
9687
+ <ExpoUIText
9688
+ textStyle=\\{{ color: theme.text, fontSize: 14 }}
9689
+ style=\\{{ opacity: 0.7 }}
9690
+ >
9691
+ {session.user.email}
9692
+ </ExpoUIText>
9693
+ </Column>
9694
+ </Host>
9695
+ <Host matchContents=\\{{ vertical: true }}>
9696
+ <Button
9697
+ label="Sign Out"
9698
+ variant="outlined"
9699
+ onPress={() => {
9700
+ authClient.signOut();
9701
+ {{#if (eq api "orpc")}}
9702
+ queryClient.invalidateQueries();
9703
+ {{/if}}
9704
+ {{#if (eq api "trpc")}}
9705
+ queryClient.invalidateQueries();
9706
+ {{/if}}
9707
+ }}
9708
+ />
9709
+ </Host>
9710
+ {{#if (eq payments "polar")}}
9711
+ <Host style={styles.paymentActions} matchContents=\\{{ vertical: true }}>
9712
+ <Column spacing={8}>
9713
+ <Button label="Upgrade to Pro" onPress={handlePolarCheckout} />
9714
+ <Button
9715
+ label="Manage Subscription"
9716
+ variant="outlined"
9717
+ onPress={handlePolarPortal}
9718
+ />
9719
+ </Column>
9720
+ </Host>
9721
+ {{/if}}
9387
9722
  </View>
9388
9723
  ) : null}
9389
9724
 
9390
9725
  {{#unless (eq api "none")}}
9391
9726
  <View style={[styles.statusCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
9392
- <Text style={[styles.cardTitle, { color: theme.text }]}>
9393
- System Status
9394
- </Text>
9727
+ <Host style={styles.cardTitleHost} matchContents=\\{{ vertical: true }}>
9728
+ <ExpoUIText
9729
+ textStyle=\\{{ color: theme.text, fontSize: 16, fontWeight: "bold" }}
9730
+ >
9731
+ System Status
9732
+ </ExpoUIText>
9733
+ </Host>
9395
9734
  <View style={styles.statusRow}>
9396
9735
  <View style={[styles.statusIndicator, { backgroundColor: isConnected ? "#10b981" : "#ef4444" }]} />
9397
9736
  <View style={styles.statusContent}>
9398
- <Text style={[styles.statusTitle, { color: theme.text }]}>
9399
- {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} Backend
9400
- </Text>
9401
- <Text style={[styles.statusText, { color: theme.text, opacity: 0.7 }]}>
9402
- {isLoading
9403
- ? "Checking connection..."
9404
- : isConnected
9405
- ? "Connected to API"
9406
- : "API Disconnected"}
9407
- </Text>
9737
+ <Host matchContents=\\{{ vertical: true }}>
9738
+ <Column spacing={4}>
9739
+ <ExpoUIText
9740
+ textStyle=\\{{ color: theme.text, fontSize: 14, fontWeight: "bold" }}
9741
+ >
9742
+ {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} Backend
9743
+ </ExpoUIText>
9744
+ <ExpoUIText
9745
+ textStyle=\\{{ color: theme.text, fontSize: 12 }}
9746
+ style=\\{{ opacity: 0.7 }}
9747
+ >
9748
+ {isLoading
9749
+ ? "Checking connection..."
9750
+ : isConnected
9751
+ ? "Connected to API"
9752
+ : "API Disconnected"}
9753
+ </ExpoUIText>
9754
+ </Column>
9755
+ </Host>
9408
9756
  </View>
9409
9757
  </View>
9410
9758
  </View>
9411
9759
 
9412
9760
  <View style={[styles.privateDataCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
9413
- <Text style={[styles.cardTitle, { color: theme.text }]}>
9414
- Private Data
9415
- </Text>
9761
+ <Host style={styles.cardTitleHost} matchContents=\\{{ vertical: true }}>
9762
+ <ExpoUIText
9763
+ textStyle=\\{{ color: theme.text, fontSize: 16, fontWeight: "bold" }}
9764
+ >
9765
+ Private Data
9766
+ </ExpoUIText>
9767
+ </Host>
9416
9768
  {privateData && (
9417
- <Text style={[styles.privateDataText, { color: theme.text, opacity: 0.7 }]}>
9418
- {privateData.data?.message}
9419
- </Text>
9769
+ <Host matchContents=\\{{ vertical: true }}>
9770
+ <ExpoUIText
9771
+ textStyle=\\{{ color: theme.text, fontSize: 14 }}
9772
+ style=\\{{ opacity: 0.7 }}
9773
+ >
9774
+ {privateData.data?.message ?? ""}
9775
+ </ExpoUIText>
9776
+ </Host>
9420
9777
  )}
9421
9778
  </View>
9422
9779
  {{/unless}}
@@ -9438,45 +9795,34 @@ scrollView: {
9438
9795
  flex: 1,
9439
9796
  },
9440
9797
  content: {
9441
- padding: 16,
9798
+ paddingHorizontal: 20,
9799
+ paddingTop: 28,
9800
+ paddingBottom: 32,
9442
9801
  },
9443
- title: {
9444
- fontSize: 24,
9445
- fontWeight: "bold",
9446
- marginBottom: 16,
9802
+ titleHost: {
9803
+ alignSelf: "stretch",
9804
+ height: 34,
9805
+ marginBottom: 24,
9447
9806
  },
9448
9807
  userCard: {
9449
9808
  marginBottom: 16,
9450
9809
  padding: 16,
9451
9810
  borderWidth: 1,
9811
+ borderRadius: 16,
9452
9812
  },
9453
9813
  userHeader: {
9454
9814
  marginBottom: 8,
9455
9815
  },
9456
- userText: {
9457
- fontSize: 16,
9458
- },
9459
- userName: {
9460
- fontWeight: "bold",
9461
- },
9462
- userEmail: {
9463
- fontSize: 14,
9464
- marginBottom: 12,
9465
- },
9466
- signOutButton: {
9467
- padding: 12,
9468
- },
9469
- signOutText: {
9470
- color: "#ffffff",
9816
+ paymentActions: {
9817
+ marginTop: 12,
9471
9818
  },
9472
9819
  statusCard: {
9473
9820
  marginBottom: 16,
9474
9821
  padding: 16,
9475
9822
  borderWidth: 1,
9823
+ borderRadius: 16,
9476
9824
  },
9477
- cardTitle: {
9478
- fontSize: 16,
9479
- fontWeight: "bold",
9825
+ cardTitleHost: {
9480
9826
  marginBottom: 12,
9481
9827
  },
9482
9828
  statusRow: {
@@ -9491,22 +9837,14 @@ width: 8,
9491
9837
  statusContent: {
9492
9838
  flex: 1,
9493
9839
  },
9494
- statusTitle: {
9495
- fontSize: 14,
9496
- fontWeight: "bold",
9497
- },
9498
- statusText: {
9499
- fontSize: 12,
9500
- },
9501
9840
  privateDataCard: {
9502
9841
  marginBottom: 16,
9503
9842
  padding: 16,
9504
9843
  borderWidth: 1,
9844
+ borderRadius: 16,
9505
9845
  },
9506
- privateDataText: {
9507
- fontSize: 14,
9508
- },
9509
- });`],
9846
+ });
9847
+ `],
9510
9848
  ["auth/better-auth/native/bare/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
9511
9849
  {{#if (eq api "trpc")}}
9512
9850
  import { queryClient } from "@/utils/trpc";
@@ -10025,9 +10363,41 @@ export const authClient = createAuthClient({
10025
10363
  }),
10026
10364
  ],
10027
10365
  });
10366
+ {{#if (eq payments "polar")}}
10367
+
10368
+ type PolarLinkResponse = {
10369
+ url: string;
10370
+ redirect: boolean;
10371
+ };
10372
+
10373
+ type PolarClientResponse<T> = Promise<{
10374
+ data: T | null;
10375
+ error: { message?: string } | null;
10376
+ }>;
10377
+
10378
+ type PolarNativeClient = typeof authClient & {
10379
+ checkout: (data: {
10380
+ slug?: string;
10381
+ products?: string[] | string;
10382
+ redirect?: boolean;
10383
+ successUrl?: string;
10384
+ returnUrl?: string;
10385
+ }) => PolarClientResponse<PolarLinkResponse>;
10386
+ customer: {
10387
+ portal: (data?: { redirect?: boolean }) => PolarClientResponse<PolarLinkResponse>;
10388
+ };
10389
+ };
10390
+
10391
+ export const polarNativeClient = authClient as PolarNativeClient;
10392
+ {{/if}}
10028
10393
  `],
10029
- ["auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs", `import { authClient } from "@/lib/auth-client";
10030
- import { ScrollView, Text, TouchableOpacity, View } from "react-native";
10394
+ ["auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs", `import { authClient{{#if (eq payments "polar")}}, polarNativeClient{{/if}} } from "@/lib/auth-client";
10395
+ import { ScrollView, Text, TouchableOpacity, View{{#if (eq payments "polar")}}, Alert{{/if}} } from "react-native";
10396
+ {{#if (eq payments "polar")}}
10397
+ import * as Linking from "expo-linking";
10398
+ import * as WebBrowser from "expo-web-browser";
10399
+ import { env } from "@{{projectName}}/env/native";
10400
+ {{/if}}
10031
10401
  import { StyleSheet } from "react-native-unistyles";
10032
10402
 
10033
10403
  import { Container } from "@/components/container";
@@ -10052,6 +10422,48 @@ export default function Home() {
10052
10422
  const privateData = useQuery(trpc.privateData.queryOptions());
10053
10423
  {{/if}}
10054
10424
  const { data: session } = authClient.useSession();
10425
+ {{#if (eq payments "polar")}}
10426
+
10427
+ const openPolarLink = async (url: string, returnUrl: string) => {
10428
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
10429
+ };
10430
+
10431
+ const getPolarReturnUrl = (returnUrl: string) => {
10432
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_SERVER_URL);
10433
+ url.searchParams.set("returnUrl", returnUrl);
10434
+ return url.toString();
10435
+ };
10436
+
10437
+ const handlePolarCheckout = async () => {
10438
+ const returnUrl = Linking.createURL("/");
10439
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
10440
+ const { data, error } = await polarNativeClient.checkout({
10441
+ slug: "pro",
10442
+ redirect: false,
10443
+ successUrl: polarReturnUrl,
10444
+ returnUrl: polarReturnUrl,
10445
+ });
10446
+
10447
+ if (error || !data?.url) {
10448
+ Alert.alert("Checkout unavailable", error?.message ?? "Unable to create a checkout session.");
10449
+ return;
10450
+ }
10451
+
10452
+ await openPolarLink(data.url, returnUrl);
10453
+ };
10454
+
10455
+ const handlePolarPortal = async () => {
10456
+ const returnUrl = Linking.createURL("/");
10457
+ const { data, error } = await polarNativeClient.customer.portal({ redirect: false });
10458
+
10459
+ if (error || !data?.url) {
10460
+ Alert.alert("Portal unavailable", error?.message ?? "Unable to open the customer portal.");
10461
+ return;
10462
+ }
10463
+
10464
+ await openPolarLink(data.url, returnUrl);
10465
+ };
10466
+ {{/if}}
10055
10467
 
10056
10468
  return (
10057
10469
  <Container>
@@ -10082,6 +10494,22 @@ export default function Home() {
10082
10494
  >
10083
10495
  <Text style={styles.signOutButtonText}>Sign Out</Text>
10084
10496
  </TouchableOpacity>
10497
+ {{#if (eq payments "polar")}}
10498
+ <View style={styles.paymentActions}>
10499
+ <TouchableOpacity
10500
+ style={styles.polarPrimaryButton}
10501
+ onPress={handlePolarCheckout}
10502
+ >
10503
+ <Text style={styles.polarPrimaryButtonText}>Upgrade to Pro</Text>
10504
+ </TouchableOpacity>
10505
+ <TouchableOpacity
10506
+ style={styles.polarSecondaryButton}
10507
+ onPress={handlePolarPortal}
10508
+ >
10509
+ <Text style={styles.polarSecondaryButtonText}>Manage Subscription</Text>
10510
+ </TouchableOpacity>
10511
+ </View>
10512
+ {{/if}}
10085
10513
  </View>
10086
10514
  ) : null}
10087
10515
  {{#unless (eq api "none")}}
@@ -10174,6 +10602,32 @@ const styles = StyleSheet.create((theme) => ({
10174
10602
  signOutButtonText: {
10175
10603
  fontWeight: "500",
10176
10604
  },
10605
+ paymentActions: {
10606
+ marginTop: 12,
10607
+ gap: 8,
10608
+ alignItems: "flex-start",
10609
+ },
10610
+ polarPrimaryButton: {
10611
+ backgroundColor: theme?.colors?.primary,
10612
+ paddingVertical: 8,
10613
+ paddingHorizontal: 16,
10614
+ borderRadius: 6,
10615
+ },
10616
+ polarPrimaryButtonText: {
10617
+ color: theme?.colors?.primaryForeground,
10618
+ fontWeight: "500",
10619
+ },
10620
+ polarSecondaryButton: {
10621
+ borderWidth: 1,
10622
+ borderColor: theme?.colors?.border,
10623
+ paddingVertical: 8,
10624
+ paddingHorizontal: 16,
10625
+ borderRadius: 6,
10626
+ },
10627
+ polarSecondaryButtonText: {
10628
+ color: theme?.colors?.typography,
10629
+ fontWeight: "500",
10630
+ },
10177
10631
  apiStatusCard: {
10178
10632
  marginBottom: 24,
10179
10633
  borderRadius: 8,
@@ -10678,9 +11132,14 @@ const styles = StyleSheet.create((theme) => ({
10678
11132
  },
10679
11133
  }));
10680
11134
  `],
10681
- ["auth/better-auth/native/uniwind/app/(drawer)/index.tsx.hbs", `import { Text, View, Pressable } from "react-native";
11135
+ ["auth/better-auth/native/uniwind/app/(drawer)/index.tsx.hbs", `import { Text, View, Pressable{{#if (eq payments "polar")}}, Alert{{/if}} } from "react-native";
11136
+ {{#if (eq payments "polar")}}
11137
+ import * as Linking from "expo-linking";
11138
+ import * as WebBrowser from "expo-web-browser";
11139
+ import { env } from "@{{projectName}}/env/native";
11140
+ {{/if}}
10682
11141
  import { Container } from "@/components/container";
10683
- import { authClient } from "@/lib/auth-client";
11142
+ import { authClient{{#if (eq payments "polar")}}, polarNativeClient{{/if}} } from "@/lib/auth-client";
10684
11143
  import { Ionicons } from "@expo/vector-icons";
10685
11144
  import { Card, Chip, useThemeColor } from "heroui-native";
10686
11145
  import { SignIn } from "@/components/sign-in";
@@ -10708,6 +11167,48 @@ const isConnected = healthCheck?.data === "OK";
10708
11167
  const isLoading = healthCheck?.isLoading;
10709
11168
  {{/if}}
10710
11169
  const { data: session } = authClient.useSession();
11170
+ {{#if (eq payments "polar")}}
11171
+
11172
+ const openPolarLink = async (url: string, returnUrl: string) => {
11173
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
11174
+ };
11175
+
11176
+ const getPolarReturnUrl = (returnUrl: string) => {
11177
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_SERVER_URL);
11178
+ url.searchParams.set("returnUrl", returnUrl);
11179
+ return url.toString();
11180
+ };
11181
+
11182
+ const handlePolarCheckout = async () => {
11183
+ const returnUrl = Linking.createURL("/");
11184
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
11185
+ const { data, error } = await polarNativeClient.checkout({
11186
+ slug: "pro",
11187
+ redirect: false,
11188
+ successUrl: polarReturnUrl,
11189
+ returnUrl: polarReturnUrl,
11190
+ });
11191
+
11192
+ if (error || !data?.url) {
11193
+ Alert.alert("Checkout unavailable", error?.message ?? "Unable to create a checkout session.");
11194
+ return;
11195
+ }
11196
+
11197
+ await openPolarLink(data.url, returnUrl);
11198
+ };
11199
+
11200
+ const handlePolarPortal = async () => {
11201
+ const returnUrl = Linking.createURL("/");
11202
+ const { data, error } = await polarNativeClient.customer.portal({ redirect: false });
11203
+
11204
+ if (error || !data?.url) {
11205
+ Alert.alert("Portal unavailable", error?.message ?? "Unable to open the customer portal.");
11206
+ return;
11207
+ }
11208
+
11209
+ await openPolarLink(data.url, returnUrl);
11210
+ };
11211
+ {{/if}}
10711
11212
 
10712
11213
  const mutedColor = useThemeColor("muted");
10713
11214
  const successColor = useThemeColor("success");
@@ -10742,6 +11243,22 @@ return (
10742
11243
  >
10743
11244
  <Text className="text-foreground font-medium">Sign Out</Text>
10744
11245
  </Pressable>
11246
+ {{#if (eq payments "polar")}}
11247
+ <View className="mt-4 gap-3">
11248
+ <Pressable
11249
+ className="bg-primary py-3 px-4 rounded-lg self-start active:opacity-70"
11250
+ onPress={handlePolarCheckout}
11251
+ >
11252
+ <Text className="text-foreground font-medium">Upgrade to Pro</Text>
11253
+ </Pressable>
11254
+ <Pressable
11255
+ className="border border-border py-3 px-4 rounded-lg self-start active:opacity-70"
11256
+ onPress={handlePolarPortal}
11257
+ >
11258
+ <Text className="text-foreground font-medium">Manage Subscription</Text>
11259
+ </Pressable>
11260
+ </View>
11261
+ {{/if}}
10745
11262
  </Card>
10746
11263
  ) : null}
10747
11264
 
@@ -10800,7 +11317,8 @@ return (
10800
11317
  )}
10801
11318
  </Container>
10802
11319
  );
10803
- }`],
11320
+ }
11321
+ `],
10804
11322
  ["auth/better-auth/native/uniwind/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
10805
11323
  {{#if (eq api "trpc")}}
10806
11324
  import { queryClient } from "@/utils/trpc";
@@ -11259,14 +11777,8 @@ export function createAuth({{#if (and (eq backend "self") (eq webDeploy "cloudfl
11259
11777
  env.CORS_ORIGIN,
11260
11778
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
11261
11779
  "{{projectName}}://",
11262
- ...(env.NODE_ENV === "development"
11263
- ? [
11264
- "exp://",
11265
- "exp://**",
11266
- "exp://192.168.*.*:*/**",
11267
- "http://localhost:8081",
11268
- ]
11269
- : []),
11780
+ "exp://",
11781
+ "http://localhost:8081",
11270
11782
  {{/if}}
11271
11783
  ],
11272
11784
  emailAndPassword: {
@@ -11348,14 +11860,8 @@ export function createAuth({{#if (and (eq backend "self") (eq webDeploy "cloudfl
11348
11860
  env.CORS_ORIGIN,
11349
11861
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
11350
11862
  "{{projectName}}://",
11351
- ...(env.NODE_ENV === "development"
11352
- ? [
11353
- "exp://",
11354
- "exp://**",
11355
- "exp://192.168.*.*:*/**",
11356
- "http://localhost:8081",
11357
- ]
11358
- : []),
11863
+ "exp://",
11864
+ "http://localhost:8081",
11359
11865
  {{/if}}
11360
11866
  ],
11361
11867
  emailAndPassword: {
@@ -11428,14 +11934,8 @@ export function createAuth() {
11428
11934
  env.CORS_ORIGIN,
11429
11935
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
11430
11936
  "{{projectName}}://",
11431
- ...(env.NODE_ENV === "development"
11432
- ? [
11433
- "exp://",
11434
- "exp://**",
11435
- "exp://192.168.*.*:*/**",
11436
- "http://localhost:8081",
11437
- ]
11438
- : []),
11937
+ "exp://",
11938
+ "http://localhost:8081",
11439
11939
  {{/if}}
11440
11940
  ],
11441
11941
  emailAndPassword: {
@@ -11515,14 +12015,8 @@ export function createAuth({{#if (and (eq backend "self") (eq webDeploy "cloudfl
11515
12015
  env.CORS_ORIGIN,
11516
12016
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
11517
12017
  "{{projectName}}://",
11518
- ...(env.NODE_ENV === "development"
11519
- ? [
11520
- "exp://",
11521
- "exp://**",
11522
- "exp://192.168.*.*:*/**",
11523
- "http://localhost:8081",
11524
- ]
11525
- : []),
12018
+ "exp://",
12019
+ "http://localhost:8081",
11526
12020
  {{/if}}
11527
12021
  ],
11528
12022
  emailAndPassword: {
@@ -11593,14 +12087,8 @@ export function createAuth({{#if (and (eq backend "self") (eq webDeploy "cloudfl
11593
12087
  env.CORS_ORIGIN,
11594
12088
  {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
11595
12089
  "{{projectName}}://",
11596
- ...(env.NODE_ENV === "development"
11597
- ? [
11598
- "exp://",
11599
- "exp://**",
11600
- "exp://192.168.*.*:*/**",
11601
- "http://localhost:8081",
11602
- ]
11603
- : []),
12090
+ "exp://",
12091
+ "http://localhost:8081",
11604
12092
  {{/if}}
11605
12093
  ],
11606
12094
  emailAndPassword: {
@@ -11963,40 +12451,42 @@ export const accountRelations = relations(account, ({ one }) => ({
11963
12451
  ["auth/better-auth/server/db/mongoose/mongodb/src/models/auth.model.ts.hbs", `import mongoose from 'mongoose';
11964
12452
 
11965
12453
  const { Schema, model } = mongoose;
12454
+ const { ObjectId } = Schema.Types;
11966
12455
 
11967
12456
  const userSchema = new Schema(
11968
12457
  {
11969
- _id: { type: String },
12458
+ _id: { type: ObjectId, auto: true },
11970
12459
  name: { type: String, required: true },
11971
12460
  email: { type: String, required: true, unique: true },
11972
- emailVerified: { type: Boolean, required: true },
12461
+ emailVerified: { type: Boolean, required: true, default: false },
11973
12462
  image: { type: String },
11974
- createdAt: { type: Date, required: true },
11975
- updatedAt: { type: Date, required: true },
12463
+ createdAt: { type: Date, required: true, default: Date.now },
12464
+ updatedAt: { type: Date, required: true, default: Date.now },
11976
12465
  },
11977
12466
  { collection: 'user' }
11978
12467
  );
11979
12468
 
11980
12469
  const sessionSchema = new Schema(
11981
12470
  {
11982
- _id: { type: String },
12471
+ _id: { type: ObjectId, auto: true },
11983
12472
  expiresAt: { type: Date, required: true },
11984
12473
  token: { type: String, required: true, unique: true },
11985
- createdAt: { type: Date, required: true },
11986
- updatedAt: { type: Date, required: true },
12474
+ createdAt: { type: Date, required: true, default: Date.now },
12475
+ updatedAt: { type: Date, required: true, default: Date.now },
11987
12476
  ipAddress: { type: String },
11988
12477
  userAgent: { type: String },
11989
- userId: { type: String, ref: 'User', required: true },
12478
+ userId: { type: ObjectId, ref: 'User', required: true },
11990
12479
  },
11991
12480
  { collection: 'session' }
11992
12481
  );
12482
+ sessionSchema.index({ userId: 1 });
11993
12483
 
11994
12484
  const accountSchema = new Schema(
11995
12485
  {
11996
- _id: { type: String },
12486
+ _id: { type: ObjectId, auto: true },
11997
12487
  accountId: { type: String, required: true },
11998
12488
  providerId: { type: String, required: true },
11999
- userId: { type: String, ref: 'User', required: true },
12489
+ userId: { type: ObjectId, ref: 'User', required: true },
12000
12490
  accessToken: { type: String },
12001
12491
  refreshToken: { type: String },
12002
12492
  idToken: { type: String },
@@ -12004,23 +12494,25 @@ const accountSchema = new Schema(
12004
12494
  refreshTokenExpiresAt: { type: Date },
12005
12495
  scope: { type: String },
12006
12496
  password: { type: String },
12007
- createdAt: { type: Date, required: true },
12008
- updatedAt: { type: Date, required: true },
12497
+ createdAt: { type: Date, required: true, default: Date.now },
12498
+ updatedAt: { type: Date, required: true, default: Date.now },
12009
12499
  },
12010
12500
  { collection: 'account' }
12011
12501
  );
12502
+ accountSchema.index({ userId: 1 });
12012
12503
 
12013
12504
  const verificationSchema = new Schema(
12014
12505
  {
12015
- _id: { type: String },
12506
+ _id: { type: ObjectId, auto: true },
12016
12507
  identifier: { type: String, required: true },
12017
12508
  value: { type: String, required: true },
12018
12509
  expiresAt: { type: Date, required: true },
12019
- createdAt: { type: Date },
12020
- updatedAt: { type: Date },
12510
+ createdAt: { type: Date, required: true, default: Date.now },
12511
+ updatedAt: { type: Date, required: true, default: Date.now },
12021
12512
  },
12022
12513
  { collection: 'verification' }
12023
12514
  );
12515
+ verificationSchema.index({ identifier: 1 });
12024
12516
 
12025
12517
  const User = model('User', userSchema);
12026
12518
  const Session = model('Session', sessionSchema);
@@ -12483,7 +12975,7 @@ import { authClient } from "../lib/auth-client";
12483
12975
  `],
12484
12976
  ["auth/better-auth/web/astro/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/client";
12485
12977
  {{#if (eq payments "polar")}}
12486
- import { polarClient } from "@polar-sh/better-auth";
12978
+ import { polarClient } from "@polar-sh/better-auth/client";
12487
12979
  {{/if}}
12488
12980
  {{#if (ne backend "self")}}
12489
12981
  import { PUBLIC_SERVER_URL } from "astro:env/client";
@@ -13019,7 +13511,7 @@ watchEffect(() => {
13019
13511
  `],
13020
13512
  ["auth/better-auth/web/nuxt/app/plugins/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/vue";
13021
13513
  {{#if (eq payments "polar")}}
13022
- import { polarClient } from "@polar-sh/better-auth";
13514
+ import { polarClient } from "@polar-sh/better-auth/client";
13023
13515
  {{/if}}
13024
13516
 
13025
13517
  export default defineNuxtPlugin(() => {
@@ -13045,7 +13537,7 @@ export default defineNuxtPlugin(() => {
13045
13537
  `],
13046
13538
  ["auth/better-auth/web/react/base/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/react";
13047
13539
  {{#if (eq payments "polar")}}
13048
- import { polarClient } from "@polar-sh/better-auth";
13540
+ import { polarClient } from "@polar-sh/better-auth/client";
13049
13541
  {{/if}}
13050
13542
  {{#unless (eq backend "self")}}
13051
13543
  import { env } from "@{{projectName}}/env/web";
@@ -15291,7 +15783,7 @@ export default function UserMenu() {
15291
15783
  `],
15292
15784
  ["auth/better-auth/web/solid/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/solid";
15293
15785
  {{#if (eq payments "polar")}}
15294
- import { polarClient } from "@polar-sh/better-auth";
15786
+ import { polarClient } from "@polar-sh/better-auth/client";
15295
15787
  {{/if}}
15296
15788
  import { env } from "@{{projectName}}/env/web";
15297
15789
 
@@ -15709,7 +16201,7 @@ import { PUBLIC_SERVER_URL } from "$env/static/public";
15709
16201
  {{/unless}}
15710
16202
  import { createAuthClient } from "better-auth/svelte";
15711
16203
  {{#if (eq payments "polar")}}
15712
- import { polarClient } from "@polar-sh/better-auth";
16204
+ import { polarClient } from "@polar-sh/better-auth/client";
15713
16205
  {{/if}}
15714
16206
 
15715
16207
  export const authClient = createAuthClient({
@@ -17295,11 +17787,90 @@ export const startInstance = createStart(() => {
17295
17787
  `],
17296
17788
  ["backend/convex/packages/backend/_gitignore", `
17297
17789
  .env.local
17790
+ `],
17791
+ ["backend/convex/packages/backend/convex/_generated/api.d.ts", `/* eslint-disable */
17792
+ export declare const api: any;
17793
+ export declare const internal: any;
17794
+ export declare const components: any;
17795
+ `],
17796
+ ["backend/convex/packages/backend/convex/_generated/api.js", `/* eslint-disable */
17797
+ import { anyApi, componentsGeneric } from "convex/server";
17798
+
17799
+ export const api = anyApi;
17800
+ export const internal = anyApi;
17801
+ export const components = componentsGeneric();
17802
+ `],
17803
+ ["backend/convex/packages/backend/convex/_generated/dataModel.d.ts", `/* eslint-disable */
17804
+ import type {
17805
+ DataModelFromSchemaDefinition,
17806
+ DocumentByName,
17807
+ SystemTableNames,
17808
+ TableNamesInDataModel,
17809
+ } from "convex/server";
17810
+ import type { GenericId } from "convex/values";
17811
+
17812
+ import schema from "../schema.js";
17813
+
17814
+ export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
17815
+ export type TableNames = TableNamesInDataModel<DataModel>;
17816
+ export type Doc<TableName extends TableNames> = DocumentByName<DataModel, TableName>;
17817
+ export type Id<TableName extends TableNames | SystemTableNames> = GenericId<TableName>;
17818
+ `],
17819
+ ["backend/convex/packages/backend/convex/_generated/server.d.ts", `/* eslint-disable */
17820
+ import type {
17821
+ ActionBuilder,
17822
+ GenericActionCtx,
17823
+ GenericDatabaseReader,
17824
+ GenericDatabaseWriter,
17825
+ GenericMutationCtx,
17826
+ GenericQueryCtx,
17827
+ HttpActionBuilder,
17828
+ MutationBuilder,
17829
+ QueryBuilder,
17830
+ } from "convex/server";
17831
+
17832
+ import type { DataModel } from "./dataModel.js";
17833
+
17834
+ export declare const query: QueryBuilder<DataModel, "public">;
17835
+ export declare const internalQuery: QueryBuilder<DataModel, "internal">;
17836
+ export declare const mutation: MutationBuilder<DataModel, "public">;
17837
+ export declare const internalMutation: MutationBuilder<DataModel, "internal">;
17838
+ export declare const action: ActionBuilder<DataModel, "public">;
17839
+ export declare const internalAction: ActionBuilder<DataModel, "internal">;
17840
+ export declare const httpAction: HttpActionBuilder;
17841
+
17842
+ export type QueryCtx = GenericQueryCtx<DataModel>;
17843
+ export type MutationCtx = GenericMutationCtx<DataModel>;
17844
+ export type ActionCtx = GenericActionCtx<DataModel>;
17845
+ export type DatabaseReader = GenericDatabaseReader<DataModel>;
17846
+ export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
17847
+ `],
17848
+ ["backend/convex/packages/backend/convex/_generated/server.js", `/* eslint-disable */
17849
+ import {
17850
+ actionGeneric,
17851
+ httpActionGeneric,
17852
+ internalActionGeneric,
17853
+ internalMutationGeneric,
17854
+ internalQueryGeneric,
17855
+ mutationGeneric,
17856
+ queryGeneric,
17857
+ } from "convex/server";
17858
+
17859
+ export const query = queryGeneric;
17860
+ export const internalQuery = internalQueryGeneric;
17861
+ export const mutation = mutationGeneric;
17862
+ export const internalMutation = internalMutationGeneric;
17863
+ export const action = actionGeneric;
17864
+ export const internalAction = internalActionGeneric;
17865
+ export const httpAction = httpActionGeneric;
17298
17866
  `],
17299
17867
  ["backend/convex/packages/backend/convex/convex.config.ts.hbs", `import { defineApp } from "convex/server";
17300
17868
  {{#if (eq auth "better-auth")}}
17301
17869
  import betterAuth from "@convex-dev/better-auth/convex.config";
17302
17870
  {{/if}}
17871
+ {{#if (eq payments "polar")}}
17872
+ import polar from "@convex-dev/polar/convex.config.js";
17873
+ {{/if}}
17303
17874
  {{#if (includes examples "ai")}}
17304
17875
  import agent from "@convex-dev/agent/convex.config";
17305
17876
  {{/if}}
@@ -17308,6 +17879,9 @@ const app = defineApp();
17308
17879
  {{#if (eq auth "better-auth")}}
17309
17880
  app.use(betterAuth);
17310
17881
  {{/if}}
17882
+ {{#if (eq payments "polar")}}
17883
+ app.use(polar);
17884
+ {{/if}}
17311
17885
  {{#if (includes examples "ai")}}
17312
17886
  app.use(agent);
17313
17887
  {{/if}}
@@ -17436,6 +18010,7 @@ export default defineSchema({
17436
18010
  "jsx": "react-jsx",
17437
18011
  "skipLibCheck": true,
17438
18012
  "allowSyntheticDefaultImports": true,
18013
+ "types": ["node"],
17439
18014
 
17440
18015
  /* These compiler options are required by Convex */
17441
18016
  "target": "ESNext",
@@ -17630,6 +18205,32 @@ new Elysia()
17630
18205
  {{/if}}
17631
18206
  }),
17632
18207
  )
18208
+ {{#if (and (eq auth "better-auth") (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
18209
+ .get("/polar/success", ({ request, status }) => {
18210
+ const nativeAppUrl = "{{projectName}}://";
18211
+ const allowedNativeProtocols = new Set(["exp:", new URL(nativeAppUrl).protocol]);
18212
+ const requestUrl = new URL(request.url);
18213
+ const returnUrl = requestUrl.searchParams.get("returnUrl") || nativeAppUrl;
18214
+
18215
+ let redirectUrl: URL;
18216
+ try {
18217
+ redirectUrl = new URL(returnUrl);
18218
+ } catch {
18219
+ return status(400, "Invalid return URL");
18220
+ }
18221
+
18222
+ if (!allowedNativeProtocols.has(redirectUrl.protocol)) {
18223
+ return status(400, "Invalid return URL");
18224
+ }
18225
+
18226
+ return new Response(null, {
18227
+ status: 302,
18228
+ headers: {
18229
+ Location: redirectUrl.toString(),
18230
+ },
18231
+ });
18232
+ })
18233
+ {{/if}}
17633
18234
  {{#if (eq auth "better-auth")}}
17634
18235
  .all("/api/auth/*", async (context) => {
17635
18236
  const { request, status } = context;
@@ -17712,10 +18313,8 @@ import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
17712
18313
  import { RPCHandler } from "@orpc/server/node";
17713
18314
  import { onError } from "@orpc/server";
17714
18315
  import { appRouter } from "@{{projectName}}/api/routers/index";
17715
- {{#if (or (eq auth "better-auth") (eq auth "clerk"))}}
17716
18316
  import { createContext } from "@{{projectName}}/api/context";
17717
18317
  {{/if}}
17718
- {{/if}}
17719
18318
  import cors from "cors";
17720
18319
  import express from "express";
17721
18320
  {{#if (includes examples "ai")}}
@@ -17754,6 +18353,31 @@ app.use(clerkMiddleware());
17754
18353
  app.all("/api/auth{/*path}", toNodeHandler(auth));
17755
18354
  {{/if}}
17756
18355
 
18356
+ {{#if (and (eq auth "better-auth") (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
18357
+ const nativeAppUrl = "{{projectName}}://";
18358
+ const allowedNativeProtocols = new Set(["exp:", new URL(nativeAppUrl).protocol]);
18359
+
18360
+ app.get("/polar/success", (req, res) => {
18361
+ const requestUrl = new URL(req.url, env.BETTER_AUTH_URL);
18362
+ const returnUrl = requestUrl.searchParams.get("returnUrl") || nativeAppUrl;
18363
+
18364
+ let redirectUrl: URL;
18365
+ try {
18366
+ redirectUrl = new URL(returnUrl);
18367
+ } catch {
18368
+ res.status(400).send("Invalid return URL");
18369
+ return;
18370
+ }
18371
+
18372
+ if (!allowedNativeProtocols.has(redirectUrl.protocol)) {
18373
+ res.status(400).send("Invalid return URL");
18374
+ return;
18375
+ }
18376
+
18377
+ res.redirect(302, redirectUrl.toString());
18378
+ });
18379
+
18380
+ {{/if}}
17757
18381
  {{#if (eq api "trpc")}}
17758
18382
  app.use(
17759
18383
  "/trpc",
@@ -17788,21 +18412,13 @@ const apiHandler = new OpenAPIHandler(appRouter, {
17788
18412
  app.use(async (req, res, next) => {
17789
18413
  const rpcResult = await rpcHandler.handle(req, res, {
17790
18414
  prefix: "/rpc",
17791
- {{#if (or (eq auth "better-auth") (eq auth "clerk"))}}
17792
18415
  context: await createContext({ req }),
17793
- {{else}}
17794
- context: {},
17795
- {{/if}}
17796
18416
  });
17797
18417
  if (rpcResult.matched) return;
17798
18418
 
17799
18419
  const apiResult = await apiHandler.handle(req, res, {
17800
18420
  prefix: "/api-reference",
17801
- {{#if (or (eq auth "better-auth") (eq auth "clerk"))}}
17802
18421
  context: await createContext({ req }),
17803
- {{else}}
17804
- context: {},
17805
- {{/if}}
17806
18422
  });
17807
18423
  if (apiResult.matched) return;
17808
18424
 
@@ -17913,9 +18529,37 @@ const fastify = Fastify({
17913
18529
 
17914
18530
  fastify.register(fastifyCors, baseCorsConfig);
17915
18531
  {{#if (eq auth "clerk")}}
17916
- fastify.register(clerkPlugin);
18532
+ fastify.register(clerkPlugin, {
18533
+ publishableKey: env.CLERK_PUBLISHABLE_KEY,
18534
+ secretKey: env.CLERK_SECRET_KEY,
18535
+ });
17917
18536
  {{/if}}
17918
18537
 
18538
+ {{#if (and (eq auth "better-auth") (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
18539
+ const nativeAppUrl = "{{projectName}}://";
18540
+ const allowedNativeProtocols = new Set(["exp:", new URL(nativeAppUrl).protocol]);
18541
+
18542
+ fastify.get("/polar/success", async (request, reply) => {
18543
+ const requestUrl = new URL(request.url, env.BETTER_AUTH_URL);
18544
+ const returnUrl = requestUrl.searchParams.get("returnUrl") || nativeAppUrl;
18545
+
18546
+ let redirectUrl: URL;
18547
+ try {
18548
+ redirectUrl = new URL(returnUrl);
18549
+ } catch {
18550
+ reply.status(400).send("Invalid return URL");
18551
+ return;
18552
+ }
18553
+
18554
+ if (!allowedNativeProtocols.has(redirectUrl.protocol)) {
18555
+ reply.status(400).send("Invalid return URL");
18556
+ return;
18557
+ }
18558
+
18559
+ reply.status(302).header("Location", redirectUrl.toString()).send();
18560
+ });
18561
+
18562
+ {{/if}}
17919
18563
  {{#if (eq api "orpc")}}
17920
18564
  fastify.register(async (rpcApp) => {
17921
18565
  // Fully utilize oRPC features by letting oRPC parse the request body.
@@ -18090,6 +18734,29 @@ app.on(
18090
18734
  );
18091
18735
  {{/if}}
18092
18736
 
18737
+ {{#if (and (eq auth "better-auth") (eq payments "polar") (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")))}}
18738
+ const nativeAppUrl = "{{projectName}}://";
18739
+ const allowedNativeProtocols = new Set(["exp:", new URL(nativeAppUrl).protocol]);
18740
+
18741
+ app.get("/polar/success", (c) => {
18742
+ const requestUrl = new URL(c.req.url);
18743
+ const returnUrl = requestUrl.searchParams.get("returnUrl") || nativeAppUrl;
18744
+
18745
+ let redirectUrl: URL;
18746
+ try {
18747
+ redirectUrl = new URL(returnUrl);
18748
+ } catch {
18749
+ return c.text("Invalid return URL", 400);
18750
+ }
18751
+
18752
+ if (!allowedNativeProtocols.has(redirectUrl.protocol)) {
18753
+ return c.text("Invalid return URL", 400);
18754
+ }
18755
+
18756
+ return c.redirect(redirectUrl.toString(), 302);
18757
+ });
18758
+
18759
+ {{/if}}
18093
18760
  {{#if (eq api "orpc")}}
18094
18761
  export const apiHandler = new OpenAPIHandler(appRouter, {
18095
18762
  plugins: [
@@ -18678,14 +19345,13 @@ export function createDb() {
18678
19345
  }
18679
19346
  {{/if}}
18680
19347
  `],
19348
+ ["db/drizzle/sqlite/src/migrations/.gitkeep", ``],
18681
19349
  ["db/mongoose/mongodb/src/index.ts.hbs", `import mongoose from "mongoose";
18682
19350
  import { env } from "@{{projectName}}/env/server";
18683
19351
 
18684
- await mongoose.connect(env.DATABASE_URL).catch((error) => {
18685
- console.log("Error connecting to database:", error);
18686
- });
19352
+ await mongoose.connect(env.DATABASE_URL);
18687
19353
 
18688
- const client = mongoose.connection.getClient().db("myDB");
19354
+ const client = mongoose.connection.getClient().db();
18689
19355
 
18690
19356
  export { client };
18691
19357
  `],
@@ -19007,6 +19673,7 @@ export default defineConfig({
19007
19673
  {{/if}}
19008
19674
  },
19009
19675
  });`],
19676
+ ["db/prisma/sqlite/prisma/migrations/.gitkeep", ``],
19010
19677
  ["db/prisma/sqlite/prisma/schema/schema.prisma.hbs", `generator client {
19011
19678
  provider = "prisma-client"
19012
19679
  output = "../generated"
@@ -19252,7 +19919,6 @@ import { Ionicons } from "@expo/vector-icons";
19252
19919
  import {
19253
19920
  useUIMessages,
19254
19921
  useSmoothText,
19255
- type UIMessage,
19256
19922
  } from "@convex-dev/agent/react";
19257
19923
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
19258
19924
  import { useMutation } from "convex/react";
@@ -19306,7 +19972,7 @@ export default function AIScreen() {
19306
19972
  );
19307
19973
 
19308
19974
  const hasStreamingMessage = messages?.some(
19309
- (m: UIMessage) => m.status === "streaming",
19975
+ (m) => m.status === "streaming",
19310
19976
  );
19311
19977
 
19312
19978
  useEffect(() => {
@@ -19363,7 +20029,7 @@ export default function AIScreen() {
19363
20029
  </View>
19364
20030
  ) : (
19365
20031
  <View style={styles.messagesList}>
19366
- {messages.map((message: UIMessage) => (
20032
+ {messages.map((message) => (
19367
20033
  <View
19368
20034
  key={message.key}
19369
20035
  style={[
@@ -19383,7 +20049,9 @@ export default function AIScreen() {
19383
20049
  {message.role === "user" ? "You" : "AI Assistant"}
19384
20050
  </Text>
19385
20051
  <MessageContent
19386
- text={message.text ?? ""}
20052
+ text={(message.parts ?? [])
20053
+ .map((part) => (part.type === "text" ? part.text : ""))
20054
+ .join("")}
19387
20055
  isStreaming={message.status === "streaming"}
19388
20056
  textColor={theme.text}
19389
20057
  />
@@ -19546,6 +20214,7 @@ const styles = StyleSheet.create({
19546
20214
  });
19547
20215
  {{else}}
19548
20216
  import { useRef, useEffect, useState } from "react";
20217
+ import { Ionicons } from "@expo/vector-icons";
19549
20218
  import {
19550
20219
  View,
19551
20220
  Text,
@@ -19558,8 +20227,6 @@ import {
19558
20227
  } from "react-native";
19559
20228
  import { useChat } from "@ai-sdk/react";
19560
20229
  import { DefaultChatTransport } from "ai";
19561
- import { fetch as expoFetch } from "expo/fetch";
19562
- import { Ionicons } from "@expo/vector-icons";
19563
20230
  import { Container } from "@/components/container";
19564
20231
  import { useColorScheme } from "@/lib/use-color-scheme";
19565
20232
  import { NAV_THEME } from "@/lib/constants";
@@ -19582,7 +20249,6 @@ export default function AIScreen() {
19582
20249
  const [input, setInput] = useState("");
19583
20250
  const { messages, error, sendMessage } = useChat({
19584
20251
  transport: new DefaultChatTransport({
19585
- fetch: expoFetch as unknown as typeof globalThis.fetch,
19586
20252
  api: generateAPIUrl("/ai"),
19587
20253
  }),
19588
20254
  onError: (error) => console.error(error, "AI Chat Error"),
@@ -19862,7 +20528,6 @@ import { Ionicons } from "@expo/vector-icons";
19862
20528
  import {
19863
20529
  useUIMessages,
19864
20530
  useSmoothText,
19865
- type UIMessage,
19866
20531
  } from "@convex-dev/agent/react";
19867
20532
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
19868
20533
  import { useMutation } from "convex/react";
@@ -19913,7 +20578,7 @@ export default function AIScreen() {
19913
20578
  );
19914
20579
 
19915
20580
  const hasStreamingMessage = messages?.some(
19916
- (m: UIMessage) => m.status === "streaming",
20581
+ (m) => m.status === "streaming",
19917
20582
  );
19918
20583
 
19919
20584
  useEffect(() => {
@@ -19969,7 +20634,7 @@ export default function AIScreen() {
19969
20634
  </View>
19970
20635
  ) : (
19971
20636
  <View style={styles.messagesWrapper}>
19972
- {messages.map((message: UIMessage) => (
20637
+ {messages.map((message) => (
19973
20638
  <View
19974
20639
  key={message.key}
19975
20640
  style={[
@@ -19983,7 +20648,9 @@ export default function AIScreen() {
19983
20648
  {message.role === "user" ? "You" : "AI Assistant"}
19984
20649
  </Text>
19985
20650
  <MessageContent
19986
- text={message.text ?? ""}
20651
+ text={(message.parts ?? [])
20652
+ .map((part) => (part.type === "text" ? part.text : ""))
20653
+ .join("")}
19987
20654
  isStreaming={message.status === "streaming"}
19988
20655
  style={styles.messageContent}
19989
20656
  />
@@ -20163,7 +20830,6 @@ import {
20163
20830
  } from "react-native";
20164
20831
  import { useChat } from "@ai-sdk/react";
20165
20832
  import { DefaultChatTransport } from "ai";
20166
- import { fetch as expoFetch } from "expo/fetch";
20167
20833
  import { Ionicons } from "@expo/vector-icons";
20168
20834
  import { StyleSheet, useUnistyles } from "react-native-unistyles";
20169
20835
  import { Container } from "@/components/container";
@@ -20185,7 +20851,6 @@ export default function AIScreen() {
20185
20851
  const [input, setInput] = useState("");
20186
20852
  const { messages, error, sendMessage } = useChat({
20187
20853
  transport: new DefaultChatTransport({
20188
- fetch: expoFetch as unknown as typeof globalThis.fetch,
20189
20854
  api: generateAPIUrl("/ai"),
20190
20855
  }),
20191
20856
  onError: (error) => console.error(error, "AI Chat Error"),
@@ -20474,7 +21139,6 @@ import { Ionicons } from "@expo/vector-icons";
20474
21139
  import {
20475
21140
  useUIMessages,
20476
21141
  useSmoothText,
20477
- type UIMessage,
20478
21142
  } from "@convex-dev/agent/react";
20479
21143
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
20480
21144
  import { useMutation } from "convex/react";
@@ -20521,7 +21185,7 @@ export default function AIScreen() {
20521
21185
  );
20522
21186
 
20523
21187
  const hasStreamingMessage = messages?.some(
20524
- (m: UIMessage) => m.status === "streaming",
21188
+ (m) => m.status === "streaming",
20525
21189
  );
20526
21190
 
20527
21191
  useEffect(() => {
@@ -20576,7 +21240,7 @@ export default function AIScreen() {
20576
21240
  </Surface>
20577
21241
  ) : (
20578
21242
  <View className="gap-3">
20579
- {messages.map((message: UIMessage) => (
21243
+ {messages.map((message) => (
20580
21244
  <Surface
20581
21245
  key={message.key}
20582
21246
  variant={message.role === "user" ? "tertiary" : "secondary"}
@@ -20586,7 +21250,9 @@ export default function AIScreen() {
20586
21250
  {message.role === "user" ? "You" : "AI"}
20587
21251
  </Text>
20588
21252
  <MessageContent
20589
- text={message.text ?? ""}
21253
+ text={(message.parts ?? [])
21254
+ .map((part) => (part.type === "text" ? part.text : ""))
21255
+ .join("")}
20590
21256
  isStreaming={message.status === "streaming"}
20591
21257
  />
20592
21258
  </Surface>
@@ -20649,7 +21315,6 @@ import {
20649
21315
  } from "react-native";
20650
21316
  import { useChat } from "@ai-sdk/react";
20651
21317
  import { DefaultChatTransport } from "ai";
20652
- import { fetch as expoFetch } from "expo/fetch";
20653
21318
  import { Ionicons } from "@expo/vector-icons";
20654
21319
  import { Container } from "@/components/container";
20655
21320
  import { Button, Separator, FieldError, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
@@ -20670,7 +21335,6 @@ export default function AIScreen() {
20670
21335
  const [input, setInput] = useState("");
20671
21336
  const { messages, error, sendMessage, status } = useChat({
20672
21337
  transport: new DefaultChatTransport({
20673
- fetch: expoFetch as unknown as typeof globalThis.fetch,
20674
21338
  api: generateAPIUrl("/ai"),
20675
21339
  }),
20676
21340
  onError: (error) => console.error(error, "AI Chat Error"),
@@ -20994,7 +21658,6 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
20994
21658
  import {
20995
21659
  useUIMessages,
20996
21660
  useSmoothText,
20997
- type UIMessage,
20998
21661
  } from "@convex-dev/agent/react";
20999
21662
  import { useMutation } from "convex/react";
21000
21663
  import { Send, Loader2 } from "lucide-react";
@@ -21053,7 +21716,7 @@ export default function AIPage() {
21053
21716
  }, [messages]);
21054
21717
 
21055
21718
  const hasStreamingMessage = messages?.some(
21056
- (m: UIMessage) => m.status === "streaming",
21719
+ (m) => m.status === "streaming",
21057
21720
  );
21058
21721
 
21059
21722
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -21087,7 +21750,7 @@ export default function AIPage() {
21087
21750
  Ask me anything to get started!
21088
21751
  </div>
21089
21752
  ) : (
21090
- messages.map((message: UIMessage) => (
21753
+ messages.map((message) => (
21091
21754
  <div
21092
21755
  key={message.key}
21093
21756
  className={\`p-3 rounded-lg \${
@@ -21100,7 +21763,9 @@ export default function AIPage() {
21100
21763
  {message.role === "user" ? "You" : "AI Assistant"}
21101
21764
  </p>
21102
21765
  <MessageContent
21103
- text={message.text ?? ""}
21766
+ text={(message.parts ?? [])
21767
+ .map((part) => (part.type === "text" ? part.text : ""))
21768
+ .join("")}
21104
21769
  isStreaming={message.status === "streaming"}
21105
21770
  />
21106
21771
  </div>
@@ -21260,7 +21925,6 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
21260
21925
  import {
21261
21926
  useUIMessages,
21262
21927
  useSmoothText,
21263
- type UIMessage,
21264
21928
  } from "@convex-dev/agent/react";
21265
21929
  import { useMutation } from "convex/react";
21266
21930
  import { Send, Loader2 } from "lucide-react";
@@ -21303,7 +21967,7 @@ const AI: React.FC = () => {
21303
21967
  }, [messages]);
21304
21968
 
21305
21969
  const hasStreamingMessage = messages?.some(
21306
- (m: UIMessage) => m.status === "streaming",
21970
+ (m) => m.status === "streaming",
21307
21971
  );
21308
21972
 
21309
21973
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -21337,7 +22001,7 @@ const AI: React.FC = () => {
21337
22001
  Ask me anything to get started!
21338
22002
  </div>
21339
22003
  ) : (
21340
- messages.map((message: UIMessage) => (
22004
+ messages.map((message) => (
21341
22005
  <div
21342
22006
  key={message.key}
21343
22007
  className={\`p-3 rounded-lg \${
@@ -21350,7 +22014,9 @@ const AI: React.FC = () => {
21350
22014
  {message.role === "user" ? "You" : "AI Assistant"}
21351
22015
  </p>
21352
22016
  <MessageContent
21353
- text={message.text ?? ""}
22017
+ text={(message.parts ?? [])
22018
+ .map((part) => (part.type === "text" ? part.text : ""))
22019
+ .join("")}
21354
22020
  isStreaming={message.status === "streaming"}
21355
22021
  />
21356
22022
  </div>
@@ -21496,7 +22162,6 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
21496
22162
  import {
21497
22163
  useUIMessages,
21498
22164
  useSmoothText,
21499
- type UIMessage,
21500
22165
  } from "@convex-dev/agent/react";
21501
22166
  import { createFileRoute } from "@tanstack/react-router";
21502
22167
  import { useMutation } from "convex/react";
@@ -21544,7 +22209,7 @@ function RouteComponent() {
21544
22209
  }, [messages]);
21545
22210
 
21546
22211
  const hasStreamingMessage = messages?.some(
21547
- (m: UIMessage) => m.status === "streaming",
22212
+ (m) => m.status === "streaming",
21548
22213
  );
21549
22214
 
21550
22215
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -21578,7 +22243,7 @@ function RouteComponent() {
21578
22243
  Ask me anything to get started!
21579
22244
  </div>
21580
22245
  ) : (
21581
- messages.map((message: UIMessage) => (
22246
+ messages.map((message) => (
21582
22247
  <div
21583
22248
  key={message.key}
21584
22249
  className={\`p-3 rounded-lg \${
@@ -21591,7 +22256,9 @@ function RouteComponent() {
21591
22256
  {message.role === "user" ? "You" : "AI Assistant"}
21592
22257
  </p>
21593
22258
  <MessageContent
21594
- text={message.text ?? ""}
22259
+ text={(message.parts ?? [])
22260
+ .map((part) => (part.type === "text" ? part.text : ""))
22261
+ .join("")}
21595
22262
  isStreaming={message.status === "streaming"}
21596
22263
  />
21597
22264
  </div>
@@ -21739,7 +22406,6 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
21739
22406
  import {
21740
22407
  useUIMessages,
21741
22408
  useSmoothText,
21742
- type UIMessage,
21743
22409
  } from "@convex-dev/agent/react";
21744
22410
  import { createFileRoute } from "@tanstack/react-router";
21745
22411
  import { useMutation } from "convex/react";
@@ -21787,7 +22453,7 @@ function RouteComponent() {
21787
22453
  }, [messages]);
21788
22454
 
21789
22455
  const hasStreamingMessage = messages?.some(
21790
- (m: UIMessage) => m.status === "streaming",
22456
+ (m) => m.status === "streaming",
21791
22457
  );
21792
22458
 
21793
22459
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -21821,7 +22487,7 @@ function RouteComponent() {
21821
22487
  Ask me anything to get started!
21822
22488
  </div>
21823
22489
  ) : (
21824
- messages.map((message: UIMessage) => (
22490
+ messages.map((message) => (
21825
22491
  <div
21826
22492
  key={message.key}
21827
22493
  className={\`p-3 rounded-lg \${
@@ -21834,7 +22500,9 @@ function RouteComponent() {
21834
22500
  {message.role === "user" ? "You" : "AI Assistant"}
21835
22501
  </p>
21836
22502
  <MessageContent
21837
- text={message.text ?? ""}
22503
+ text={(message.parts ?? [])
22504
+ .map((part) => (part.type === "text" ? part.text : ""))
22505
+ .join("")}
21838
22506
  isStreaming={message.status === "streaming"}
21839
22507
  />
21840
22508
  </div>
@@ -22149,7 +22817,7 @@ import { Ionicons } from "@expo/vector-icons";
22149
22817
  {{#if (eq backend "convex")}}
22150
22818
  import { useMutation, useQuery } from "convex/react";
22151
22819
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
22152
- import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
22820
+ import type { Doc, Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
22153
22821
  {{else}}
22154
22822
  import { useMutation, useQuery } from "@tanstack/react-query";
22155
22823
  {{/if}}
@@ -22199,7 +22867,7 @@ export default function TodosScreen() {
22199
22867
  }
22200
22868
 
22201
22869
  const isLoading = !todos;
22202
- const completedCount = todos?.filter((t) => t.completed).length || 0;
22870
+ const completedCount = todos?.filter((t: Doc<"todos">) => t.completed).length || 0;
22203
22871
  const totalCount = todos?.length || 0;
22204
22872
  {{else}}
22205
22873
  {{#if (eq api "orpc")}}
@@ -22408,7 +23076,7 @@ export default function TodosScreen() {
22408
23076
 
22409
23077
  {todos && todos.length > 0 && (
22410
23078
  <View style={styles.todosList}>
22411
- {todos.map((todo) => (
23079
+ {todos.map((todo: Doc<"todos">) => (
22412
23080
  <View
22413
23081
  key={todo._id}
22414
23082
  style={[
@@ -22598,9 +23266,10 @@ const styles = StyleSheet.create({
22598
23266
  fontSize: 16,
22599
23267
  },
22600
23268
  addButton: {
22601
- padding: 12,
22602
- justifyContent: "center",
23269
+ width: 48,
23270
+ height: 48,
22603
23271
  alignItems: "center",
23272
+ justifyContent: "center",
22604
23273
  },
22605
23274
  centerContainer: {
22606
23275
  alignItems: "center",
@@ -22638,23 +23307,24 @@ const styles = StyleSheet.create({
22638
23307
  alignItems: "center",
22639
23308
  gap: 12,
22640
23309
  },
22641
- checkbox: {
22642
- width: 20,
22643
- height: 20,
22644
- borderWidth: 2,
22645
- justifyContent: "center",
22646
- alignItems: "center",
22647
- },
22648
23310
  todoTextContainer: {
22649
23311
  flex: 1,
22650
23312
  },
22651
23313
  todoText: {
22652
23314
  fontSize: 16,
22653
23315
  },
23316
+ checkbox: {
23317
+ width: 24,
23318
+ height: 24,
23319
+ borderWidth: 2,
23320
+ alignItems: "center",
23321
+ justifyContent: "center",
23322
+ },
22654
23323
  deleteButton: {
22655
- padding: 8,
23324
+ padding: 4,
22656
23325
  },
22657
- });`],
23326
+ });
23327
+ `],
22658
23328
  ["examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs", `import { useState } from "react";
22659
23329
  import {
22660
23330
  View,
@@ -22671,7 +23341,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
22671
23341
  {{#if (eq backend "convex")}}
22672
23342
  import { useMutation, useQuery } from "convex/react";
22673
23343
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
22674
- import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
23344
+ import type { Doc, Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
22675
23345
  {{else}}
22676
23346
  import { useMutation, useQuery } from "@tanstack/react-query";
22677
23347
  {{/if}}
@@ -22839,7 +23509,7 @@ export default function TodosScreen() {
22839
23509
  {todos && todos.length === 0 && !isLoading && (
22840
23510
  <Text style={styles.emptyText}>No todos yet. Add one!</Text>
22841
23511
  )}
22842
- {todos?.map((todo) => (
23512
+ {todos?.map((todo: Doc<"todos">) => (
22843
23513
  <View key={todo._id} style={styles.todoItem}>
22844
23514
  <TouchableOpacity
22845
23515
  onPress={() => handleToggleTodo(todo._id, todo.completed)}
@@ -23002,7 +23672,7 @@ import { Ionicons } from "@expo/vector-icons";
23002
23672
  {{#if (eq backend "convex")}}
23003
23673
  import { useMutation, useQuery } from "convex/react";
23004
23674
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
23005
- import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
23675
+ import type { Doc, Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
23006
23676
  {{else}}
23007
23677
  import { useMutation, useQuery } from "@tanstack/react-query";
23008
23678
  {{/if}}
@@ -23093,7 +23763,7 @@ export default function TodosScreen() {
23093
23763
  };
23094
23764
 
23095
23765
  const isLoading = !todos;
23096
- const completedCount = todos?.filter((t) => t.completed).length || 0;
23766
+ const completedCount = todos?.filter((t: Doc<"todos">) => t.completed).length || 0;
23097
23767
  const totalCount = todos?.length || 0;
23098
23768
  {{else}}
23099
23769
  const handleAddTodo = () => {
@@ -23205,7 +23875,7 @@ export default function TodosScreen() {
23205
23875
 
23206
23876
  {todos && todos.length > 0 && (
23207
23877
  <View className="gap-2">
23208
- {todos.map((todo) => (
23878
+ {todos.map((todo: Doc<"todos">) => (
23209
23879
  <Surface key={todo._id} variant="secondary" className="p-3 rounded-lg">
23210
23880
  <View className="flex-row items-center gap-3">
23211
23881
  <Checkbox
@@ -23277,7 +23947,8 @@ export default function TodosScreen() {
23277
23947
  </ScrollView>
23278
23948
  </Container>
23279
23949
  );
23280
- }`],
23950
+ }
23951
+ `],
23281
23952
  ["examples/todo/server/drizzle/base/src/routers/todo.ts.hbs", `{{#if (eq api "orpc")}}
23282
23953
  import { eq } from "drizzle-orm";
23283
23954
  import z from "zod";
@@ -23412,19 +24083,22 @@ export const todo = sqliteTable("todo", {
23412
24083
  `],
23413
24084
  ["examples/todo/server/mongoose/base/src/routers/todo.ts.hbs", `{{#if (eq api "orpc")}}
23414
24085
  import z from "zod";
24086
+ import "@{{projectName}}/db";
23415
24087
  import { publicProcedure } from "../index";
23416
24088
  import { Todo } from "@{{projectName}}/db/models/todo.model";
23417
24089
 
23418
24090
  export const todoRouter = {
23419
24091
  getAll: publicProcedure.handler(async () => {
23420
- return await Todo.find().lean();
24092
+ const todos = await Todo.find().lean();
24093
+ return todos.map((todo) => ({ ...todo, id: todo.id }));
23421
24094
  }),
23422
24095
 
23423
24096
  create: publicProcedure
23424
24097
  .input(z.object({ text: z.string().min(1) }))
23425
24098
  .handler(async ({ input }) => {
23426
24099
  const newTodo = await Todo.create({ text: input.text });
23427
- return newTodo.toObject();
24100
+ const todo = newTodo.toObject();
24101
+ return { ...todo, id: todo.id };
23428
24102
  }),
23429
24103
 
23430
24104
  toggle: publicProcedure
@@ -23446,19 +24120,22 @@ export const todoRouter = {
23446
24120
 
23447
24121
  {{#if (eq api "trpc")}}
23448
24122
  import z from "zod";
24123
+ import "@{{projectName}}/db";
23449
24124
  import { router, publicProcedure } from "../index";
23450
24125
  import { Todo } from "@{{projectName}}/db/models/todo.model";
23451
24126
 
23452
24127
  export const todoRouter = router({
23453
24128
  getAll: publicProcedure.query(async () => {
23454
- return await Todo.find().lean();
24129
+ const todos = await Todo.find().lean();
24130
+ return todos.map((todo) => ({ ...todo, id: todo.id }));
23455
24131
  }),
23456
24132
 
23457
24133
  create: publicProcedure
23458
24134
  .input(z.object({ text: z.string().min(1) }))
23459
24135
  .mutation(async ({ input }) => {
23460
24136
  const newTodo = await Todo.create({ text: input.text });
23461
- return newTodo.toObject();
24137
+ const todo = newTodo.toObject();
24138
+ return { ...todo, id: todo.id };
23462
24139
  }),
23463
24140
 
23464
24141
  toggle: publicProcedure
@@ -23483,8 +24160,9 @@ const { Schema, model } = mongoose;
23483
24160
 
23484
24161
  const todoSchema = new Schema({
23485
24162
  id: {
23486
- type: mongoose.Schema.Types.ObjectId,
23487
- auto: true,
24163
+ type: String,
24164
+ required: true,
24165
+ default: () => new mongoose.Types.ObjectId().toString(),
23488
24166
  },
23489
24167
  text: {
23490
24168
  type: String,
@@ -23495,7 +24173,8 @@ const todoSchema = new Schema({
23495
24173
  default: false,
23496
24174
  },
23497
24175
  }, {
23498
- collection: 'todo'
24176
+ collection: 'todo',
24177
+ id: false,
23499
24178
  });
23500
24179
 
23501
24180
  const Todo = model('Todo', todoSchema);
@@ -24103,6 +24782,9 @@ import { trpc } from "@/utils/trpc";
24103
24782
  {{/if}}
24104
24783
  {{/if}}
24105
24784
 
24785
+ {{#unless (eq backend "convex")}}
24786
+ type TodoId = {{#if (or (eq orm "mongoose") (eq database "mongodb"))}}string{{else}}number{{/if}};
24787
+ {{/unless}}
24106
24788
 
24107
24789
  export default function TodosPage() {
24108
24790
  const [newTodoText, setNewTodoText] = useState("");
@@ -24179,11 +24861,11 @@ export default function TodosPage() {
24179
24861
  }
24180
24862
  };
24181
24863
 
24182
- const handleToggleTodo = (id: number, completed: boolean) => {
24864
+ const handleToggleTodo = (id: TodoId, completed: boolean) => {
24183
24865
  toggleMutation.mutate({ id, completed: !completed });
24184
24866
  };
24185
24867
 
24186
- const handleDeleteTodo = (id: number) => {
24868
+ const handleDeleteTodo = (id: TodoId) => {
24187
24869
  deleteMutation.mutate({ id });
24188
24870
  };
24189
24871
  {{/if}}
@@ -24347,6 +25029,10 @@ import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
24347
25029
  import { useMutation, useQuery } from "@tanstack/react-query";
24348
25030
  {{/if}}
24349
25031
 
25032
+ {{#unless (eq backend "convex")}}
25033
+ type TodoId = {{#if (or (eq orm "mongoose") (eq database "mongodb"))}}string{{else}}number{{/if}};
25034
+ {{/unless}}
25035
+
24350
25036
  export default function Todos() {
24351
25037
  const [newTodoText, setNewTodoText] = useState("");
24352
25038
 
@@ -24422,11 +25108,11 @@ export default function Todos() {
24422
25108
  }
24423
25109
  };
24424
25110
 
24425
- const handleToggleTodo = (id: number, completed: boolean) => {
25111
+ const handleToggleTodo = (id: TodoId, completed: boolean) => {
24426
25112
  toggleMutation.mutate({ id, completed: !completed });
24427
25113
  };
24428
25114
 
24429
- const handleDeleteTodo = (id: number) => {
25115
+ const handleDeleteTodo = (id: TodoId) => {
24430
25116
  deleteMutation.mutate({ id });
24431
25117
  };
24432
25118
  {{/if}}
@@ -24591,6 +25277,10 @@ import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
24591
25277
  import { useMutation, useQuery } from "@tanstack/react-query";
24592
25278
  {{/if}}
24593
25279
 
25280
+ {{#unless (eq backend "convex")}}
25281
+ type TodoId = {{#if (or (eq orm "mongoose") (eq database "mongodb"))}}string{{else}}number{{/if}};
25282
+ {{/unless}}
25283
+
24594
25284
  export const Route = createFileRoute("/todos")({
24595
25285
  component: TodosRoute,
24596
25286
  });
@@ -24670,11 +25360,11 @@ function TodosRoute() {
24670
25360
  }
24671
25361
  };
24672
25362
 
24673
- const handleToggleTodo = (id: number, completed: boolean) => {
25363
+ const handleToggleTodo = (id: TodoId, completed: boolean) => {
24674
25364
  toggleMutation.mutate({ id, completed: !completed });
24675
25365
  };
24676
25366
 
24677
- const handleDeleteTodo = (id: number) => {
25367
+ const handleDeleteTodo = (id: TodoId) => {
24678
25368
  deleteMutation.mutate({ id });
24679
25369
  };
24680
25370
  {{/if}}
@@ -24845,6 +25535,10 @@ import { orpc } from "@/utils/orpc";
24845
25535
  import { useMutation, useQuery } from "@tanstack/react-query";
24846
25536
  {{/if}}
24847
25537
 
25538
+ {{#unless (eq backend "convex")}}
25539
+ type TodoId = {{#if (or (eq orm "mongoose") (eq database "mongodb"))}}string{{else}}number{{/if}};
25540
+ {{/unless}}
25541
+
24848
25542
  export const Route = createFileRoute("/todos")({
24849
25543
  component: TodosRoute,
24850
25544
  });
@@ -24946,11 +25640,11 @@ function TodosRoute() {
24946
25640
  }
24947
25641
  };
24948
25642
 
24949
- const handleToggleTodo = (id: number, completed: boolean) => {
25643
+ const handleToggleTodo = (id: TodoId, completed: boolean) => {
24950
25644
  toggleMutation.mutate({ id, completed: !completed });
24951
25645
  };
24952
25646
 
24953
- const handleDeleteTodo = (id: number) => {
25647
+ const handleDeleteTodo = (id: TodoId) => {
24954
25648
  deleteMutation.mutate({ id });
24955
25649
  };
24956
25650
  {{/if}}
@@ -25541,11 +26235,15 @@ shamefully-hoist=true
25541
26235
  strict-peer-dependencies=false
25542
26236
  {{/if}}`],
25543
26237
  ["extras/bunfig.toml.hbs", `[install]
25544
- {{#if (or (includes frontend "nuxt"))}}
25545
- linker = "hoisted" # having issues with Nuxt when linker is isolated
26238
+ {{#if (includes frontend "nuxt")}}
26239
+ linker = "hoisted" # Nuxt needs hoisting for its dependency resolver
25546
26240
  {{else}}
25547
26241
  linker = "isolated"
25548
- {{/if}}`],
26242
+ {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
26243
+ peer = false # Expo native projects declare SDK peers explicitly; this keeps Bun isolated installs deduped for native modules
26244
+ {{/if}}
26245
+ {{/if}}
26246
+ `],
25549
26247
  ["extras/env.d.ts.hbs", `{{#if (eq serverDeploy "cloudflare")}}
25550
26248
  import { type server } from "@{{projectName}}/infra/alchemy.run";
25551
26249
  {{else}}
@@ -25567,9 +26265,40 @@ declare module "cloudflare:workers" {
25567
26265
  }
25568
26266
  }
25569
26267
  `],
25570
- ["extras/pnpm-workspace.yaml", `packages:
26268
+ ["extras/pnpm-workspace.yaml.hbs", `packages:
25571
26269
  - "apps/*"
25572
26270
  - "packages/*"
26271
+ {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq orm "prisma") (includes addons "lefthook") (includes addons "nx") (includes addons "pwa") (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
26272
+
26273
+ # pnpm 11 blocks dependency lifecycle scripts unless they are approved here.
26274
+ # Entries are scoped to packages this generated stack can pull in.
26275
+ allowBuilds:
26276
+ {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (includes frontend "tanstack-start"))}}
26277
+ esbuild: true
26278
+ {{/if}}
26279
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start") (includes frontend "next"))}}
26280
+ msw: true
26281
+ {{/if}}
26282
+ {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
26283
+ msgpackr-extract: true
26284
+ {{/if}}
26285
+ {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (includes addons "pwa"))}}
26286
+ sharp: true
26287
+ {{/if}}
26288
+ {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare"))}}
26289
+ workerd: true
26290
+ {{/if}}
26291
+ {{#if (eq orm "prisma")}}
26292
+ "@prisma/engines": true
26293
+ prisma: true
26294
+ {{/if}}
26295
+ {{#if (includes addons "lefthook")}}
26296
+ lefthook: true
26297
+ {{/if}}
26298
+ {{#if (includes addons "nx")}}
26299
+ nx: true
26300
+ {{/if}}
26301
+ {{/if}}
25573
26302
  `],
25574
26303
  ["frontend/astro/_gitignore", `# build output
25575
26304
  dist/
@@ -25938,12 +26667,7 @@ import { env } from "@{{projectName}}/env/native";
25938
26667
  {{/if}}
25939
26668
 
25940
26669
  import { Stack } from "expo-router";
25941
- import {
25942
- DarkTheme,
25943
- DefaultTheme,
25944
- type Theme,
25945
- ThemeProvider,
25946
- } from "@react-navigation/native";
26670
+ import { DarkTheme, DefaultTheme, ThemeProvider } from "expo-router/react-navigation";
25947
26671
  import { StatusBar } from "expo-status-bar";
25948
26672
  import { GestureHandlerRootView } from "react-native-gesture-handler";
25949
26673
  {{#if (eq api "trpc")}}
@@ -25956,11 +26680,11 @@ import { NAV_THEME } from "@/lib/constants";
25956
26680
  import { useColorScheme } from "@/lib/use-color-scheme";
25957
26681
  import { StyleSheet } from "react-native";
25958
26682
 
25959
- const LIGHT_THEME: Theme = {
26683
+ const LIGHT_THEME = {
25960
26684
  ...DefaultTheme,
25961
26685
  colors: NAV_THEME.light,
25962
26686
  };
25963
- const DARK_THEME: Theme = {
26687
+ const DARK_THEME = {
25964
26688
  ...DarkTheme,
25965
26689
  colors: NAV_THEME.dark,
25966
26690
  };
@@ -25971,9 +26695,6 @@ export const unstable_settings = {
25971
26695
 
25972
26696
  {{#if (eq backend "convex")}}
25973
26697
  const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
25974
- {{#if (eq auth "better-auth")}}
25975
- expectAuth: true,
25976
- {{/if}}
25977
26698
  unsavedChangesWarning: false,
25978
26699
  });
25979
26700
  {{/if}}
@@ -26240,7 +26961,8 @@ export default function TabLayout() {
26240
26961
 
26241
26962
  `],
26242
26963
  ["frontend/native/bare/app/(drawer)/(tabs)/index.tsx.hbs", `import { Container } from "@/components/container";
26243
- import { ScrollView, Text, View, StyleSheet } from "react-native";
26964
+ import { Column, Host, Text as ExpoUIText } from "@expo/ui";
26965
+ import { ScrollView, View, StyleSheet } from "react-native";
26244
26966
  import { useColorScheme } from "@/lib/use-color-scheme";
26245
26967
  import { NAV_THEME } from "@/lib/constants";
26246
26968
 
@@ -26252,12 +26974,21 @@ export default function TabOne() {
26252
26974
  <Container>
26253
26975
  <ScrollView style={styles.scrollView}>
26254
26976
  <View style={styles.content}>
26255
- <Text style={[styles.title, { color: theme.text }]}>
26256
- Tab One
26257
- </Text>
26258
- <Text style={[styles.subtitle, { color: theme.text, opacity: 0.7 }]}>
26259
- Explore the first section of your app
26260
- </Text>
26977
+ <Host matchContents=\\{{ vertical: true }}>
26978
+ <Column spacing={8}>
26979
+ <ExpoUIText
26980
+ textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold" }}
26981
+ >
26982
+ Tab One
26983
+ </ExpoUIText>
26984
+ <ExpoUIText
26985
+ textStyle=\\{{ color: theme.text, fontSize: 16 }}
26986
+ style=\\{{ opacity: 0.7 }}
26987
+ >
26988
+ Explore the first section of your app
26989
+ </ExpoUIText>
26990
+ </Column>
26991
+ </Host>
26261
26992
  </View>
26262
26993
  </ScrollView>
26263
26994
  </Container>
@@ -26272,19 +27003,11 @@ const styles = StyleSheet.create({
26272
27003
  content: {
26273
27004
  paddingVertical: 16,
26274
27005
  },
26275
- title: {
26276
- fontSize: 24,
26277
- fontWeight: "bold",
26278
- marginBottom: 8,
26279
- },
26280
- subtitle: {
26281
- fontSize: 16,
26282
- },
26283
27006
  });
26284
-
26285
27007
  `],
26286
27008
  ["frontend/native/bare/app/(drawer)/(tabs)/two.tsx.hbs", `import { Container } from "@/components/container";
26287
- import { ScrollView, Text, View, StyleSheet } from "react-native";
27009
+ import { Column, Host, Text as ExpoUIText } from "@expo/ui";
27010
+ import { ScrollView, View, StyleSheet } from "react-native";
26288
27011
  import { useColorScheme } from "@/lib/use-color-scheme";
26289
27012
  import { NAV_THEME } from "@/lib/constants";
26290
27013
 
@@ -26296,12 +27019,21 @@ export default function TabTwo() {
26296
27019
  <Container>
26297
27020
  <ScrollView style={styles.scrollView}>
26298
27021
  <View style={styles.content}>
26299
- <Text style={[styles.title, { color: theme.text }]}>
26300
- Tab Two
26301
- </Text>
26302
- <Text style={[styles.subtitle, { color: theme.text, opacity: 0.7 }]}>
26303
- Discover more features and content
26304
- </Text>
27022
+ <Host matchContents=\\{{ vertical: true }}>
27023
+ <Column spacing={8}>
27024
+ <ExpoUIText
27025
+ textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold" }}
27026
+ >
27027
+ Tab Two
27028
+ </ExpoUIText>
27029
+ <ExpoUIText
27030
+ textStyle=\\{{ color: theme.text, fontSize: 16 }}
27031
+ style=\\{{ opacity: 0.7 }}
27032
+ >
27033
+ Discover more features and content
27034
+ </ExpoUIText>
27035
+ </Column>
27036
+ </Host>
26305
27037
  </View>
26306
27038
  </ScrollView>
26307
27039
  </Container>
@@ -26316,18 +27048,15 @@ const styles = StyleSheet.create({
26316
27048
  content: {
26317
27049
  paddingVertical: 16,
26318
27050
  },
26319
- title: {
26320
- fontSize: 24,
26321
- fontWeight: "bold",
26322
- marginBottom: 8,
26323
- },
26324
- subtitle: {
26325
- fontSize: 16,
26326
- },
26327
27051
  });
26328
-
26329
27052
  `],
26330
- ["frontend/native/bare/app/(drawer)/index.tsx.hbs", `import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
27053
+ ["frontend/native/bare/app/(drawer)/index.tsx.hbs", `import { {{#if (or (eq auth "clerk") (eq auth "better-auth"))}}Button, {{/if}}Column, Host, Text as ExpoUIText } from "@expo/ui";
27054
+ import { View, ScrollView, StyleSheet{{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}, Alert{{/if}} } from "react-native";
27055
+ {{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}
27056
+ import * as Linking from "expo-linking";
27057
+ import * as WebBrowser from "expo-web-browser";
27058
+ import { env } from "@{{projectName}}/env/native";
27059
+ {{/if}}
26331
27060
  import { Container } from "@/components/container";
26332
27061
  import { useColorScheme } from "@/lib/use-color-scheme";
26333
27062
  import { NAV_THEME } from "@/lib/constants";
@@ -26340,17 +27069,17 @@ import { useQuery } from "@tanstack/react-query";
26340
27069
  import { trpc } from "@/utils/trpc";
26341
27070
  {{/if}}
26342
27071
  {{#if (and (eq backend "convex") (eq auth "clerk"))}}
26343
- import { Link } from "expo-router";
27072
+ import { router } from "expo-router";
26344
27073
  import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
26345
27074
  import { api } from "@{{ projectName }}/backend/convex/_generated/api";
26346
27075
  import { useUser } from "@clerk/expo";
26347
27076
  import { SignOutButton } from "@/components/sign-out-button";
26348
27077
  {{else if (and (ne backend "convex") (eq auth "clerk"))}}
26349
- import { Link } from "expo-router";
27078
+ import { router } from "expo-router";
26350
27079
  import { useAuth, useUser } from "@clerk/expo";
26351
27080
  import { SignOutButton } from "@/components/sign-out-button";
26352
27081
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
26353
- import { useConvexAuth, useQuery } from "convex/react";
27082
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
26354
27083
  import { api } from "@{{ projectName }}/backend/convex/_generated/api";
26355
27084
  import { authClient } from "@/lib/auth-client";
26356
27085
  import { SignIn } from "@/components/sign-in";
@@ -26380,17 +27109,72 @@ const { user } = useUser();
26380
27109
  const healthCheck = useQuery(api.healthCheck.get);
26381
27110
  const { isAuthenticated } = useConvexAuth();
26382
27111
  const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
27112
+ {{#if (eq payments "polar")}}
27113
+ const products = useQuery(api.polar.listAllProducts);
27114
+ const subscription = useQuery(api.polar.getCurrentSubscription);
27115
+ const generateCheckoutLink = useAction(api.polar.generateCheckoutLink);
27116
+ const generateCustomerPortalUrl = useAction(api.polar.generateCustomerPortalUrl);
27117
+ const recurringProduct = products?.find((product) => product.isRecurring);
27118
+
27119
+ const openPolarLink = async (url: string, returnUrl: string) => {
27120
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
27121
+ };
27122
+
27123
+ const getPolarReturnUrl = (returnUrl: string) => {
27124
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_CONVEX_SITE_URL);
27125
+ url.searchParams.set("returnUrl", returnUrl);
27126
+ return url.toString();
27127
+ };
27128
+
27129
+ const handlePolarCheckout = async () => {
27130
+ try {
27131
+ if (!recurringProduct) {
27132
+ Alert.alert("Checkout unavailable", "No recurring Polar product is available yet.");
27133
+ return;
27134
+ }
27135
+
27136
+ const returnUrl = Linking.createURL("/");
27137
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
27138
+ const { url } = await generateCheckoutLink({
27139
+ productIds: [recurringProduct.id],
27140
+ origin: env.EXPO_PUBLIC_CONVEX_SITE_URL,
27141
+ successUrl: polarReturnUrl,
27142
+ });
27143
+
27144
+ await openPolarLink(url, returnUrl);
27145
+ } catch {
27146
+ Alert.alert("Checkout failed", "Unable to open Polar checkout. Please try again.");
27147
+ }
27148
+ };
27149
+
27150
+ const handlePolarPortal = async () => {
27151
+ try {
27152
+ const returnUrl = Linking.createURL("/");
27153
+ const { url } = await generateCustomerPortalUrl({
27154
+ returnUrl: getPolarReturnUrl(returnUrl),
27155
+ });
27156
+
27157
+ await openPolarLink(url, returnUrl);
27158
+ } catch {
27159
+ Alert.alert("Portal unavailable", "Unable to open the customer portal. Please try again.");
27160
+ }
27161
+ };
27162
+ {{/if}}
26383
27163
  {{else if (eq backend "convex")}}
26384
27164
  const healthCheck = useQuery(api.healthCheck.get);
26385
27165
  {{/if}}
26386
27166
 
26387
27167
  return (
26388
27168
  <Container>
26389
- <ScrollView style={styles.scrollView}>
27169
+ <ScrollView style={styles.scrollView} contentInsetAdjustmentBehavior="never">
26390
27170
  <View style={styles.content}>
26391
- <Text style={[styles.title, { color: theme.text }]}>
26392
- BETTER T STACK
26393
- </Text>
27171
+ <Host style={styles.titleHost}>
27172
+ <ExpoUIText
27173
+ textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold", textAlign: "center" }}
27174
+ >
27175
+ BETTER T STACK
27176
+ </ExpoUIText>
27177
+ </Host>
26394
27178
 
26395
27179
  {{#unless (and (eq backend "convex") (eq auth "better-auth"))}}
26396
27180
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
@@ -26398,16 +27182,25 @@ return (
26398
27182
  <View style={styles.statusRow}>
26399
27183
  <View style={[styles.statusIndicator, { backgroundColor: healthCheck ? "#10b981" : "#f59e0b" }]} />
26400
27184
  <View style={styles.statusContent}>
26401
- <Text style={[styles.statusTitle, { color: theme.text }]}>
26402
- Convex
26403
- </Text>
26404
- <Text style={[styles.statusText, { color: theme.text, opacity: 0.7 }]}>
26405
- {healthCheck === undefined
26406
- ? "Checking..."
26407
- : healthCheck === "OK"
26408
- ? "Connected to API"
26409
- : "API Disconnected"}
26410
- </Text>
27185
+ <Host matchContents=\\{{ vertical: true }}>
27186
+ <Column spacing={4}>
27187
+ <ExpoUIText
27188
+ textStyle=\\{{ color: theme.text, fontSize: 14, fontWeight: "bold" }}
27189
+ >
27190
+ Convex
27191
+ </ExpoUIText>
27192
+ <ExpoUIText
27193
+ textStyle=\\{{ color: theme.text, fontSize: 12 }}
27194
+ style=\\{{ opacity: 0.7 }}
27195
+ >
27196
+ {healthCheck === undefined
27197
+ ? "Checking..."
27198
+ : healthCheck === "OK"
27199
+ ? "Connected to API"
27200
+ : "API Disconnected"}
27201
+ </ExpoUIText>
27202
+ </Column>
27203
+ </Host>
26411
27204
  </View>
26412
27205
  </View>
26413
27206
  {{else}}
@@ -26415,16 +27208,25 @@ return (
26415
27208
  <View style={styles.statusRow}>
26416
27209
  <View style={[styles.statusIndicator, { backgroundColor: healthCheck.data ? "#10b981" : "#f59e0b" }]} />
26417
27210
  <View style={styles.statusContent}>
26418
- <Text style={[styles.statusTitle, { color: theme.text }]}>
26419
- {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}}
26420
- </Text>
26421
- <Text style={[styles.statusText, { color: theme.text, opacity: 0.7 }]}>
26422
- {healthCheck.isLoading
26423
- ? "Checking connection..."
26424
- : healthCheck.data
26425
- ? "All systems operational"
26426
- : "Service unavailable"}
26427
- </Text>
27211
+ <Host matchContents=\\{{ vertical: true }}>
27212
+ <Column spacing={4}>
27213
+ <ExpoUIText
27214
+ textStyle=\\{{ color: theme.text, fontSize: 14, fontWeight: "bold" }}
27215
+ >
27216
+ {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}}
27217
+ </ExpoUIText>
27218
+ <ExpoUIText
27219
+ textStyle=\\{{ color: theme.text, fontSize: 12 }}
27220
+ style=\\{{ opacity: 0.7 }}
27221
+ >
27222
+ {healthCheck.isLoading
27223
+ ? "Checking connection..."
27224
+ : healthCheck.data
27225
+ ? "All systems operational"
27226
+ : "Service unavailable"}
27227
+ </ExpoUIText>
27228
+ </Column>
27229
+ </Host>
26428
27230
  </View>
26429
27231
  </View>
26430
27232
  {{/unless}}
@@ -26434,85 +27236,154 @@ return (
26434
27236
 
26435
27237
  {{#if (and (eq backend "convex") (eq auth "clerk"))}}
26436
27238
  <Authenticated>
26437
- <Text style=\\{{ color: theme.text }}>Hello {user?.emailAddresses[0].emailAddress}</Text>
26438
- <Text style=\\{{ color: theme.text }}>Private Data: {privateData?.message}</Text>
27239
+ <Host style={styles.authHost} matchContents=\\{{ vertical: true }}>
27240
+ <Column spacing={6}>
27241
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 14 }}>
27242
+ {\`Hello \${user?.emailAddresses[0].emailAddress ?? ""}\`}
27243
+ </ExpoUIText>
27244
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 14 }}>
27245
+ {\`Private Data: \${privateData?.message ?? ""}\`}
27246
+ </ExpoUIText>
27247
+ </Column>
27248
+ </Host>
26439
27249
  <SignOutButton />
26440
27250
  </Authenticated>
26441
27251
  <Unauthenticated>
26442
- <Link href="/(auth)/sign-in">
26443
- <Text style=\\{{ color: theme.primary }}>Sign in</Text>
26444
- </Link>
26445
- <Link href="/(auth)/sign-up">
26446
- <Text style=\\{{ color: theme.primary }}>Sign up</Text>
26447
- </Link>
27252
+ <Host style={styles.authActionsHost} matchContents=\\{{ vertical: true }}>
27253
+ <Column spacing={8}>
27254
+ <Button
27255
+ label="Sign in"
27256
+ variant="outlined"
27257
+ onPress={() => router.push("/(auth)/sign-in")}
27258
+ />
27259
+ <Button
27260
+ label="Sign up"
27261
+ onPress={() => router.push("/(auth)/sign-up")}
27262
+ />
27263
+ </Column>
27264
+ </Host>
26448
27265
  </Unauthenticated>
26449
27266
  <AuthLoading>
26450
- <Text style=\\{{ color: theme.text }}>Loading...</Text>
27267
+ <Host matchContents=\\{{ vertical: true }}>
27268
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 14 }}>
27269
+ Loading...
27270
+ </ExpoUIText>
27271
+ </Host>
26451
27272
  </AuthLoading>
26452
27273
  {{/if}}
26453
27274
 
26454
27275
  {{#if (and (ne backend "convex") (eq auth "clerk"))}}
26455
27276
  {!isLoaded ? (
26456
- <Text style=\\{{ color: theme.text }}>Loading...</Text>
27277
+ <Host matchContents=\\{{ vertical: true }}>
27278
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 14 }}>
27279
+ Loading...
27280
+ </ExpoUIText>
27281
+ </Host>
26457
27282
  ) : isSignedIn ? (
26458
27283
  <View style={[styles.userCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
26459
- <View style={styles.userHeader}>
26460
- <Text style={[styles.userText, { color: theme.text }]}>
26461
- Welcome, <Text style={styles.userName}>{user?.fullName ?? user?.firstName ?? "there"}</Text>
26462
- </Text>
26463
- </View>
26464
- <Text style={[styles.userEmail, { color: theme.text, opacity: 0.7 }]}>
26465
- {user?.emailAddresses[0]?.emailAddress}
26466
- </Text>
27284
+ <Host style={styles.userHeader} matchContents=\\{{ vertical: true }}>
27285
+ <Column spacing={8}>
27286
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 16 }}>
27287
+ {\`Welcome, \${user?.fullName ?? user?.firstName ?? "there"}\`}
27288
+ </ExpoUIText>
27289
+ <ExpoUIText
27290
+ textStyle=\\{{ color: theme.text, fontSize: 14 }}
27291
+ style=\\{{ opacity: 0.7 }}
27292
+ >
27293
+ {user?.emailAddresses[0]?.emailAddress ?? ""}
27294
+ </ExpoUIText>
27295
+ </Column>
27296
+ </Host>
26467
27297
  <SignOutButton />
26468
27298
  </View>
26469
27299
  ) : (
26470
27300
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
26471
- <Link href="/(auth)/sign-in">
26472
- <Text style=\\{{ color: theme.primary }}>Sign in</Text>
26473
- </Link>
26474
- <Link href="/(auth)/sign-up">
26475
- <Text style=\\{{ color: theme.primary }}>Sign up</Text>
26476
- </Link>
27301
+ <Host style={styles.authActionsHost} matchContents=\\{{ vertical: true }}>
27302
+ <Column spacing={8}>
27303
+ <Button
27304
+ label="Sign in"
27305
+ variant="outlined"
27306
+ onPress={() => router.push("/(auth)/sign-in")}
27307
+ />
27308
+ <Button
27309
+ label="Sign up"
27310
+ onPress={() => router.push("/(auth)/sign-up")}
27311
+ />
27312
+ </Column>
27313
+ </Host>
26477
27314
  </View>
26478
27315
  )}
26479
27316
  {{/if}}
26480
27317
 
26481
27318
  {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
26482
- {user ? (
26483
- <View style={[styles.userCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
26484
- <View style={styles.userHeader}>
26485
- <Text style={[styles.userText, { color: theme.text }]}>
26486
- Welcome, <Text style={styles.userName}>{user.name}</Text>
26487
- </Text>
26488
- </View>
26489
- <Text style={[styles.userEmail, { color: theme.text, opacity: 0.7 }]}>
26490
- {user.email}
26491
- </Text>
26492
- <TouchableOpacity style={[styles.signOutButton, { backgroundColor: theme.notification }]} onPress={()=> {
26493
- authClient.signOut();
26494
- }}
26495
- >
26496
- <Text style={styles.signOutText}>Sign Out</Text>
26497
- </TouchableOpacity>
26498
- </View>
26499
- ) : null}
26500
27319
  <View style={[styles.statusCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
26501
- <Text style={[styles.statusCardTitle, { color: theme.text }]}>
26502
- API Status
26503
- </Text>
27320
+ <Host style={styles.statusCardTitleHost} matchContents=\\{{ vertical: true }}>
27321
+ <ExpoUIText
27322
+ textStyle=\\{{ color: theme.text, fontSize: 16, fontWeight: "bold" }}
27323
+ >
27324
+ API Status
27325
+ </ExpoUIText>
27326
+ </Host>
26504
27327
  <View style={styles.statusRow}>
26505
- <View style={[styles.statusIndicator, { backgroundColor: healthCheck ? "#10b981" : "#ef4444" }]} />
26506
- <Text style={[styles.statusText, { color: theme.text, opacity: 0.7 }]}>
26507
- {healthCheck === undefined
26508
- ? "Checking..."
26509
- : healthCheck === "OK"
26510
- ? "Connected to API"
26511
- : "API Disconnected"}
26512
- </Text>
27328
+ <View style={[styles.statusIndicator, { backgroundColor: healthCheck ? "#10b981" : "#f59e0b" }]} />
27329
+ <View style={styles.statusContent}>
27330
+ <Host matchContents=\\{{ vertical: true }}>
27331
+ <ExpoUIText
27332
+ textStyle=\\{{ color: theme.text, fontSize: 12 }}
27333
+ style=\\{{ opacity: 0.7 }}
27334
+ >
27335
+ {healthCheck === undefined
27336
+ ? "Checking..."
27337
+ : healthCheck === "OK"
27338
+ ? "Connected to API"
27339
+ : "API Disconnected"}
27340
+ </ExpoUIText>
27341
+ </Host>
27342
+ </View>
26513
27343
  </View>
26514
27344
  </View>
26515
- {!user && (
27345
+
27346
+ {user ? (
27347
+ <View style={[styles.userCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
27348
+ <Host style={styles.userHeader} matchContents>
27349
+ <Column spacing={6}>
27350
+ <ExpoUIText textStyle=\\{{ color: theme.text, fontSize: 16, fontWeight: "bold" }}>
27351
+ {\`Welcome, \${user.name}\`}
27352
+ </ExpoUIText>
27353
+ <ExpoUIText
27354
+ textStyle=\\{{ color: theme.text, fontSize: 14 }}
27355
+ style=\\{{ opacity: 0.7 }}
27356
+ >
27357
+ {user.email}
27358
+ </ExpoUIText>
27359
+ </Column>
27360
+ </Host>
27361
+ <Host matchContents=\\{{ vertical: true }}>
27362
+ <Button
27363
+ label="Sign Out"
27364
+ variant="outlined"
27365
+ onPress={() => {
27366
+ authClient.signOut();
27367
+ }}
27368
+ />
27369
+ </Host>
27370
+ {{#if (eq payments "polar")}}
27371
+ <Host style={styles.paymentActions} matchContents=\\{{ vertical: true }}>
27372
+ <Column spacing={8}>
27373
+ {subscription ? (
27374
+ <Button
27375
+ label="Manage Subscription"
27376
+ variant="outlined"
27377
+ onPress={handlePolarPortal}
27378
+ />
27379
+ ) : (
27380
+ <Button label="Upgrade to Pro" onPress={handlePolarCheckout} />
27381
+ )}
27382
+ </Column>
27383
+ </Host>
27384
+ {{/if}}
27385
+ </View>
27386
+ ) : (
26516
27387
  <>
26517
27388
  <SignIn />
26518
27389
  <SignUp />
@@ -26530,12 +27401,14 @@ scrollView: {
26530
27401
  flex: 1,
26531
27402
  },
26532
27403
  content: {
26533
- padding: 16,
27404
+ paddingHorizontal: 20,
27405
+ paddingTop: 28,
27406
+ paddingBottom: 32,
26534
27407
  },
26535
- title: {
26536
- fontSize: 24,
26537
- fontWeight: "bold",
26538
- marginBottom: 16,
27408
+ titleHost: {
27409
+ alignSelf: "stretch",
27410
+ height: 34,
27411
+ marginBottom: 24,
26539
27412
  },
26540
27413
  card: {
26541
27414
  padding: 16,
@@ -26548,56 +27421,45 @@ alignItems: "center",
26548
27421
  gap: 8,
26549
27422
  },
26550
27423
  statusIndicator: {
26551
- height: 8,
26552
- width: 8,
27424
+ height: 10,
27425
+ width: 10,
27426
+ borderRadius: 999,
26553
27427
  },
26554
27428
  statusContent: {
26555
27429
  flex: 1,
26556
27430
  },
26557
- statusTitle: {
26558
- fontSize: 14,
26559
- fontWeight: "bold",
26560
- },
26561
- statusText: {
26562
- fontSize: 12,
26563
- },
26564
27431
  userCard: {
26565
27432
  marginBottom: 16,
26566
27433
  padding: 16,
26567
27434
  borderWidth: 1,
27435
+ borderRadius: 16,
26568
27436
  },
26569
27437
  userHeader: {
26570
27438
  marginBottom: 8,
26571
27439
  },
26572
- userText: {
26573
- fontSize: 16,
27440
+ paymentActions: {
27441
+ marginTop: 12,
26574
27442
  },
26575
- userName: {
26576
- fontWeight: "bold",
26577
- },
26578
- userEmail: {
26579
- fontSize: 14,
27443
+ authHost: {
26580
27444
  marginBottom: 12,
26581
27445
  },
26582
- signOutButton: {
26583
- padding: 12,
26584
- },
26585
- signOutText: {
26586
- color: "#ffffff",
27446
+ authActionsHost: {
27447
+ marginTop: 4,
26587
27448
  },
26588
27449
  statusCard: {
26589
27450
  marginBottom: 16,
26590
27451
  padding: 16,
26591
27452
  borderWidth: 1,
27453
+ borderRadius: 16,
26592
27454
  },
26593
- statusCardTitle: {
27455
+ statusCardTitleHost: {
26594
27456
  marginBottom: 8,
26595
- fontWeight: "bold",
26596
27457
  },
26597
27458
  });
26598
27459
  `],
26599
27460
  ["frontend/native/bare/app/+not-found.tsx.hbs", `import { Container } from "@/components/container";
26600
- import { Link, Stack } from "expo-router";
27461
+ import { Button, Column, Host, Text as ExpoUIText } from "@expo/ui";
27462
+ import { Stack, router } from "expo-router";
26601
27463
  import { Text, View, StyleSheet } from "react-native";
26602
27464
  import { useColorScheme } from "@/lib/use-color-scheme";
26603
27465
  import { NAV_THEME } from "@/lib/constants";
@@ -26613,17 +27475,26 @@ export default function NotFoundScreen() {
26613
27475
  <View style={styles.container}>
26614
27476
  <View style={styles.content}>
26615
27477
  <Text style={styles.emoji}>🤔</Text>
26616
- <Text style={[styles.title, { color: theme.text }]}>
26617
- Page Not Found
26618
- </Text>
26619
- <Text style={[styles.subtitle, { color: theme.text, opacity: 0.7 }]}>
26620
- Sorry, the page you're looking for doesn't exist.
26621
- </Text>
26622
- <Link href="/" asChild>
26623
- <Text style={[styles.link, { color: theme.primary, backgroundColor: \`\${theme.primary}1a\` }]}>
26624
- Go to Home
26625
- </Text>
26626
- </Link>
27478
+ <Host matchContents=\\{{ vertical: true }}>
27479
+ <Column spacing={12} alignment="center">
27480
+ <ExpoUIText
27481
+ textStyle=\\{{ color: theme.text, fontSize: 20, fontWeight: "bold", textAlign: "center" }}
27482
+ >
27483
+ Page Not Found
27484
+ </ExpoUIText>
27485
+ <ExpoUIText
27486
+ textStyle=\\{{ color: theme.text, fontSize: 14, textAlign: "center" }}
27487
+ style=\\{{ opacity: 0.7 }}
27488
+ >
27489
+ Sorry, the page you're looking for doesn't exist.
27490
+ </ExpoUIText>
27491
+ <Button
27492
+ label="Go to Home"
27493
+ variant="outlined"
27494
+ onPress={() => router.replace("/")}
27495
+ />
27496
+ </Column>
27497
+ </Host>
26627
27498
  </View>
26628
27499
  </View>
26629
27500
  </Container>
@@ -26645,25 +27516,11 @@ const styles = StyleSheet.create({
26645
27516
  fontSize: 48,
26646
27517
  marginBottom: 16,
26647
27518
  },
26648
- title: {
26649
- fontSize: 20,
26650
- fontWeight: "bold",
26651
- marginBottom: 8,
26652
- textAlign: "center",
26653
- },
26654
- subtitle: {
26655
- fontSize: 14,
26656
- textAlign: "center",
26657
- marginBottom: 24,
26658
- },
26659
- link: {
26660
- padding: 12,
26661
- },
26662
27519
  });
26663
-
26664
27520
  `],
26665
27521
  ["frontend/native/bare/app/modal.tsx.hbs", `import { Container } from "@/components/container";
26666
- import { Text, View, StyleSheet } from "react-native";
27522
+ import { Button, Column, Host, Text as ExpoUIText } from "@expo/ui";
27523
+ import { View, StyleSheet } from "react-native";
26667
27524
  import { useColorScheme } from "@/lib/use-color-scheme";
26668
27525
  import { NAV_THEME } from "@/lib/constants";
26669
27526
 
@@ -26674,9 +27531,22 @@ export default function Modal() {
26674
27531
  return (
26675
27532
  <Container>
26676
27533
  <View style={styles.container}>
26677
- <View style={styles.header}>
26678
- <Text style={[styles.title, { color: theme.text }]}>Modal</Text>
26679
- </View>
27534
+ <Host style={styles.expoUiHost}>
27535
+ <Column spacing={12} alignment="center">
27536
+ <ExpoUIText
27537
+ textStyle=\\{{ color: theme.text, fontSize: 20, fontWeight: "bold" }}
27538
+ >
27539
+ Modal
27540
+ </ExpoUIText>
27541
+ <ExpoUIText
27542
+ textStyle=\\{{ color: theme.text, fontSize: 14, textAlign: "center" }}
27543
+ style=\\{{ opacity: 0.7 }}
27544
+ >
27545
+ Built with Expo UI universal components
27546
+ </ExpoUIText>
27547
+ <Button label="Native control" onPress={() => null} />
27548
+ </Column>
27549
+ </Host>
26680
27550
  </View>
26681
27551
  </Container>
26682
27552
  );
@@ -26687,15 +27557,11 @@ const styles = StyleSheet.create({
26687
27557
  flex: 1,
26688
27558
  padding: 16,
26689
27559
  },
26690
- header: {
26691
- marginBottom: 16,
26692
- },
26693
- title: {
26694
- fontSize: 20,
26695
- fontWeight: "bold",
27560
+ expoUiHost: {
27561
+ alignSelf: "stretch",
27562
+ padding: 16,
26696
27563
  },
26697
27564
  });
26698
-
26699
27565
  `],
26700
27566
  ["frontend/native/bare/components/container.tsx.hbs", `import React from "react";
26701
27567
  import { SafeAreaView } from "react-native-safe-area-context";
@@ -26710,7 +27576,10 @@ export function Container({ children }: { children: React.ReactNode }) {
26710
27576
  : NAV_THEME.light.background;
26711
27577
 
26712
27578
  return (
26713
- <SafeAreaView style={[styles.container, { backgroundColor }]}>
27579
+ <SafeAreaView
27580
+ edges={["left", "right", "bottom"]}
27581
+ style={[styles.container, { backgroundColor }]}
27582
+ >
26714
27583
  {children}
26715
27584
  </SafeAreaView>
26716
27585
  );
@@ -26721,7 +27590,6 @@ const styles = StyleSheet.create({
26721
27590
  flex: 1,
26722
27591
  },
26723
27592
  });
26724
-
26725
27593
  `],
26726
27594
  ["frontend/native/bare/components/header-button.tsx.hbs", `import FontAwesome from "@expo/vector-icons/FontAwesome";
26727
27595
  import { forwardRef } from "react";
@@ -26773,13 +27641,14 @@ const styles = StyleSheet.create({
26773
27641
  `],
26774
27642
  ["frontend/native/bare/components/tabbar-icon.tsx.hbs", `import FontAwesome from "@expo/vector-icons/FontAwesome";
26775
27643
 
27644
+ type FontAwesomeProps = React.ComponentProps<typeof FontAwesome>;
27645
+
26776
27646
  export const TabBarIcon = (props: {
26777
- name: React.ComponentProps<typeof FontAwesome>["name"];
26778
- color: string;
27647
+ name: FontAwesomeProps["name"];
27648
+ color: FontAwesomeProps["color"];
26779
27649
  }) => {
26780
27650
  return <FontAwesome size={24} style=\\{{ marginBottom: -3 }} {...props} />;
26781
27651
  };
26782
-
26783
27652
  `],
26784
27653
  ["frontend/native/bare/lib/constants.ts.hbs", `export const NAV_THEME = {
26785
27654
  light: {
@@ -26841,41 +27710,39 @@ module.exports = config;
26841
27710
  "web": "expo start --web"
26842
27711
  },
26843
27712
  "dependencies": {
27713
+ "@expo/ui": "~56.0.12",
26844
27714
  "@expo/vector-icons": "^15.1.1",
26845
- "@react-navigation/bottom-tabs": "^7.15.9",
26846
- "@react-navigation/drawer": "^7.9.4",
26847
- "@react-navigation/native": "^7.2.2",
26848
27715
  "@tanstack/react-query": "^5.99.2",
26849
27716
  {{#if (includes examples "ai")}}
26850
27717
  "@stardazed/streams-text-encoding": "^1.0.2",
26851
27718
  "@ungap/structured-clone": "^1.3.0",
26852
27719
  {{/if}}
26853
- "expo": "^55.0.17",
26854
- "expo-constants": "~55.0.15",
26855
- "expo-crypto": "~55.0.14",
26856
- "expo-font": "~55.0.6",
26857
- "expo-linking": "~55.0.14",
26858
- "expo-network": "~55.0.13",
26859
- "expo-router": "~55.0.13",
26860
- "expo-secure-store": "~55.0.13",
26861
- "expo-splash-screen": "~55.0.19",
26862
- "expo-status-bar": "~55.0.5",
26863
- "expo-system-ui": "~55.0.16",
26864
- "expo-web-browser": "~55.0.14",
26865
- "react": "19.2.0",
26866
- "react-dom": "19.2.0",
26867
- "react-native": "0.83.6",
26868
- "react-native-gesture-handler": "~2.30.0",
26869
- "react-native-reanimated": "4.2.1",
26870
- "react-native-safe-area-context": "~5.6.2",
26871
- "react-native-screens": "~4.23.0",
27720
+ "expo": "~56.0.3",
27721
+ "expo-constants": "~56.0.14",
27722
+ "expo-crypto": "~56.0.3",
27723
+ "expo-font": "~56.0.5",
27724
+ "expo-linking": "~56.0.11",
27725
+ "expo-network": "~56.0.4",
27726
+ "expo-router": "~56.2.5",
27727
+ "expo-secure-store": "~56.0.4",
27728
+ "expo-splash-screen": "~56.0.9",
27729
+ "expo-status-bar": "~56.0.4",
27730
+ "expo-system-ui": "~56.0.5",
27731
+ "expo-web-browser": "~56.0.5",
27732
+ "react": "19.2.3",
27733
+ "react-dom": "19.2.3",
27734
+ "react-native": "0.85.3",
27735
+ "react-native-gesture-handler": "~2.31.1",
27736
+ "react-native-reanimated": "4.3.1",
27737
+ "react-native-safe-area-context": "~5.7.0",
27738
+ "react-native-screens": "4.25.2",
26872
27739
  "react-native-web": "~0.21.0",
26873
- "react-native-worklets": "0.7.4"
27740
+ "react-native-worklets": "0.8.3"
26874
27741
  },
26875
27742
  "devDependencies": {
26876
- "@babel/core": "^7.28.0",
26877
- "@types/react": "~19.2.10",
26878
- "typescript": "~5.9.2"
27743
+ "@babel/core": "^7.29.0",
27744
+ "@types/react": "~19.2.14",
27745
+ "typescript": "^6"
26879
27746
  },
26880
27747
  "private": true
26881
27748
  }
@@ -27023,9 +27890,6 @@ export const unstable_settings = {
27023
27890
 
27024
27891
  {{#if (eq backend "convex")}}
27025
27892
  const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
27026
- {{#if (eq auth "better-auth")}}
27027
- expectAuth: true,
27028
- {{/if}}
27029
27893
  unsavedChangesWarning: false,
27030
27894
  });
27031
27895
  {{/if}}
@@ -27430,7 +28294,12 @@ const styles = StyleSheet.create((theme) => ({
27430
28294
  },
27431
28295
  }));
27432
28296
  `],
27433
- ["frontend/native/unistyles/app/(drawer)/index.tsx.hbs", `import { ScrollView, Text, View, TouchableOpacity } from "react-native";
28297
+ ["frontend/native/unistyles/app/(drawer)/index.tsx.hbs", `import { ScrollView, Text, View, TouchableOpacity{{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}, Alert{{/if}} } from "react-native";
28298
+ {{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}
28299
+ import * as Linking from "expo-linking";
28300
+ import * as WebBrowser from "expo-web-browser";
28301
+ import { env } from "@{{projectName}}/env/native";
28302
+ {{/if}}
27434
28303
  import { StyleSheet } from "react-native-unistyles";
27435
28304
  import { Container } from "@/components/container";
27436
28305
 
@@ -27453,7 +28322,7 @@ import { Link } from "expo-router";
27453
28322
  import { useAuth, useUser } from "@clerk/expo";
27454
28323
  import { SignOutButton } from "@/components/sign-out-button";
27455
28324
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
27456
- import { useConvexAuth, useQuery } from "convex/react";
28325
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
27457
28326
  import { api } from "@{{ projectName }}/backend/convex/_generated/api";
27458
28327
  import { authClient } from "@/lib/auth-client";
27459
28328
  import { SignIn } from "@/components/sign-in";
@@ -27481,6 +28350,57 @@ export default function Home() {
27481
28350
  const healthCheck = useQuery(api.healthCheck.get);
27482
28351
  const { isAuthenticated } = useConvexAuth();
27483
28352
  const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
28353
+ {{#if (eq payments "polar")}}
28354
+ const products = useQuery(api.polar.listAllProducts);
28355
+ const subscription = useQuery(api.polar.getCurrentSubscription);
28356
+ const generateCheckoutLink = useAction(api.polar.generateCheckoutLink);
28357
+ const generateCustomerPortalUrl = useAction(api.polar.generateCustomerPortalUrl);
28358
+ const recurringProduct = products?.find((product) => product.isRecurring);
28359
+
28360
+ const openPolarLink = async (url: string, returnUrl: string) => {
28361
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
28362
+ };
28363
+
28364
+ const getPolarReturnUrl = (returnUrl: string) => {
28365
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_CONVEX_SITE_URL);
28366
+ url.searchParams.set("returnUrl", returnUrl);
28367
+ return url.toString();
28368
+ };
28369
+
28370
+ const handlePolarCheckout = async () => {
28371
+ try {
28372
+ if (!recurringProduct) {
28373
+ Alert.alert("Checkout unavailable", "No recurring Polar product is available yet.");
28374
+ return;
28375
+ }
28376
+
28377
+ const returnUrl = Linking.createURL("/");
28378
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
28379
+ const { url } = await generateCheckoutLink({
28380
+ productIds: [recurringProduct.id],
28381
+ origin: env.EXPO_PUBLIC_CONVEX_SITE_URL,
28382
+ successUrl: polarReturnUrl,
28383
+ });
28384
+
28385
+ await openPolarLink(url, returnUrl);
28386
+ } catch {
28387
+ Alert.alert("Checkout failed", "Unable to open Polar checkout. Please try again.");
28388
+ }
28389
+ };
28390
+
28391
+ const handlePolarPortal = async () => {
28392
+ try {
28393
+ const returnUrl = Linking.createURL("/");
28394
+ const { url } = await generateCustomerPortalUrl({
28395
+ returnUrl: getPolarReturnUrl(returnUrl),
28396
+ });
28397
+
28398
+ await openPolarLink(url, returnUrl);
28399
+ } catch {
28400
+ Alert.alert("Portal unavailable", "Unable to open the customer portal. Please try again.");
28401
+ }
28402
+ };
28403
+ {{/if}}
27484
28404
  {{else if (eq backend "convex")}}
27485
28405
  const healthCheck = useQuery(api.healthCheck.get);
27486
28406
  {{/if}}
@@ -27623,6 +28543,25 @@ export default function Home() {
27623
28543
  >
27624
28544
  <Text style={styles.signOutText}>Sign Out</Text>
27625
28545
  </TouchableOpacity>
28546
+ {{#if (eq payments "polar")}}
28547
+ <View style={styles.paymentActions}>
28548
+ {subscription ? (
28549
+ <TouchableOpacity
28550
+ style={styles.polarSecondaryButton}
28551
+ onPress={handlePolarPortal}
28552
+ >
28553
+ <Text style={styles.polarSecondaryButtonText}>Manage Subscription</Text>
28554
+ </TouchableOpacity>
28555
+ ) : (
28556
+ <TouchableOpacity
28557
+ style={styles.polarPrimaryButton}
28558
+ onPress={handlePolarCheckout}
28559
+ >
28560
+ <Text style={styles.polarPrimaryButtonText}>Upgrade to Pro</Text>
28561
+ </TouchableOpacity>
28562
+ )}
28563
+ </View>
28564
+ {{/if}}
27626
28565
  </View>
27627
28566
  ) : null}
27628
28567
  <View style={styles.apiStatusCard}>
@@ -27775,6 +28714,31 @@ const styles = StyleSheet.create((theme) => ({
27775
28714
  color: theme.colors.destructiveForeground,
27776
28715
  fontWeight: "500",
27777
28716
  },
28717
+ paymentActions: {
28718
+ marginTop: theme.spacing.sm,
28719
+ alignItems: "flex-start",
28720
+ },
28721
+ polarPrimaryButton: {
28722
+ backgroundColor: theme.colors.primary,
28723
+ paddingVertical: theme.spacing.sm,
28724
+ paddingHorizontal: theme.spacing.md,
28725
+ borderRadius: theme.borderRadius.md,
28726
+ },
28727
+ polarPrimaryButtonText: {
28728
+ color: theme.colors.primaryForeground,
28729
+ fontWeight: "500",
28730
+ },
28731
+ polarSecondaryButton: {
28732
+ borderWidth: 1,
28733
+ borderColor: theme.colors.border,
28734
+ paddingVertical: theme.spacing.sm,
28735
+ paddingHorizontal: theme.spacing.md,
28736
+ borderRadius: theme.borderRadius.md,
28737
+ },
28738
+ polarSecondaryButtonText: {
28739
+ color: theme.colors.foreground,
28740
+ fontWeight: "500",
28741
+ },
27778
28742
  apiStatusCard: {
27779
28743
  marginBottom: theme.spacing.md,
27780
28744
  borderRadius: theme.borderRadius.lg,
@@ -28017,9 +28981,11 @@ const styles = StyleSheet.create((theme) => ({
28017
28981
  `],
28018
28982
  ["frontend/native/unistyles/components/tabbar-icon.tsx.hbs", `import FontAwesome from "@expo/vector-icons/FontAwesome";
28019
28983
 
28984
+ type FontAwesomeProps = React.ComponentProps<typeof FontAwesome>;
28985
+
28020
28986
  export const TabBarIcon = (props: {
28021
- name: React.ComponentProps<typeof FontAwesome>["name"];
28022
- color: string;
28987
+ name: FontAwesomeProps["name"];
28988
+ color: FontAwesomeProps["color"];
28023
28989
  }) => {
28024
28990
  return <FontAwesome size={24} style=\\{{ marginBottom: -3 }} {...props} />;
28025
28991
  };
@@ -28046,45 +29012,42 @@ module.exports = config;
28046
29012
  },
28047
29013
  "dependencies": {
28048
29014
  "@expo/vector-icons": "^15.1.1",
28049
- "@react-navigation/bottom-tabs": "^7.15.9",
28050
- "@react-navigation/drawer": "^7.9.4",
28051
- "@react-navigation/native": "^7.2.2",
28052
29015
  {{#if (includes examples "ai")}}
28053
29016
  "@stardazed/streams-text-encoding": "^1.0.2",
28054
29017
  "@ungap/structured-clone": "^1.3.0",
28055
29018
  {{/if}}
28056
- "babel-preset-expo": "~55.0.18",
28057
- "expo": "^55.0.17",
28058
- "expo-constants": "~55.0.15",
28059
- "expo-crypto": "~55.0.14",
28060
- "expo-dev-client": "~55.0.28",
28061
- "expo-font": "~55.0.6",
28062
- "expo-linking": "~55.0.14",
28063
- "expo-network": "~55.0.13",
28064
- "expo-router": "~55.0.13",
28065
- "expo-secure-store": "~55.0.13",
28066
- "expo-splash-screen": "~55.0.19",
28067
- "expo-status-bar": "~55.0.5",
28068
- "expo-system-ui": "~55.0.16",
28069
- "expo-web-browser": "~55.0.14",
28070
- "react": "19.2.0",
28071
- "react-dom": "19.2.0",
28072
- "react-native": "0.83.6",
29019
+ "babel-preset-expo": "~56.0.0",
29020
+ "expo": "~56.0.3",
29021
+ "expo-constants": "~56.0.14",
29022
+ "expo-crypto": "~56.0.3",
29023
+ "expo-dev-client": "~56.0.14",
29024
+ "expo-font": "~56.0.5",
29025
+ "expo-linking": "~56.0.11",
29026
+ "expo-network": "~56.0.4",
29027
+ "expo-router": "~56.2.5",
29028
+ "expo-secure-store": "~56.0.4",
29029
+ "expo-splash-screen": "~56.0.9",
29030
+ "expo-status-bar": "~56.0.4",
29031
+ "expo-system-ui": "~56.0.5",
29032
+ "expo-web-browser": "~56.0.5",
29033
+ "react": "19.2.3",
29034
+ "react-dom": "19.2.3",
29035
+ "react-native": "0.85.3",
28073
29036
  "react-native-edge-to-edge": "^1.8.1",
28074
- "react-native-gesture-handler": "~2.30.0",
28075
- "react-native-nitro-modules": "^0.35.4",
28076
- "react-native-reanimated": "4.2.1",
28077
- "react-native-safe-area-context": "~5.6.2",
28078
- "react-native-screens": "~4.23.0",
28079
- "react-native-unistyles": "^3.2.3",
29037
+ "react-native-gesture-handler": "~2.31.1",
29038
+ "react-native-nitro-modules": "^0.35.7",
29039
+ "react-native-reanimated": "4.3.1",
29040
+ "react-native-safe-area-context": "~5.7.0",
29041
+ "react-native-screens": "4.25.2",
29042
+ "react-native-unistyles": "^3.2.4",
28080
29043
  "react-native-web": "~0.21.0",
28081
- "react-native-worklets": "0.7.4"
29044
+ "react-native-worklets": "0.8.3"
28082
29045
  },
28083
29046
  "devDependencies": {
28084
- "ajv": "^8.17.1",
28085
- "@babel/core": "^7.28.0",
28086
- "@types/react": "~19.2.10",
28087
- "typescript": "~5.9.2"
29047
+ "ajv": "^8.20.0",
29048
+ "@babel/core": "^7.29.0",
29049
+ "@types/react": "~19.2.14",
29050
+ "typescript": "^6"
28088
29051
  }
28089
29052
  }
28090
29053
  `],
@@ -28194,7 +29157,7 @@ export const darkTheme = {
28194
29157
  "strict": true,
28195
29158
  "jsx": "react-jsx",
28196
29159
  "paths": {
28197
- "@/*": ["*"]
29160
+ "@/*": ["./*"]
28198
29161
  }
28199
29162
  },
28200
29163
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
@@ -28326,9 +29289,6 @@ export const unstable_settings = {
28326
29289
 
28327
29290
  {{#if (eq backend "convex")}}
28328
29291
  const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
28329
- {{#if (eq auth "better-auth")}}
28330
- expectAuth: true,
28331
- {{/if}}
28332
29292
  unsavedChangesWarning: false,
28333
29293
  });
28334
29294
  {{/if}}
@@ -28580,7 +29540,7 @@ export default function TabLayout() {
28580
29540
  name="index"
28581
29541
  options=\\{{
28582
29542
  title: "Home",
28583
- tabBarIcon: ({ color, size }: { color: string; size: number }) => (
29543
+ tabBarIcon: ({ color, size }) => (
28584
29544
  <Ionicons name="home" size={size} color={color} />
28585
29545
  ),
28586
29546
  }}
@@ -28589,7 +29549,7 @@ export default function TabLayout() {
28589
29549
  name="two"
28590
29550
  options=\\{{
28591
29551
  title: "Explore",
28592
- tabBarIcon: ({ color, size }: { color: string; size: number }) => (
29552
+ tabBarIcon: ({ color, size }) => (
28593
29553
  <Ionicons name="compass" size={size} color={color} />
28594
29554
  ),
28595
29555
  }}
@@ -28630,7 +29590,12 @@ export default function TabTwo() {
28630
29590
  );
28631
29591
  }
28632
29592
  `],
28633
- ["frontend/native/uniwind/app/(drawer)/index.tsx.hbs", `import { Text, View } from "react-native";
29593
+ ["frontend/native/uniwind/app/(drawer)/index.tsx.hbs", `import { Text, View{{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}, Alert{{/if}} } from "react-native";
29594
+ {{#if (and (eq backend "convex") (eq auth "better-auth") (eq payments "polar"))}}
29595
+ import * as Linking from "expo-linking";
29596
+ import * as WebBrowser from "expo-web-browser";
29597
+ import { env } from "@{{projectName}}/env/native";
29598
+ {{/if}}
28634
29599
  import { Container } from "@/components/container";
28635
29600
  {{#if (eq api "orpc")}}
28636
29601
  import { useQuery } from "@tanstack/react-query";
@@ -28651,7 +29616,7 @@ import { Link } from "expo-router";
28651
29616
  import { useAuth, useUser } from "@clerk/expo";
28652
29617
  import { SignOutButton } from "@/components/sign-out-button";
28653
29618
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
28654
- import { useConvexAuth, useQuery } from "convex/react";
29619
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
28655
29620
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
28656
29621
  import { authClient } from "@/lib/auth-client";
28657
29622
  import { SignIn } from "@/components/sign-in";
@@ -28683,6 +29648,57 @@ const { user } = useUser();
28683
29648
  const healthCheck = useQuery(api.healthCheck.get);
28684
29649
  const { isAuthenticated } = useConvexAuth();
28685
29650
  const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
29651
+ {{#if (eq payments "polar")}}
29652
+ const products = useQuery(api.polar.listAllProducts);
29653
+ const subscription = useQuery(api.polar.getCurrentSubscription);
29654
+ const generateCheckoutLink = useAction(api.polar.generateCheckoutLink);
29655
+ const generateCustomerPortalUrl = useAction(api.polar.generateCustomerPortalUrl);
29656
+ const recurringProduct = products?.find((product) => product.isRecurring);
29657
+
29658
+ const openPolarLink = async (url: string, returnUrl: string) => {
29659
+ await WebBrowser.openAuthSessionAsync(url, returnUrl);
29660
+ };
29661
+
29662
+ const getPolarReturnUrl = (returnUrl: string) => {
29663
+ const url = new URL("/polar/success", env.EXPO_PUBLIC_CONVEX_SITE_URL);
29664
+ url.searchParams.set("returnUrl", returnUrl);
29665
+ return url.toString();
29666
+ };
29667
+
29668
+ const handlePolarCheckout = async () => {
29669
+ try {
29670
+ if (!recurringProduct) {
29671
+ Alert.alert("Checkout unavailable", "No recurring Polar product is available yet.");
29672
+ return;
29673
+ }
29674
+
29675
+ const returnUrl = Linking.createURL("/");
29676
+ const polarReturnUrl = getPolarReturnUrl(returnUrl);
29677
+ const { url } = await generateCheckoutLink({
29678
+ productIds: [recurringProduct.id],
29679
+ origin: env.EXPO_PUBLIC_CONVEX_SITE_URL,
29680
+ successUrl: polarReturnUrl,
29681
+ });
29682
+
29683
+ await openPolarLink(url, returnUrl);
29684
+ } catch {
29685
+ Alert.alert("Checkout failed", "Unable to open Polar checkout. Please try again.");
29686
+ }
29687
+ };
29688
+
29689
+ const handlePolarPortal = async () => {
29690
+ try {
29691
+ const returnUrl = Linking.createURL("/");
29692
+ const { url } = await generateCustomerPortalUrl({
29693
+ returnUrl: getPolarReturnUrl(returnUrl),
29694
+ });
29695
+
29696
+ await openPolarLink(url, returnUrl);
29697
+ } catch {
29698
+ Alert.alert("Portal unavailable", "Unable to open the customer portal. Please try again.");
29699
+ }
29700
+ };
29701
+ {{/if}}
28686
29702
  {{else if (eq backend "convex")}}
28687
29703
  const healthCheck = useQuery(api.healthCheck.get);
28688
29704
  {{/if}}
@@ -28834,6 +29850,19 @@ return (
28834
29850
  Sign Out
28835
29851
  </Button>
28836
29852
  </View>
29853
+ {{#if (eq payments "polar")}}
29854
+ <View className="mt-4 gap-3">
29855
+ {subscription ? (
29856
+ <Button variant="secondary" onPress={handlePolarPortal}>
29857
+ Manage Subscription
29858
+ </Button>
29859
+ ) : (
29860
+ <Button onPress={handlePolarCheckout}>
29861
+ Upgrade to Pro
29862
+ </Button>
29863
+ )}
29864
+ </View>
29865
+ {{/if}}
28837
29866
  </Surface>
28838
29867
  ) : null}
28839
29868
  <Surface variant="secondary" className="p-4 rounded-xl">
@@ -29106,46 +30135,44 @@ module.exports = uniwindConfig;
29106
30135
  "web": "expo start --web"
29107
30136
  },
29108
30137
  "dependencies": {
29109
- "@expo/metro-runtime": "~55.0.10",
30138
+ "@expo/metro-runtime": "~56.0.11",
29110
30139
  "@expo/vector-icons": "^15.1.1",
29111
- "@gorhom/bottom-sheet": "^5.2.10",
29112
- "@react-navigation/drawer": "^7.9.4",
29113
- "@react-navigation/elements": "^2.9.14",
30140
+ "@gorhom/bottom-sheet": "^5.2.14",
29114
30141
  {{#if (includes examples "ai")}}
29115
30142
  "@stardazed/streams-text-encoding": "^1.0.2",
29116
30143
  "@ungap/structured-clone": "^1.3.0",
29117
30144
  {{/if}}
29118
- "expo": "^55.0.17",
29119
- "expo-constants": "~55.0.15",
29120
- "expo-font": "~55.0.6",
29121
- "expo-haptics": "~55.0.14",
29122
- "expo-linking": "~55.0.14",
29123
- "expo-network": "~55.0.13",
29124
- "expo-router": "~55.0.13",
29125
- "expo-secure-store": "~55.0.13",
29126
- "expo-status-bar": "~55.0.5",
29127
- "expo-web-browser": "~55.0.14",
29128
- "heroui-native": "^1.0.2",
29129
- "react": "19.2.0",
29130
- "react-dom": "19.2.0",
29131
- "react-native": "0.83.6",
29132
- "react-native-gesture-handler": "~2.30.0",
29133
- "react-native-keyboard-controller": "1.20.7",
29134
- "react-native-reanimated": "4.2.1",
29135
- "react-native-safe-area-context": "~5.6.2",
29136
- "react-native-screens": "~4.23.0",
29137
- "react-native-svg": "15.15.3",
30145
+ "expo": "~56.0.3",
30146
+ "expo-constants": "~56.0.14",
30147
+ "expo-font": "~56.0.5",
30148
+ "expo-haptics": "~56.0.3",
30149
+ "expo-linking": "~56.0.11",
30150
+ "expo-network": "~56.0.4",
30151
+ "expo-router": "~56.2.5",
30152
+ "expo-secure-store": "~56.0.4",
30153
+ "expo-status-bar": "~56.0.4",
30154
+ "expo-web-browser": "~56.0.5",
30155
+ "heroui-native": "^1.0.3",
30156
+ "react": "19.2.3",
30157
+ "react-dom": "19.2.3",
30158
+ "react-native": "0.85.3",
30159
+ "react-native-gesture-handler": "~2.31.1",
30160
+ "react-native-keyboard-controller": "1.21.6",
30161
+ "react-native-reanimated": "4.3.1",
30162
+ "react-native-safe-area-context": "~5.7.0",
30163
+ "react-native-screens": "4.25.2",
30164
+ "react-native-svg": "15.15.4",
29138
30165
  "react-native-web": "~0.21.0",
29139
- "react-native-worklets": "0.7.4",
29140
- "tailwind-merge": "^3.5.0",
30166
+ "react-native-worklets": "0.8.3",
30167
+ "tailwind-merge": "^3.6.0",
29141
30168
  "tailwind-variants": "^3.2.2",
29142
- "tailwindcss": "^4.2.4",
29143
- "uniwind": "^1.6.3"
30169
+ "tailwindcss": "^4.3.0",
30170
+ "uniwind": "^1.7.0"
29144
30171
  },
29145
30172
  "devDependencies": {
29146
- "@types/node": "^24.10.0",
29147
- "@types/react": "~19.2.10",
29148
- "typescript": "~5.9.2"
30173
+ "@types/node": "^25.9.1",
30174
+ "@types/react": "~19.2.14",
30175
+ "typescript": "^6"
29149
30176
  }
29150
30177
  }
29151
30178
  `],
@@ -29165,6 +30192,8 @@ module.exports = uniwindConfig;
29165
30192
  ]
29166
30193
  }`],
29167
30194
  ["frontend/native/uniwind/uniwind-env.d.ts", `/// <reference types="uniwind/types" />
30195
+
30196
+ declare module "*.css";
29168
30197
  `],
29169
30198
  ["frontend/nuxt/_gitignore", `# Nuxt dev/build outputs
29170
30199
  .output
@@ -29505,15 +30534,15 @@ initOpenNextCloudflareForDev();
29505
30534
  "lucide-react": "^0.546.0",
29506
30535
  "next": "^16.2.0",
29507
30536
  "next-themes": "^0.4.6",
29508
- "react": "^19.2.3",
29509
- "react-dom": "^19.2.3",
30537
+ "react": "^19.2.6",
30538
+ "react-dom": "^19.2.6",
29510
30539
  "sonner": "^2.0.5",
29511
30540
  "babel-plugin-react-compiler": "^1.0.0"
29512
30541
  },
29513
30542
  "devDependencies": {
29514
30543
  "@tailwindcss/postcss": "^4.1.18",
29515
30544
  "@types/node": "^20",
29516
- "@types/react": "^19.2.10",
30545
+ "@types/react": "^19.2.15",
29517
30546
  "@types/react-dom": "^19.2.3",
29518
30547
  "tailwindcss": "^4.1.18"
29519
30548
  }
@@ -29910,8 +30939,8 @@ export function ThemeProvider({
29910
30939
  "isbot": "^5.1.39",
29911
30940
  "lucide-react": "^1.8.0",
29912
30941
  "next-themes": "^0.4.6",
29913
- "react": "^19.2.5",
29914
- "react-dom": "^19.2.5",
30942
+ "react": "^19.2.6",
30943
+ "react-dom": "^19.2.6",
29915
30944
  "react-router": "^7.14.1",
29916
30945
  "sonner": "^2.0.7"
29917
30946
  },
@@ -29919,7 +30948,7 @@ export function ThemeProvider({
29919
30948
  "@react-router/dev": "^7.14.1",
29920
30949
  "@tailwindcss/vite": "^4.2.2",
29921
30950
  "@types/node": "^20",
29922
- "@types/react": "^19.2.14",
30951
+ "@types/react": "^19.2.15",
29923
30952
  "@types/react-dom": "^19.2.3",
29924
30953
  "react-router-devtools": "^1.1.0",
29925
30954
  "tailwindcss": "^4.2.2",
@@ -30085,13 +31114,7 @@ export default function App() {
30085
31114
  {{else}}
30086
31115
  export default function App() {
30087
31116
  {{/if}}
30088
- {{#if (eq auth "better-auth")}}
30089
- const convex = new ConvexReactClient(env.VITE_CONVEX_URL, {
30090
- expectAuth: true,
30091
- });
30092
- {{else}}
30093
31117
  const convex = new ConvexReactClient(env.VITE_CONVEX_URL);
30094
- {{/if}}
30095
31118
  {{#if (eq auth "clerk")}}
30096
31119
  return (
30097
31120
  <ClerkProvider loaderData={loaderData}>
@@ -30453,15 +31476,15 @@ export default defineConfig({
30453
31476
  "@tanstack/react-router": "^1.168.22",
30454
31477
  "lucide-react": "^1.8.0",
30455
31478
  "next-themes": "^0.4.6",
30456
- "react": "^19.2.5",
30457
- "react-dom": "^19.2.5",
31479
+ "react": "^19.2.6",
31480
+ "react-dom": "^19.2.6",
30458
31481
  "sonner": "^2.0.7"
30459
31482
  },
30460
31483
  "devDependencies": {
30461
31484
  "@tanstack/react-router-devtools": "^1.166.13",
30462
31485
  "@tanstack/router-plugin": "^1.167.22",
30463
31486
  "@types/node": "^22.13.14",
30464
- "@types/react": "^19.2.14",
31487
+ "@types/react": "^19.2.15",
30465
31488
  "@types/react-dom": "^19.2.3",
30466
31489
  "@vitejs/plugin-react": "^6.0.1",
30467
31490
  "postcss": "^8.5.10",
@@ -30545,13 +31568,7 @@ import { routeTree } from "./routeTree.gen";
30545
31568
  {{else}}
30546
31569
  import { ConvexProvider } from "convex/react";
30547
31570
  {{/if}}
30548
- {{#if (eq auth "better-auth")}}
30549
- const convex = new ConvexReactClient(env.VITE_CONVEX_URL, {
30550
- expectAuth: true,
30551
- });
30552
- {{else}}
30553
31571
  const convex = new ConvexReactClient(env.VITE_CONVEX_URL);
30554
- {{/if}}
30555
31572
  {{/if}}
30556
31573
 
30557
31574
  {{#if (and (eq auth "clerk") (ne backend "convex") (ne api "none"))}}
@@ -30904,8 +31921,8 @@ export default defineConfig({
30904
31921
  "@tanstack/react-start": "^1.167.41",
30905
31922
  "lucide-react": "^1.8.0",
30906
31923
  "next-themes": "^0.4.6",
30907
- "react": "^19.2.5",
30908
- "react-dom": "^19.2.5",
31924
+ "react": "^19.2.6",
31925
+ "react-dom": "^19.2.6",
30909
31926
  "sonner": "^2.0.7",
30910
31927
  "tailwindcss": "^4.2.2"
30911
31928
  },
@@ -30913,7 +31930,7 @@ export default defineConfig({
30913
31930
  "@tanstack/react-router-devtools": "^1.166.13",
30914
31931
  "@testing-library/dom": "^10.4.1",
30915
31932
  "@testing-library/react": "^16.3.2",
30916
- "@types/react": "^19.2.14",
31933
+ "@types/react": "^19.2.15",
30917
31934
  "@types/react-dom": "^19.2.3",
30918
31935
  "@vitejs/plugin-react": "^6.0.1",
30919
31936
  "jsdom": "^29.0.2",
@@ -30933,12 +31950,10 @@ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query
30933
31950
  import { ConvexQueryClient } from "@convex-dev/react-query";
30934
31951
  import { routeTree } from "./routeTree.gen";
30935
31952
  import Loader from "./components/loader";
30936
- import "./index.css";
30937
31953
  import { env } from "@{{projectName}}/env/web";
30938
31954
  {{else}}
30939
31955
  import { createRouter as createTanStackRouter } from "@tanstack/react-router";
30940
31956
  import Loader from "./components/loader";
30941
- import "./index.css";
30942
31957
  import { routeTree } from "./routeTree.gen";
30943
31958
  {{#if (eq api "trpc")}}
30944
31959
  import { QueryCache, QueryClient } from "@tanstack/react-query";
@@ -33106,14 +34121,14 @@ await app.finalize();
33106
34121
  "clsx": "^2.1.1",
33107
34122
  "lucide-react": "^0.546.0",
33108
34123
  "next-themes": "^0.4.6",
33109
- "react": "^19.2.3",
33110
- "react-dom": "^19.2.3",
34124
+ "react": "^19.2.6",
34125
+ "react-dom": "^19.2.6",
33111
34126
  "sonner": "^2.0.5",
33112
34127
  "tailwind-merge": "^3.3.1",
33113
34128
  "tw-animate-css": "^1.3.4"
33114
34129
  },
33115
34130
  "devDependencies": {
33116
- "@types/react": "^19.2.10",
34131
+ "@types/react": "^19.2.15",
33117
34132
  "@types/react-dom": "^19.2.3",
33118
34133
  "tailwindcss": "^4.1.18"
33119
34134
  },
@@ -33839,6 +34854,7 @@ export function cn(...inputs: ClassValue[]) {
33839
34854
  "compilerOptions": {
33840
34855
  "jsx": "react-jsx",
33841
34856
  "lib": ["ESNext", "DOM", "DOM.Iterable"],
34857
+ "types": [],
33842
34858
  "paths": {
33843
34859
  "@{{projectName}}/ui/*": ["./src/*"]
33844
34860
  }
@@ -33846,6 +34862,71 @@ export function cn(...inputs: ClassValue[]) {
33846
34862
  "include": ["src/**/*.ts", "src/**/*.tsx"],
33847
34863
  "exclude": ["node_modules"]
33848
34864
  }
34865
+ `],
34866
+ ["payments/polar/convex/backend/convex/polar.ts.hbs", `import { Polar } from "@convex-dev/polar";
34867
+
34868
+ import { api, components } from "./_generated/api";
34869
+ import type { DataModel } from "./_generated/dataModel";
34870
+ import { action, query } from "./_generated/server";
34871
+
34872
+ type CurrentSubscription = Awaited<ReturnType<Polar<DataModel>["getCurrentSubscription"]>>;
34873
+
34874
+ export const polar: Polar<DataModel> = new Polar<DataModel>(components.polar, {
34875
+ getUserInfo: async (ctx) => {
34876
+ const user = await ctx.runQuery(api.auth.getCurrentUser);
34877
+
34878
+ if (!user) {
34879
+ throw new Error("Not authenticated");
34880
+ }
34881
+
34882
+ if (!user.email) {
34883
+ throw new Error("Authenticated user is missing an email address");
34884
+ }
34885
+
34886
+ return {
34887
+ userId: user._id,
34888
+ email: user.email,
34889
+ };
34890
+ },
34891
+ });
34892
+
34893
+ export const {
34894
+ changeCurrentSubscription,
34895
+ cancelCurrentSubscription,
34896
+ getConfiguredProducts,
34897
+ listAllProducts,
34898
+ listAllSubscriptions,
34899
+ generateCheckoutLink,
34900
+ generateCustomerPortalUrl,
34901
+ } = polar.api();
34902
+
34903
+ export const getCurrentSubscription = query({
34904
+ args: {},
34905
+ handler: async (ctx): Promise<CurrentSubscription | null> => {
34906
+ const user = await ctx.runQuery(api.auth.getCurrentUser);
34907
+
34908
+ if (!user) {
34909
+ return null;
34910
+ }
34911
+
34912
+ return await polar.getCurrentSubscription(ctx, {
34913
+ userId: user._id,
34914
+ });
34915
+ },
34916
+ });
34917
+
34918
+ export const syncProducts = action({
34919
+ args: {},
34920
+ handler: async (ctx): Promise<void> => {
34921
+ const user = await ctx.runQuery(api.auth.getCurrentUser);
34922
+
34923
+ if (!user) {
34924
+ throw new Error("Not authenticated");
34925
+ }
34926
+
34927
+ await polar.syncProducts(ctx);
34928
+ },
34929
+ });
33849
34930
  `],
33850
34931
  ["payments/polar/server/base/src/lib/payments.ts.hbs", `import { Polar } from "@polar-sh/sdk";
33851
34932
  {{#if (and (eq backend "self") (eq webDeploy "cloudflare") (includes frontend "svelte"))}}
@@ -34004,7 +35085,7 @@ function SuccessPage() {
34004
35085
  </div>
34005
35086
  `]
34006
35087
  ]);
34007
- const TEMPLATE_COUNT = 467;
35088
+ const TEMPLATE_COUNT = 475;
34008
35089
  //#endregion
34009
35090
  export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processTemplateString, transformFilename, writeBtsConfigToVfs };
34010
35091