@better-t-stack/template-generator 3.29.0 → 3.30.1

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
@@ -775,6 +775,7 @@ const dependencyVersionMap = {
775
775
  convex: "^1.33.1",
776
776
  "@convex-dev/react-query": "^0.1.0",
777
777
  "@convex-dev/agent": "^0.3.2",
778
+ "@convex-dev/polar": "^0.9.1",
778
779
  "convex-svelte": "^0.0.12",
779
780
  "convex-nuxt": "0.1.5",
780
781
  "convex-vue": "^0.1.5",
@@ -808,7 +809,10 @@ const dependencyVersionMap = {
808
809
  "@t3-oss/env-nextjs": "^0.13.1",
809
810
  "@t3-oss/env-nuxt": "^0.13.1",
810
811
  "@polar-sh/better-auth": "^1.8.4",
811
- "@polar-sh/sdk": "^0.42.2",
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",
812
816
  evlog: "^2.14.1"
813
817
  };
814
818
  /**
@@ -2210,7 +2214,7 @@ function buildNativeVars(frontend, backend, auth) {
2210
2214
  });
2211
2215
  return vars;
2212
2216
  }
2213
- function buildConvexBackendVars(frontend, auth, examples) {
2217
+ function buildConvexBackendVars(frontend, auth, payments, examples) {
2214
2218
  const hasReactRouter = frontend.includes("react-router");
2215
2219
  const hasTanStackRouter = frontend.includes("tanstack-router");
2216
2220
  const hasNextJs = frontend.includes("next");
@@ -2255,9 +2259,25 @@ function buildConvexBackendVars(frontend, auth, examples) {
2255
2259
  comment: "Web app URL for authentication (for Expo web support)"
2256
2260
  });
2257
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
+ });
2258
2278
  return vars;
2259
2279
  }
2260
- function buildConvexCommentBlocks(frontend, auth, examples) {
2280
+ function buildConvexCommentBlocks(frontend, auth, payments, examples) {
2261
2281
  const needsConvexSiteUrl = frontend.includes("react-router") || frontend.includes("tanstack-router");
2262
2282
  const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2263
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");
@@ -2270,15 +2290,27 @@ function buildConvexCommentBlocks(frontend, auth, examples) {
2270
2290
  if (auth === "better-auth") commentBlocks += `# Set Convex environment variables
2271
2291
  # npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
2272
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
+ `;
2273
2301
  return commentBlocks;
2274
2302
  }
2275
- 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) {
2276
2304
  const hasReactRouter = frontend.includes("react-router");
2277
2305
  const hasSvelte = frontend.includes("svelte");
2278
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");
2279
2309
  let corsOrigin = "http://localhost:3001";
2280
2310
  if (hasAstro) corsOrigin = "http://localhost:4321";
2281
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}`;
2282
2314
  let databaseUrl = null;
2283
2315
  if (database !== "none" && dbSetup === "none") switch (database) {
2284
2316
  case "postgres":
@@ -2310,7 +2342,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2310
2342
  },
2311
2343
  {
2312
2344
  key: "BETTER_AUTH_URL",
2313
- value: backend === "self" ? hasSvelte ? "http://localhost:5173" : hasAstro ? "http://localhost:4321" : "http://localhost:3001" : "http://localhost:3000",
2345
+ value: betterAuthUrl,
2314
2346
  condition: hasBetterAuth
2315
2347
  },
2316
2348
  {
@@ -2330,7 +2362,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2330
2362
  },
2331
2363
  {
2332
2364
  key: "POLAR_SUCCESS_URL",
2333
- value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
2365
+ value: polarSuccessUrl,
2334
2366
  condition: payments === "polar"
2335
2367
  },
2336
2368
  {
@@ -2351,7 +2383,7 @@ function buildServerVars(backend, frontend, auth, api, database, dbSetup, runtim
2351
2383
  ];
2352
2384
  }
2353
2385
  function processEnvVariables(vfs, config) {
2354
- 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;
2355
2387
  const hasReactRouter = frontend.includes("react-router");
2356
2388
  const hasTanStackRouter = frontend.includes("tanstack-router");
2357
2389
  const hasTanStackStart = frontend.includes("tanstack-start");
@@ -2372,13 +2404,13 @@ function processEnvVariables(vfs, config) {
2372
2404
  const convexBackendDir = "packages/backend";
2373
2405
  if (vfs.directoryExists(convexBackendDir)) {
2374
2406
  const envLocalPath = `${convexBackendDir}/.env.local`;
2375
- const commentBlocks = buildConvexCommentBlocks(frontend, auth, examples);
2407
+ const commentBlocks = buildConvexCommentBlocks(frontend, auth, payments, examples);
2376
2408
  if (commentBlocks) {
2377
2409
  let currentContent = "";
2378
2410
  if (vfs.exists(envLocalPath)) currentContent = vfs.readFile(envLocalPath) || "";
2379
2411
  vfs.writeFile(envLocalPath, commentBlocks + currentContent);
2380
2412
  }
2381
- const convexBackendVars = buildConvexBackendVars(frontend, auth, examples);
2413
+ const convexBackendVars = buildConvexBackendVars(frontend, auth, payments, examples);
2382
2414
  if (convexBackendVars.length > 0) {
2383
2415
  let existingContent = "";
2384
2416
  if (vfs.exists(envLocalPath)) existingContent = vfs.readFile(envLocalPath) || "";
@@ -2388,7 +2420,7 @@ function processEnvVariables(vfs, config) {
2388
2420
  }
2389
2421
  return;
2390
2422
  }
2391
- 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);
2392
2424
  if (backend === "self") {
2393
2425
  const webDir = "apps/web";
2394
2426
  if (vfs.directoryExists(webDir)) writeEnvFile(vfs, `${webDir}/.env`, serverVars);
@@ -2609,11 +2641,37 @@ function getDeployTargets() {
2609
2641
  //#endregion
2610
2642
  //#region src/processors/payments-deps.ts
2611
2643
  function processPaymentsDeps(vfs, config) {
2612
- const { payments, frontend } = config;
2644
+ const { payments, frontend, backend } = config;
2613
2645
  if (!payments || payments === "none") return;
2646
+ const backendPath = "packages/backend/package.json";
2614
2647
  const authPath = "packages/auth/package.json";
2615
2648
  const webPath = "apps/web/package.json";
2616
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
+ }
2617
2675
  if (vfs.exists(authPath)) addPackageDependency({
2618
2676
  vfs,
2619
2677
  packagePath: authPath,
@@ -3728,7 +3786,6 @@ async function processAuthTemplates(vfs, templates, config) {
3728
3786
  //#region src/template-handlers/payments.ts
3729
3787
  async function processPaymentsTemplates(vfs, templates, config) {
3730
3788
  if (!config.payments || config.payments === "none") return;
3731
- if (config.backend === "convex") return;
3732
3789
  const hasReactWeb = config.frontend.some((f) => [
3733
3790
  "tanstack-router",
3734
3791
  "react-router",
@@ -3738,7 +3795,10 @@ async function processPaymentsTemplates(vfs, templates, config) {
3738
3795
  const hasNuxtWeb = config.frontend.includes("nuxt");
3739
3796
  const hasSvelteWeb = config.frontend.includes("svelte");
3740
3797
  const hasSolidWeb = config.frontend.includes("solid");
3741
- 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);
3742
3802
  if (hasReactWeb) {
3743
3803
  const reactFramework = config.frontend.find((f) => [
3744
3804
  "tanstack-router",
@@ -6231,14 +6291,56 @@ export const getCurrentUser = query({
6231
6291
  `],
6232
6292
  ["auth/better-auth/convex/backend/convex/http.ts.hbs", `import { httpRouter } from "convex/server";
6233
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}}
6234
6300
 
6235
6301
  const http = httpRouter();
6236
6302
 
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
+ }),
6332
+ });
6333
+
6334
+ {{/if}}
6237
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"))}}
6238
6336
  authComponent.registerRoutes(http, createAuth, { cors: true });
6239
6337
  {{else}}
6240
6338
  authComponent.registerRoutes(http, createAuth);
6241
6339
  {{/if}}
6340
+ {{#if (eq payments "polar")}}
6341
+
6342
+ polar.registerRoutes(http);
6343
+ {{/if}}
6242
6344
 
6243
6345
  export default http;
6244
6346
  `],
@@ -7559,6 +7661,10 @@ export const { GET, POST } = handler;
7559
7661
  import SignInForm from "@/components/sign-in-form";
7560
7662
  import SignUpForm from "@/components/sign-up-form";
7561
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}}
7562
7668
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
7563
7669
  import {
7564
7670
  Authenticated,
@@ -7568,18 +7674,58 @@ import {
7568
7674
  } from "convex/react";
7569
7675
  import { useState } from "react";
7570
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
+
7571
7722
  export default function DashboardPage() {
7572
7723
  const [showSignIn, setShowSignIn] = useState(false);
7573
- const privateData = useQuery(api.privateData.get);
7574
7724
 
7575
7725
  return (
7576
7726
  <>
7577
7727
  <Authenticated>
7578
- <div>
7579
- <h1>Dashboard</h1>
7580
- <p>privateData: {privateData?.message}</p>
7581
- <UserMenu />
7582
- </div>
7728
+ <DashboardContent />
7583
7729
  </Authenticated>
7584
7730
  <Unauthenticated>
7585
7731
  {showSignIn ? (
@@ -8305,6 +8451,10 @@ export const authClient = createAuthClient({
8305
8451
  ["auth/better-auth/convex/web/react/react-router/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
8306
8452
  import SignUpForm from "@/components/sign-up-form";
8307
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}}
8308
8458
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
8309
8459
  import {
8310
8460
  Authenticated,
@@ -8316,11 +8466,44 @@ import { useState } from "react";
8316
8466
 
8317
8467
  function PrivateDashboardContent() {
8318
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}}
8319
8476
 
8320
8477
  return (
8321
8478
  <div>
8322
8479
  <h1>Dashboard</h1>
8323
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}}
8324
8507
  <UserMenu />
8325
8508
  </div>
8326
8509
  );
@@ -8709,6 +8892,10 @@ export const authClient = createAuthClient({
8709
8892
  ["auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
8710
8893
  import SignUpForm from "@/components/sign-up-form";
8711
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}}
8712
8899
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
8713
8900
  import { createFileRoute } from "@tanstack/react-router";
8714
8901
  import {
@@ -8725,11 +8912,44 @@ export const Route = createFileRoute("/dashboard")({
8725
8912
 
8726
8913
  function PrivateDashboardContent() {
8727
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}}
8728
8922
 
8729
8923
  return (
8730
8924
  <div>
8731
8925
  <h1>Dashboard</h1>
8732
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}}
8733
8953
  <UserMenu />
8734
8954
  </div>
8735
8955
  );
@@ -9133,6 +9353,10 @@ export const Route = createFileRoute("/api/auth/$")({
9133
9353
  ["auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs", `import SignInForm from "@/components/sign-in-form";
9134
9354
  import SignUpForm from "@/components/sign-up-form";
9135
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}}
9136
9360
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
9137
9361
  import { createFileRoute } from "@tanstack/react-router";
9138
9362
  import {
@@ -9147,18 +9371,58 @@ export const Route = createFileRoute("/dashboard")({
9147
9371
  component: RouteComponent,
9148
9372
  });
9149
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
+
9150
9419
  function RouteComponent() {
9151
9420
  const [showSignIn, setShowSignIn] = useState(false);
9152
- const privateData = useQuery(api.privateData.get);
9153
9421
 
9154
9422
  return (
9155
9423
  <>
9156
9424
  <Authenticated>
9157
- <div>
9158
- <h1>Dashboard</h1>
9159
- <p>privateData: {privateData?.message}</p>
9160
- <UserMenu />
9161
- </div>
9425
+ <PrivateDashboardContent />
9162
9426
  </Authenticated>
9163
9427
  <Unauthenticated>
9164
9428
  {showSignIn ? (
@@ -9321,11 +9585,16 @@ export const Route = createFileRoute('/api/auth/$')({
9321
9585
  })
9322
9586
  `],
9323
9587
  ["auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs", `import { Button, Column, Host, Text as ExpoUIText } from "@expo/ui";
9324
- import { View, ScrollView, StyleSheet } from "react-native";
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,19 +9622,59 @@ 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
- <Host style={styles.titleHost} matchContents=\\{{ vertical: true }}>
9362
- <Column>
9363
- <ExpoUIText
9364
- textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold" }}
9365
- >
9366
- BETTER T STACK
9367
- </ExpoUIText>
9368
- </Column>
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>
9369
9678
  </Host>
9370
9679
 
9371
9680
  {session?.user ? (
@@ -9398,6 +9707,18 @@ return (
9398
9707
  }}
9399
9708
  />
9400
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}}
9401
9722
  </View>
9402
9723
  ) : null}
9403
9724
 
@@ -9479,7 +9800,8 @@ paddingTop: 28,
9479
9800
  paddingBottom: 32,
9480
9801
  },
9481
9802
  titleHost: {
9482
- alignSelf: "center",
9803
+ alignSelf: "stretch",
9804
+ height: 34,
9483
9805
  marginBottom: 24,
9484
9806
  },
9485
9807
  userCard: {
@@ -9491,6 +9813,9 @@ borderRadius: 16,
9491
9813
  userHeader: {
9492
9814
  marginBottom: 8,
9493
9815
  },
9816
+ paymentActions: {
9817
+ marginTop: 12,
9818
+ },
9494
9819
  statusCard: {
9495
9820
  marginBottom: 16,
9496
9821
  padding: 16,
@@ -10038,9 +10363,41 @@ export const authClient = createAuthClient({
10038
10363
  }),
10039
10364
  ],
10040
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}}
10041
10393
  `],
10042
- ["auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs", `import { authClient } from "@/lib/auth-client";
10043
- 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}}
10044
10401
  import { StyleSheet } from "react-native-unistyles";
10045
10402
 
10046
10403
  import { Container } from "@/components/container";
@@ -10065,6 +10422,48 @@ export default function Home() {
10065
10422
  const privateData = useQuery(trpc.privateData.queryOptions());
10066
10423
  {{/if}}
10067
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}}
10068
10467
 
10069
10468
  return (
10070
10469
  <Container>
@@ -10095,6 +10494,22 @@ export default function Home() {
10095
10494
  >
10096
10495
  <Text style={styles.signOutButtonText}>Sign Out</Text>
10097
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}}
10098
10513
  </View>
10099
10514
  ) : null}
10100
10515
  {{#unless (eq api "none")}}
@@ -10187,6 +10602,32 @@ const styles = StyleSheet.create((theme) => ({
10187
10602
  signOutButtonText: {
10188
10603
  fontWeight: "500",
10189
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
+ },
10190
10631
  apiStatusCard: {
10191
10632
  marginBottom: 24,
10192
10633
  borderRadius: 8,
@@ -10691,9 +11132,14 @@ const styles = StyleSheet.create((theme) => ({
10691
11132
  },
10692
11133
  }));
10693
11134
  `],
10694
- ["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}}
10695
11141
  import { Container } from "@/components/container";
10696
- import { authClient } from "@/lib/auth-client";
11142
+ import { authClient{{#if (eq payments "polar")}}, polarNativeClient{{/if}} } from "@/lib/auth-client";
10697
11143
  import { Ionicons } from "@expo/vector-icons";
10698
11144
  import { Card, Chip, useThemeColor } from "heroui-native";
10699
11145
  import { SignIn } from "@/components/sign-in";
@@ -10721,6 +11167,48 @@ const isConnected = healthCheck?.data === "OK";
10721
11167
  const isLoading = healthCheck?.isLoading;
10722
11168
  {{/if}}
10723
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}}
10724
11212
 
10725
11213
  const mutedColor = useThemeColor("muted");
10726
11214
  const successColor = useThemeColor("success");
@@ -10755,6 +11243,22 @@ return (
10755
11243
  >
10756
11244
  <Text className="text-foreground font-medium">Sign Out</Text>
10757
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}}
10758
11262
  </Card>
10759
11263
  ) : null}
10760
11264
 
@@ -10813,7 +11317,8 @@ return (
10813
11317
  )}
10814
11318
  </Container>
10815
11319
  );
10816
- }`],
11320
+ }
11321
+ `],
10817
11322
  ["auth/better-auth/native/uniwind/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
10818
11323
  {{#if (eq api "trpc")}}
10819
11324
  import { queryClient } from "@/utils/trpc";
@@ -12470,7 +12975,7 @@ import { authClient } from "../lib/auth-client";
12470
12975
  `],
12471
12976
  ["auth/better-auth/web/astro/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/client";
12472
12977
  {{#if (eq payments "polar")}}
12473
- import { polarClient } from "@polar-sh/better-auth";
12978
+ import { polarClient } from "@polar-sh/better-auth/client";
12474
12979
  {{/if}}
12475
12980
  {{#if (ne backend "self")}}
12476
12981
  import { PUBLIC_SERVER_URL } from "astro:env/client";
@@ -13006,7 +13511,7 @@ watchEffect(() => {
13006
13511
  `],
13007
13512
  ["auth/better-auth/web/nuxt/app/plugins/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/vue";
13008
13513
  {{#if (eq payments "polar")}}
13009
- import { polarClient } from "@polar-sh/better-auth";
13514
+ import { polarClient } from "@polar-sh/better-auth/client";
13010
13515
  {{/if}}
13011
13516
 
13012
13517
  export default defineNuxtPlugin(() => {
@@ -13032,7 +13537,7 @@ export default defineNuxtPlugin(() => {
13032
13537
  `],
13033
13538
  ["auth/better-auth/web/react/base/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/react";
13034
13539
  {{#if (eq payments "polar")}}
13035
- import { polarClient } from "@polar-sh/better-auth";
13540
+ import { polarClient } from "@polar-sh/better-auth/client";
13036
13541
  {{/if}}
13037
13542
  {{#unless (eq backend "self")}}
13038
13543
  import { env } from "@{{projectName}}/env/web";
@@ -15278,7 +15783,7 @@ export default function UserMenu() {
15278
15783
  `],
15279
15784
  ["auth/better-auth/web/solid/src/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/solid";
15280
15785
  {{#if (eq payments "polar")}}
15281
- import { polarClient } from "@polar-sh/better-auth";
15786
+ import { polarClient } from "@polar-sh/better-auth/client";
15282
15787
  {{/if}}
15283
15788
  import { env } from "@{{projectName}}/env/web";
15284
15789
 
@@ -15696,7 +16201,7 @@ import { PUBLIC_SERVER_URL } from "$env/static/public";
15696
16201
  {{/unless}}
15697
16202
  import { createAuthClient } from "better-auth/svelte";
15698
16203
  {{#if (eq payments "polar")}}
15699
- import { polarClient } from "@polar-sh/better-auth";
16204
+ import { polarClient } from "@polar-sh/better-auth/client";
15700
16205
  {{/if}}
15701
16206
 
15702
16207
  export const authClient = createAuthClient({
@@ -17363,6 +17868,9 @@ export const httpAction = httpActionGeneric;
17363
17868
  {{#if (eq auth "better-auth")}}
17364
17869
  import betterAuth from "@convex-dev/better-auth/convex.config";
17365
17870
  {{/if}}
17871
+ {{#if (eq payments "polar")}}
17872
+ import polar from "@convex-dev/polar/convex.config.js";
17873
+ {{/if}}
17366
17874
  {{#if (includes examples "ai")}}
17367
17875
  import agent from "@convex-dev/agent/convex.config";
17368
17876
  {{/if}}
@@ -17371,6 +17879,9 @@ const app = defineApp();
17371
17879
  {{#if (eq auth "better-auth")}}
17372
17880
  app.use(betterAuth);
17373
17881
  {{/if}}
17882
+ {{#if (eq payments "polar")}}
17883
+ app.use(polar);
17884
+ {{/if}}
17374
17885
  {{#if (includes examples "ai")}}
17375
17886
  app.use(agent);
17376
17887
  {{/if}}
@@ -17694,6 +18205,32 @@ new Elysia()
17694
18205
  {{/if}}
17695
18206
  }),
17696
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}}
17697
18234
  {{#if (eq auth "better-auth")}}
17698
18235
  .all("/api/auth/*", async (context) => {
17699
18236
  const { request, status } = context;
@@ -17816,6 +18353,31 @@ app.use(clerkMiddleware());
17816
18353
  app.all("/api/auth{/*path}", toNodeHandler(auth));
17817
18354
  {{/if}}
17818
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}}
17819
18381
  {{#if (eq api "trpc")}}
17820
18382
  app.use(
17821
18383
  "/trpc",
@@ -17973,6 +18535,31 @@ fastify.register(clerkPlugin, {
17973
18535
  });
17974
18536
  {{/if}}
17975
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}}
17976
18563
  {{#if (eq api "orpc")}}
17977
18564
  fastify.register(async (rpcApp) => {
17978
18565
  // Fully utilize oRPC features by letting oRPC parse the request body.
@@ -18147,6 +18734,29 @@ app.on(
18147
18734
  );
18148
18735
  {{/if}}
18149
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}}
18150
18760
  {{#if (eq api "orpc")}}
18151
18761
  export const apiHandler = new OpenAPIHandler(appRouter, {
18152
18762
  plugins: [
@@ -26441,7 +27051,12 @@ const styles = StyleSheet.create({
26441
27051
  });
26442
27052
  `],
26443
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";
26444
- import { View, ScrollView, StyleSheet } from "react-native";
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}}
26445
27060
  import { Container } from "@/components/container";
26446
27061
  import { useColorScheme } from "@/lib/use-color-scheme";
26447
27062
  import { NAV_THEME } from "@/lib/constants";
@@ -26464,7 +27079,7 @@ import { router } from "expo-router";
26464
27079
  import { useAuth, useUser } from "@clerk/expo";
26465
27080
  import { SignOutButton } from "@/components/sign-out-button";
26466
27081
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
26467
- import { useConvexAuth, useQuery } from "convex/react";
27082
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
26468
27083
  import { api } from "@{{ projectName }}/backend/convex/_generated/api";
26469
27084
  import { authClient } from "@/lib/auth-client";
26470
27085
  import { SignIn } from "@/components/sign-in";
@@ -26494,22 +27109,71 @@ const { user } = useUser();
26494
27109
  const healthCheck = useQuery(api.healthCheck.get);
26495
27110
  const { isAuthenticated } = useConvexAuth();
26496
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}}
26497
27163
  {{else if (eq backend "convex")}}
26498
27164
  const healthCheck = useQuery(api.healthCheck.get);
26499
27165
  {{/if}}
26500
27166
 
26501
27167
  return (
26502
27168
  <Container>
26503
- <ScrollView style={styles.scrollView}>
27169
+ <ScrollView style={styles.scrollView} contentInsetAdjustmentBehavior="never">
26504
27170
  <View style={styles.content}>
26505
- <Host style={styles.titleHost} matchContents=\\{{ vertical: true }}>
26506
- <Column>
26507
- <ExpoUIText
26508
- textStyle=\\{{ color: theme.text, fontSize: 24, fontWeight: "bold" }}
26509
- >
26510
- BETTER T STACK
26511
- </ExpoUIText>
26512
- </Column>
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>
26513
27177
  </Host>
26514
27178
 
26515
27179
  {{#unless (and (eq backend "convex") (eq auth "better-auth"))}}
@@ -26703,6 +27367,21 @@ return (
26703
27367
  }}
26704
27368
  />
26705
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}}
26706
27385
  </View>
26707
27386
  ) : (
26708
27387
  <>
@@ -26727,7 +27406,8 @@ paddingTop: 28,
26727
27406
  paddingBottom: 32,
26728
27407
  },
26729
27408
  titleHost: {
26730
- alignSelf: "center",
27409
+ alignSelf: "stretch",
27410
+ height: 34,
26731
27411
  marginBottom: 24,
26732
27412
  },
26733
27413
  card: {
@@ -26757,6 +27437,9 @@ borderRadius: 16,
26757
27437
  userHeader: {
26758
27438
  marginBottom: 8,
26759
27439
  },
27440
+ paymentActions: {
27441
+ marginTop: 12,
27442
+ },
26760
27443
  authHost: {
26761
27444
  marginBottom: 12,
26762
27445
  },
@@ -26893,7 +27576,10 @@ export function Container({ children }: { children: React.ReactNode }) {
26893
27576
  : NAV_THEME.light.background;
26894
27577
 
26895
27578
  return (
26896
- <SafeAreaView style={[styles.container, { backgroundColor }]}>
27579
+ <SafeAreaView
27580
+ edges={["left", "right", "bottom"]}
27581
+ style={[styles.container, { backgroundColor }]}
27582
+ >
26897
27583
  {children}
26898
27584
  </SafeAreaView>
26899
27585
  );
@@ -26904,7 +27590,6 @@ const styles = StyleSheet.create({
26904
27590
  flex: 1,
26905
27591
  },
26906
27592
  });
26907
-
26908
27593
  `],
26909
27594
  ["frontend/native/bare/components/header-button.tsx.hbs", `import FontAwesome from "@expo/vector-icons/FontAwesome";
26910
27595
  import { forwardRef } from "react";
@@ -27050,7 +27735,7 @@ module.exports = config;
27050
27735
  "react-native-gesture-handler": "~2.31.1",
27051
27736
  "react-native-reanimated": "4.3.1",
27052
27737
  "react-native-safe-area-context": "~5.7.0",
27053
- "react-native-screens": "4.25.1",
27738
+ "react-native-screens": "4.25.2",
27054
27739
  "react-native-web": "~0.21.0",
27055
27740
  "react-native-worklets": "0.8.3"
27056
27741
  },
@@ -27609,7 +28294,12 @@ const styles = StyleSheet.create((theme) => ({
27609
28294
  },
27610
28295
  }));
27611
28296
  `],
27612
- ["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}}
27613
28303
  import { StyleSheet } from "react-native-unistyles";
27614
28304
  import { Container } from "@/components/container";
27615
28305
 
@@ -27632,7 +28322,7 @@ import { Link } from "expo-router";
27632
28322
  import { useAuth, useUser } from "@clerk/expo";
27633
28323
  import { SignOutButton } from "@/components/sign-out-button";
27634
28324
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
27635
- import { useConvexAuth, useQuery } from "convex/react";
28325
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
27636
28326
  import { api } from "@{{ projectName }}/backend/convex/_generated/api";
27637
28327
  import { authClient } from "@/lib/auth-client";
27638
28328
  import { SignIn } from "@/components/sign-in";
@@ -27660,6 +28350,57 @@ export default function Home() {
27660
28350
  const healthCheck = useQuery(api.healthCheck.get);
27661
28351
  const { isAuthenticated } = useConvexAuth();
27662
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}}
27663
28404
  {{else if (eq backend "convex")}}
27664
28405
  const healthCheck = useQuery(api.healthCheck.get);
27665
28406
  {{/if}}
@@ -27802,6 +28543,25 @@ export default function Home() {
27802
28543
  >
27803
28544
  <Text style={styles.signOutText}>Sign Out</Text>
27804
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}}
27805
28565
  </View>
27806
28566
  ) : null}
27807
28567
  <View style={styles.apiStatusCard}>
@@ -27954,6 +28714,31 @@ const styles = StyleSheet.create((theme) => ({
27954
28714
  color: theme.colors.destructiveForeground,
27955
28715
  fontWeight: "500",
27956
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
+ },
27957
28742
  apiStatusCard: {
27958
28743
  marginBottom: theme.spacing.md,
27959
28744
  borderRadius: theme.borderRadius.lg,
@@ -28253,7 +29038,7 @@ module.exports = config;
28253
29038
  "react-native-nitro-modules": "^0.35.7",
28254
29039
  "react-native-reanimated": "4.3.1",
28255
29040
  "react-native-safe-area-context": "~5.7.0",
28256
- "react-native-screens": "4.25.1",
29041
+ "react-native-screens": "4.25.2",
28257
29042
  "react-native-unistyles": "^3.2.4",
28258
29043
  "react-native-web": "~0.21.0",
28259
29044
  "react-native-worklets": "0.8.3"
@@ -28805,7 +29590,12 @@ export default function TabTwo() {
28805
29590
  );
28806
29591
  }
28807
29592
  `],
28808
- ["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}}
28809
29599
  import { Container } from "@/components/container";
28810
29600
  {{#if (eq api "orpc")}}
28811
29601
  import { useQuery } from "@tanstack/react-query";
@@ -28826,7 +29616,7 @@ import { Link } from "expo-router";
28826
29616
  import { useAuth, useUser } from "@clerk/expo";
28827
29617
  import { SignOutButton } from "@/components/sign-out-button";
28828
29618
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
28829
- import { useConvexAuth, useQuery } from "convex/react";
29619
+ import { {{#if (eq payments "polar")}}useAction, {{/if}}useConvexAuth, useQuery } from "convex/react";
28830
29620
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
28831
29621
  import { authClient } from "@/lib/auth-client";
28832
29622
  import { SignIn } from "@/components/sign-in";
@@ -28858,6 +29648,57 @@ const { user } = useUser();
28858
29648
  const healthCheck = useQuery(api.healthCheck.get);
28859
29649
  const { isAuthenticated } = useConvexAuth();
28860
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}}
28861
29702
  {{else if (eq backend "convex")}}
28862
29703
  const healthCheck = useQuery(api.healthCheck.get);
28863
29704
  {{/if}}
@@ -29009,6 +29850,19 @@ return (
29009
29850
  Sign Out
29010
29851
  </Button>
29011
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}}
29012
29866
  </Surface>
29013
29867
  ) : null}
29014
29868
  <Surface variant="secondary" className="p-4 rounded-xl">
@@ -29306,7 +30160,7 @@ module.exports = uniwindConfig;
29306
30160
  "react-native-keyboard-controller": "1.21.6",
29307
30161
  "react-native-reanimated": "4.3.1",
29308
30162
  "react-native-safe-area-context": "~5.7.0",
29309
- "react-native-screens": "4.25.1",
30163
+ "react-native-screens": "4.25.2",
29310
30164
  "react-native-svg": "15.15.4",
29311
30165
  "react-native-web": "~0.21.0",
29312
30166
  "react-native-worklets": "0.8.3",
@@ -34000,6 +34854,7 @@ export function cn(...inputs: ClassValue[]) {
34000
34854
  "compilerOptions": {
34001
34855
  "jsx": "react-jsx",
34002
34856
  "lib": ["ESNext", "DOM", "DOM.Iterable"],
34857
+ "types": [],
34003
34858
  "paths": {
34004
34859
  "@{{projectName}}/ui/*": ["./src/*"]
34005
34860
  }
@@ -34007,6 +34862,71 @@ export function cn(...inputs: ClassValue[]) {
34007
34862
  "include": ["src/**/*.ts", "src/**/*.tsx"],
34008
34863
  "exclude": ["node_modules"]
34009
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
+ });
34010
34930
  `],
34011
34931
  ["payments/polar/server/base/src/lib/payments.ts.hbs", `import { Polar } from "@polar-sh/sdk";
34012
34932
  {{#if (and (eq backend "self") (eq webDeploy "cloudflare") (includes frontend "svelte"))}}
@@ -34165,7 +35085,7 @@ function SuccessPage() {
34165
35085
  </div>
34166
35086
  `]
34167
35087
  ]);
34168
- const TEMPLATE_COUNT = 474;
35088
+ const TEMPLATE_COUNT = 475;
34169
35089
  //#endregion
34170
35090
  export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processTemplateString, transformFilename, writeBtsConfigToVfs };
34171
35091