feat: add dodo as a payment provider#980
feat: add dodo as a payment provider#980atybdot wants to merge 35 commits intoAmanVarshney01:mainfrom
Conversation
Added Dodo option in CLI payments dodo to the shared payments enum, surfaced it in the CLI prompt, and enforced compatibility checks with provider-specific error messaging. Added a Dodo incompatibility integration test.
…emplates.generated.ts
…ates/auth/better-auth/server/base/src/index.ts.hbs
…ates/auth/better-auth/web/astro/src/pages/dashboard.astro.hbs
…ates/auth/better-auth/web/react/next/src/app/dashboard/dashboard.tsx.hbs
…ates/auth/better-auth/web/react/react-router/src/routes/dashboard.tsx.hbs
…ates/auth/better-auth/web/react/tanstack-router/src/routes/dashboard.tsx.hbs
…ates/auth/better-auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbs
…ates/auth/better-auth/web/solid/src/routes/dashboard.tsx.hbs
…ates/auth/better-auth/web/svelte/src/routes/dashboard/+page.svelte.hbs
|
@atybdot is attempting to deploy a commit to the Better T Stack Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThis PR extends the CLI and template generator to support Dodo Payments as a new payment provider option alongside Polar. The changes include CLI prompts, compatibility validation, environment variable configuration, documentation updates, template files for payment success pages, dashboard components for subscription management, and corresponding tests across all frontend frameworks (React, Vue, Solid, Svelte, Astro). Changes
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required. |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/template-generator/templates/auth/better-auth/server/base/src/index.ts.hbs (2)
322-326:⚠️ Potential issue | 🔴 CriticalMissing Dodo imports in mongoose section.
The Dodo plugin block (lines 380–404) uses
dodopayments,checkout,portal,webhooks, anddodoPaymentsClient, but there is no corresponding import block for themongooseORM path. Generated code will fail whenpayments === "dodo".🐛 Proposed fix to add missing Dodo imports
{{`#if` (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; {{/if}} +{{`#if` (eq payments "dodo")}} +import { dodopayments, checkout, portal, webhooks } from "@dodopayments/better-auth"; +import { dodoPaymentsClient } from "./lib/payments"; +{{/if}} import { client } from "@{{projectName}}/db";
413-417:⚠️ Potential issue | 🔴 CriticalMissing Dodo imports in "none" ORM section.
Same issue as the mongoose section: lines 471–495 reference Dodo identifiers that are never imported when
orm === "none".🐛 Proposed fix to add missing Dodo imports
{{`#if` (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; {{/if}} +{{`#if` (eq payments "dodo")}} +import { dodopayments, checkout, portal, webhooks } from "@dodopayments/better-auth"; +import { dodoPaymentsClient } from "./lib/payments"; +{{/if}} export const auth = betterAuth({
🧹 Nitpick comments (5)
packages/template-generator/src/processors/payments-deps.ts (1)
13-24: Consider centralizing the supported web-frontend list.This inline list is correct, but it’s easy to drift from other modules over time. Consider extracting it to a shared constant/helper used by all payment/template processors.
packages/template-generator/templates/auth/better-auth/server/base/src/index.ts.hbs (1)
84-84: Consider using an environment variable for Dodo'ssuccessUrl.Polar uses
env.POLAR_SUCCESS_URLwhile Dodo has a hardcoded"/payment/success". For deployment flexibility and consistency, consider introducingenv.DODO_PAYMENTS_SUCCESS_URL(or reusing a shared env var).packages/template-generator/templates/payments/dodo/web/astro/src/pages/payment/success.astro.hbs (1)
19-21: Minor: Redundant initial classes on redirect div.The
hiddenclass will take precedence, hiding the element. While this works (sincehiddenis removed via JavaScript), having bothhiddenandflexin the initial class list is slightly confusing. Consider keeping justhiddeninitially and addingflexvia JavaScript when revealing, or use a different approach like inline styles. This is a minor readability nit.packages/template-generator/templates/payments/dodo/web/react/next/src/app/payment/success/page.tsx.hbs (1)
3-6: Consider conditionally importingauthClient.The
authClientimport on line 6 is always included but only used whenbackend !== "self". This results in an unused import in the generated code when the "self" backend is selected.♻️ Proposed fix to conditionally import authClient
{{`#if` (eq backend "self")}} import { auth } from "@{{projectName}}/auth"; +{{else}} +import { authClient } from "@/lib/auth-client"; {{/if}} -import { authClient } from "@/lib/auth-client";packages/template-generator/src/templates.generated.ts (1)
6630-6653: Webhook handler only logs events—consider adding a TODO comment.The
onPayloadhandler is a minimal placeholder that only logs the event type. While this is appropriate for scaffolding, users might not realize they need to implement actual business logic here (e.g., updating subscription status in the database).Consider adding a TODO comment to guide users:
Suggested improvement
webhooks({ webhookKey: env.DODO_PAYMENTS_WEBHOOK_SECRET, onPayload: async (payload) => { + // TODO: Handle webhook events (e.g., subscription.active, subscription.cancelled) + // Update your database accordingly console.log("Received webhook:", payload.event_type); }, }),
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 59ab6ec1-aff0-44d5-b49a-ca635156852b
📒 Files selected for processing (45)
apps/cli/README.mdapps/cli/src/helpers/addons/mcp-setup.tsapps/cli/src/prompts/payments.tsapps/cli/src/utils/compatibility-rules.tsapps/cli/test/addon-setup-regressions.test.tsapps/cli/test/integration.test.tsapps/cli/test/payments-dodo.test.tsapps/web/content/docs/cli/compatibility.mdxapps/web/content/docs/cli/index.mdxapps/web/content/docs/cli/options.mdxapps/web/content/docs/index.mdxapps/web/src/app/(home)/new/_components/utils.tsapps/web/src/lib/constant.tspackages/template-generator/src/processors/env-vars.tspackages/template-generator/src/processors/payments-deps.tspackages/template-generator/src/templates.generated.tspackages/template-generator/src/utils/add-deps.tspackages/template-generator/templates/auth/better-auth/server/base/src/index.ts.hbspackages/template-generator/templates/auth/better-auth/web/astro/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/astro/src/pages/dashboard.astro.hbspackages/template-generator/templates/auth/better-auth/web/nuxt/app/pages/dashboard.vue.hbspackages/template-generator/templates/auth/better-auth/web/nuxt/app/plugins/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/react/base/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/react/next/src/app/dashboard/dashboard.tsx.hbspackages/template-generator/templates/auth/better-auth/web/react/next/src/app/dashboard/page.tsx.hbspackages/template-generator/templates/auth/better-auth/web/react/react-router/src/routes/dashboard.tsx.hbspackages/template-generator/templates/auth/better-auth/web/react/tanstack-router/src/routes/dashboard.tsx.hbspackages/template-generator/templates/auth/better-auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbspackages/template-generator/templates/auth/better-auth/web/solid/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/solid/src/routes/dashboard.tsx.hbspackages/template-generator/templates/auth/better-auth/web/svelte/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/svelte/src/routes/dashboard/+page.svelte.hbspackages/template-generator/templates/packages/env/src/server.ts.hbspackages/template-generator/templates/packages/infra/alchemy.run.ts.hbspackages/template-generator/templates/payments/dodo/server/base/src/lib/payments.ts.hbspackages/template-generator/templates/payments/dodo/web/astro/src/pages/payment/success.astro.hbspackages/template-generator/templates/payments/dodo/web/nuxt/app/pages/payment/success.vue.hbspackages/template-generator/templates/payments/dodo/web/react/next/src/app/payment/success/page.tsx.hbspackages/template-generator/templates/payments/dodo/web/react/react-router/src/routes/payment-success.tsx.hbspackages/template-generator/templates/payments/dodo/web/react/tanstack-router/src/routes/payment/success.tsx.hbspackages/template-generator/templates/payments/dodo/web/react/tanstack-start/src/functions/get-payment.ts.hbspackages/template-generator/templates/payments/dodo/web/react/tanstack-start/src/routes/payment/success.tsx.hbspackages/template-generator/templates/payments/dodo/web/solid/src/routes/payment/success.tsx.hbspackages/template-generator/templates/payments/dodo/web/svelte/src/routes/payment/success/+page.svelte.hbspackages/types/src/schemas.ts
| // need to update the dodo logo in r2 | ||
| icon: `${ICON_BASE_URL}/dodo.svg`, | ||
| color: "from-amber-400 to-amber-600", |
There was a problem hiding this comment.
Replace the placeholder Dodo icon reference or provide a safe fallback.
The inline note indicates the asset is not ready yet, so this entry can render a broken icon in the UI. Please either ship the icon asset now or use a temporary fallback until it exists.
| ["payments/dodo/web/react/tanstack-start/src/functions/get-payment.ts.hbs", `import { authClient } from "@/lib/auth-client"; | ||
| import { authMiddleware } from "@/middleware/auth"; | ||
| import { createServerFn } from "@tanstack/react-start"; | ||
| import { getRequestHeaders } from "@tanstack/react-start/server"; | ||
|
|
||
| export const getPayment = createServerFn({ method: "GET" }) | ||
| .middleware([authMiddleware]) | ||
| .handler(async () => { | ||
| const { data: customerState } = await authClient.dodopayments.customer.state({ | ||
| fetchOptions: { | ||
| headers: getRequestHeaders(), | ||
| }, | ||
| }); | ||
| return customerState; | ||
| }); | ||
| `], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify other get-payment templates to understand expected patterns
echo "=== Polar get-payment template for comparison ==="
rg -A 20 "payments/polar.*get-payment" --type ts
echo "=== All Dodo customer API usages ==="
rg "dodopayments\.customer\.(state|subscriptions)" --type ts -B2 -A5Repository: AmanVarshney01/create-better-t-stack
Length of output: 7467
🏁 Script executed:
#!/bin/bash
# Find all Dodo get-payment implementations across frameworks
echo "=== All Dodo get-payment templates ==="
rg "payments/dodo.*get-payment" -A 15 --type ts
echo -e "\n=== Dashboard components that use customerState ==="
rg "customerState.*data.*items" --type ts -B3 -A3Repository: AmanVarshney01/create-better-t-stack
Length of output: 9240
🏁 Script executed:
#!/bin/bash
# Find all get-payment templates across all payment providers and frameworks
echo "=== All get-payment templates in codebase ==="
rg "get-payment.*\.hbs" --type ts | head -20
# Verify there's only one Dodo get-payment
echo -e "\n=== Dodo get-payment count ==="
rg "payments/dodo.*get-payment" --type ts | wc -lRepository: AmanVarshney01/create-better-t-stack
Length of output: 524
API mismatch: get-payment.ts uses incorrect API method and destructuring pattern.
This is the only Dodo get-payment template in the codebase, and it uses authClient.dodopayments.customer.state() with incorrect destructuring. All other Dodo templates and dashboard components consistently use authClient.dodopayments.customer.subscriptions.list() and expect a response with { data: { items: [...] } } structure.
The current code destructures const { data: customerState } = await authClient.dodopayments.customer.state() and returns customerState, which removes the data wrapper. Dashboard components across all frameworks check:
const hasProSubscription = (customerState?.data?.items?.length ?? 0) > 0;This will fail because customerState?.data will be undefined.
Proposed fix to align with all other Dodo templates
export const getPayment = createServerFn({ method: "GET" })
.middleware([authMiddleware])
.handler(async () => {
- const { data: customerState } = await authClient.dodopayments.customer.state({
+ const customerState = await authClient.dodopayments.customer.subscriptions.list({
+ query: {
+ limit: 10,
+ page: 1,
+ active: true,
+ },
fetchOptions: {
headers: getRequestHeaders(),
},
});
return customerState;
});| ["payments/dodo/web/react/tanstack-start/src/routes/payment/success.tsx.hbs", `import { createFileRoute, useSearch } from "@tanstack/react-router"; | ||
| import { getUser } from "@/functions/get-user"; | ||
|
|
||
| export const Route = createFileRoute("/payment/success")({ | ||
| component: SuccessPage, | ||
| validateSearch: (search) => ({ | ||
| checkout_id: search.checkout_id as string, | ||
| }), | ||
| beforeLoad: async () => { | ||
| const session = await getUser(); | ||
| return { session }; | ||
| }, | ||
| }); | ||
|
|
||
| function SuccessPage() { | ||
| const { checkout_id } = useSearch({ from: "/payment/success" }); | ||
|
|
||
| return ( | ||
| <div className="container mx-auto px-4 py-8"> | ||
| <h1>Payment Successful!</h1> | ||
| {checkout_id && <p>Checkout ID: {checkout_id}</p>} | ||
| </div> | ||
| ); | ||
| } | ||
| `], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check what getUser returns when no session exists
rg -A 15 "export const getUser" --type tsRepository: AmanVarshney01/create-better-t-stack
Length of output: 2279
🏁 Script executed:
#!/bin/bash
# Find the TanStack Router payment success page mentioned at lines 29266-29274
sed -n '29266,29274p' packages/template-generator/src/templates.generated.tsRepository: AmanVarshney01/create-better-t-stack
Length of output: 260
🏁 Script executed:
#!/bin/bash
# Search for other payment success page templates to see their auth patterns
rg "payment/success" packages/template-generator/src/templates.generated.ts | head -20Repository: AmanVarshney01/create-better-t-stack
Length of output: 1313
🏁 Script executed:
#!/bin/bash
# Look for redirect patterns in middleware and server functions
rg "throw redirect" packages/template-generator/src/templates.generated.ts | head -10Repository: AmanVarshney01/create-better-t-stack
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Check the full TanStack Start success page template to see if there's any route guard
rg -B 5 -A 30 'payments/dodo/web/react/tanstack-start/src/routes/payment/success.tsx.hbs' packages/template-generator/src/templates.generated.ts | head -50Repository: AmanVarshney01/create-better-t-stack
Length of output: 1105
🏁 Script executed:
#!/bin/bash
# Check if TanStack Start templates import redirect and how they use it
rg -A 3 'tanstack-start.*redirect' packages/template-generator/src/templates.generated.ts | head -20Repository: AmanVarshney01/create-better-t-stack
Length of output: 62
Add explicit session validation in beforeLoad.
The getUser() server function with authMiddleware retrieves the session but does not redirect on missing authentication. Unlike the TanStack Router variant which explicitly checks if (!session.data) and redirects, this template lacks that protection. Unauthenticated users could reach the success page.
Add the following check for consistency and safety:
Suggested improvement
beforeLoad: async () => {
const session = await getUser();
+ if (!session) {
+ throw redirect({
+ to: "/login",
+ });
+ }
return { session };
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ["payments/dodo/web/react/tanstack-start/src/routes/payment/success.tsx.hbs", `import { createFileRoute, useSearch } from "@tanstack/react-router"; | |
| import { getUser } from "@/functions/get-user"; | |
| export const Route = createFileRoute("/payment/success")({ | |
| component: SuccessPage, | |
| validateSearch: (search) => ({ | |
| checkout_id: search.checkout_id as string, | |
| }), | |
| beforeLoad: async () => { | |
| const session = await getUser(); | |
| return { session }; | |
| }, | |
| }); | |
| function SuccessPage() { | |
| const { checkout_id } = useSearch({ from: "/payment/success" }); | |
| return ( | |
| <div className="container mx-auto px-4 py-8"> | |
| <h1>Payment Successful!</h1> | |
| {checkout_id && <p>Checkout ID: {checkout_id}</p>} | |
| </div> | |
| ); | |
| } | |
| `], | |
| import { createFileRoute, useSearch, redirect } from "@tanstack/react-router"; | |
| import { getUser } from "@/functions/get-user"; | |
| export const Route = createFileRoute("/payment/success")({ | |
| component: SuccessPage, | |
| validateSearch: (search) => ({ | |
| checkout_id: search.checkout_id as string, | |
| }), | |
| beforeLoad: async () => { | |
| const session = await getUser(); | |
| if (!session) { | |
| throw redirect({ | |
| to: "/login", | |
| }); | |
| } | |
| return { session }; | |
| }, | |
| }); | |
| function SuccessPage() { | |
| const { checkout_id } = useSearch({ from: "/payment/success" }); | |
| return ( | |
| <div className="container mx-auto px-4 py-8"> | |
| <h1>Payment Successful!</h1> | |
| {checkout_id && <p>Checkout ID: {checkout_id}</p>} | |
| </div> | |
| ); | |
| } |
| {{#if (eq payments "polar")}} | ||
| customerState: ReturnType<typeof authClient.customer.state>; | ||
| {{/if}} | ||
| {{else if (eq payments "dodo")}} | ||
| customerState: { data?: { items?: unknown[] } }; | ||
| {{/if}} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the template file to see the full context
cat -n "packages/template-generator/templates/auth/better-auth/web/react/next/src/app/dashboard/dashboard.tsx.hbs"Repository: AmanVarshney01/create-better-t-stack
Length of output: 2664
🏁 Script executed:
# Find files that might define authClient or provide type information
find "packages" -name "*.ts" -o -name "*.tsx" -o -name "*.d.ts" | head -20Repository: AmanVarshney01/create-better-t-stack
Length of output: 939
🏁 Script executed:
# Search for authClient.customer.state definition
rg "authClient\.customer\.state" --type ts --type tsxRepository: AmanVarshney01/create-better-t-stack
Length of output: 107
🏁 Script executed:
# Search for Better Auth package to understand the API
find . -name "*auth*" -type f \( -name "*.ts" -o -name "*.tsx" \) | grep -E "(client|index)" | head -10Repository: AmanVarshney01/create-better-t-stack
Length of output: 62
🏁 Script executed:
# Let me search for where this Dashboard component is called
rg "Dashboard" --type ts --type js --type tsx -A 3 -B 3 | grep -E "(customerState|import.*Dashboard|<Dashboard)" | head -30Repository: AmanVarshney01/create-better-t-stack
Length of output: 107
🏁 Script executed:
# Let's also check if there's a layout or parent component that prepares customerState
find "packages/template-generator/templates" -name "*.hbs" | xargs rg "customerState" -B 5 -A 5 | head -60Repository: AmanVarshney01/create-better-t-stack
Length of output: 7580
🏁 Script executed:
# Let's find the auth client setup to understand the API
find . -path "*/lib/auth-client*" -o -path "*/auth/client*" -o -path "*better-auth*" | head -20Repository: AmanVarshney01/create-better-t-stack
Length of output: 1813
🏁 Script executed:
# Search for actual usage of authClient.customer.state in the generated code or examples
rg "customer\.state" --type ts --type js | head -20Repository: AmanVarshney01/create-better-t-stack
Length of output: 1291
🏁 Script executed:
# Check the Better Auth documentation or types by searching for how customerState is typically prepared
rg "authClient\.customer" --type ts --type js -A 2 | head -40Repository: AmanVarshney01/create-better-t-stack
Length of output: 2998
🏁 Script executed:
# Let me find where the Dashboard component is instantiated and how customerState is prepared
find "packages/template-generator/templates" -name "*.hbs" -path "*dashboard*" ! -name "*dashboard.tsx.hbs"Repository: AmanVarshney01/create-better-t-stack
Length of output: 776
🏁 Script executed:
# Check the layout or parent component for the same path
cat -n "packages/template-generator/templates/auth/better-auth/web/react/next/src/app/layout.tsx.hbs"Repository: AmanVarshney01/create-better-t-stack
Length of output: 204
🏁 Script executed:
# Let me search for the layout file in the same directory
ls -la "packages/template-generator/templates/auth/better-auth/web/react/next/src/app/" 2>/dev/null || find "packages/template-generator/templates/auth/better-auth/web/react/next" -name "*.hbs" -type fRepository: AmanVarshney01/create-better-t-stack
Length of output: 296
🏁 Script executed:
# Let's look at the page.tsx.hbs which should import the Dashboard component
cat -n "packages/template-generator/templates/auth/better-auth/web/react/next/src/app/dashboard/page.tsx.hbs"Repository: AmanVarshney01/create-better-t-stack
Length of output: 1659
🏁 Script executed:
# Let's also check for a layout file at a higher level
find "packages/template-generator/templates/auth/better-auth/web/react/next" -name "layout.tsx.hbs" -o -name "layout.tsx"Repository: AmanVarshney01/create-better-t-stack
Length of output: 62
🏁 Script executed:
# Let me search for how customerState is passed to the Dashboard in the Next.js variant
rg "Dashboard" "packages/template-generator/templates/auth/better-auth/web/react/next" -A 5 -B 5Repository: AmanVarshney01/create-better-t-stack
Length of output: 4677
🏁 Script executed:
# Let me check if there's a Better Auth type definition or example showing the actual shape
rg "activeSubscriptions" --type ts --type jsRepository: AmanVarshney01/create-better-t-stack
Length of output: 1869
🏁 Script executed:
# Let's search for any type definitions or documentation about better-auth customer.state response
rg "customer\.state" "packages/template-generator" -B 3 -A 5 | grep -E "(type|interface|activeSubscriptions|data)" | head -40Repository: AmanVarshney01/create-better-t-stack
Length of output: 4833
🏁 Script executed:
# Let's verify the suggested fix syntax is correct by checking if Awaited is used elsewhere in the templates
rg "Awaited" "packages/template-generator/templates" -B 2 -A 2Repository: AmanVarshney01/create-better-t-stack
Length of output: 668
Fix the customerState prop type to match the resolved data shape being passed.
In page.tsx.hbs, line 28 destructures { data: customerState } from the Polar response before passing it to Dashboard, but the prop type at line 22 uses ReturnType<typeof authClient.customer.state> — the unresolved async return type, not the unwrapped .data object. This causes a TypeScript mismatch: lines 36–37 access customerState?.activeSubscriptions, which does not exist on the Promise-like return type. The Dodo branch (line 34) has the same structural issue with a handwritten shape that can drift from the actual client response.
Use Awaited<ReturnType<...>>["data"] for Polar and apply the same pattern to Dodo to derive the type consistently from the actual auth client method.
Suggested fix
+{{`#if` (eq payments "polar")}}
+type CustomerState = Awaited<
+ ReturnType<typeof authClient.customer.state>
+>["data"];
+{{else if (eq payments "dodo")}}
+type CustomerState = Awaited<
+ ReturnType<typeof authClient.dodopayments.customer.subscriptions.list>
+>;
+{{/if}}
+
export default function Dashboard({
{{`#if` (or (eq payments "polar") (eq payments "dodo"))}}
customerState,
{{/if}}
session
}: {
-{{`#if` (eq payments "polar")}}
- customerState: ReturnType<typeof authClient.customer.state>;
-{{else if (eq payments "dodo")}}
- customerState: { data?: { items?: unknown[] } };
-{{/if}}
+{{`#if` (or (eq payments "polar") (eq payments "dodo"))}}
+ customerState: CustomerState;
+{{/if}}
session: typeof authClient.$Infer.Session;
}) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {{#if (eq payments "polar")}} | |
| customerState: ReturnType<typeof authClient.customer.state>; | |
| {{/if}} | |
| {{else if (eq payments "dodo")}} | |
| customerState: { data?: { items?: unknown[] } }; | |
| {{/if}} | |
| {{`#if` (eq payments "polar")}} | |
| type CustomerState = Awaited< | |
| ReturnType<typeof authClient.customer.state> | |
| >["data"]; | |
| {{else if (eq payments "dodo")}} | |
| type CustomerState = Awaited< | |
| ReturnType<typeof authClient.dodopayments.customer.subscriptions.list> | |
| >; | |
| {{/if}} | |
| export default function Dashboard({ | |
| {{`#if` (or (eq payments "polar") (eq payments "dodo"))}} | |
| customerState, | |
| {{/if}} | |
| session | |
| }: { | |
| {{`#if` (or (eq payments "polar") (eq payments "dodo"))}} | |
| customerState: CustomerState; | |
| {{/if}} | |
| session: typeof authClient.$Infer.Session; | |
| }) { |
| {{#if (or (eq payments "polar") (eq payments "dodo"))}} | ||
| const [customerState, setCustomerState] = useState<any>(null); | ||
| {{/if}} | ||
| {{/if}} |
There was a problem hiding this comment.
Don't render the billing state before the subscription request finishes.
customerState starts as null, so Lines 75-101 treat every logged-in user as free on the first paint. Existing subscribers will briefly see the "Free" plan and the upgrade CTA instead of the manage-subscription action. Add a separate loading flag or move this fetch into a loader, and hide the billing UI until the provider-specific request completes.
Also applies to: 49-64, 75-103
| $effect(() => { | ||
| if ($sessionQuery.data) { | ||
| authClient.dodopayments.customer.subscriptions | ||
| .list({ | ||
| query: { | ||
| limit: 10, | ||
| page: 1, | ||
| active: true, | ||
| }, | ||
| }) | ||
| .then((data) => { | ||
| customerState = data; | ||
| }); |
There was a problem hiding this comment.
Handle Dodo subscription fetch failures explicitly.
The Promise chain has no .catch, so failed requests can produce unhandled rejections and leave the UI in an incorrect implicit “Free” state.
Proposed fix
$effect(() => {
if ($sessionQuery.data) {
authClient.dodopayments.customer.subscriptions
.list({
query: {
limit: 10,
page: 1,
active: true,
},
})
.then((data) => {
customerState = data;
- });
+ })
+ .catch(() => {
+ customerState = { data: { items: [] } };
+ });
}
});| {{#if (eq payments "dodo")}} | ||
| DODO_PAYMENTS_API_KEY: alchemy.secret.env.DODO_PAYMENTS_API_KEY!, | ||
| DODO_PAYMENTS_WEBHOOK_SECRET: alchemy.secret.env.DODO_PAYMENTS_WEBHOOK_SECRET!, | ||
| DODO_PAYMENTS_ENVIRONMENT: alchemy.env.DODO_PAYMENTS_ENVIRONMENT!, | ||
| {{/if}} |
There was a problem hiding this comment.
Add missing Dodo bindings to the Cloudflare Worker("server") section.
Dodo vars were added for web bindings, but the server worker bindings still only handle Polar. For serverDeploy = cloudflare + payments = dodo, generated server code can miss required env bindings at runtime.
Suggested fix
{{`#if` (eq payments "polar")}}
POLAR_ACCESS_TOKEN: alchemy.secret.env.POLAR_ACCESS_TOKEN!,
POLAR_SUCCESS_URL: alchemy.env.POLAR_SUCCESS_URL!,
{{/if}}
+{{`#if` (eq payments "dodo")}}
+ DODO_PAYMENTS_API_KEY: alchemy.secret.env.DODO_PAYMENTS_API_KEY!,
+ DODO_PAYMENTS_WEBHOOK_SECRET: alchemy.secret.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
+ DODO_PAYMENTS_ENVIRONMENT: alchemy.env.DODO_PAYMENTS_ENVIRONMENT!,
+{{/if}}Also applies to: 151-155, 220-224, 308-312
| useEffect(() => { | ||
| let active = true; | ||
| async function loadSession() { | ||
| const session = await authClient.getSession(); | ||
| if (active && !session.data) { | ||
| navigate("/login", { replace: true }); | ||
| } | ||
| } | ||
|
|
||
| loadSession(); | ||
| return () => { | ||
| active = false; | ||
| }; | ||
| }, [navigate]); | ||
|
|
||
| return ( | ||
| <div className="container mx-auto px-4 py-8"> | ||
| <h1>Payment Successful!</h1> | ||
| {checkout_id && <p>Checkout ID: {checkout_id}</p>} | ||
| </div> |
There was a problem hiding this comment.
Protect the page before rendering success content.
At Line 11, auth is checked only after initial render, so unauthenticated users can briefly see the success UI (and checkout_id) before redirect. This weakens route protection.
Suggested fix
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
@@
export default function PaymentSuccessPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
+ const [authorized, setAuthorized] = useState<boolean | null>(null);
const checkout_id = searchParams.get("checkout_id");
@@
useEffect(() => {
let active = true;
async function loadSession() {
- const session = await authClient.getSession();
- if (active && !session.data) {
- navigate("/login", { replace: true });
- }
+ try {
+ const session = await authClient.getSession();
+ if (!active) return;
+ if (!session.data) {
+ navigate("/login", { replace: true });
+ return;
+ }
+ setAuthorized(true);
+ } catch {
+ if (active) navigate("/login", { replace: true });
+ }
}
@@
}, [navigate]);
+
+ if (authorized === null) return null;| redirect({ | ||
| to: "/login", | ||
| throw: true, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the exact @tanstack/react-routerversion used by this repository, what is the recommended redirect pattern insidebeforeLoadfor auth guards:throw redirect(...), return redirect(...), or redirect({ throw: true, ... })?
💡 Result:
The recommended redirect pattern inside beforeLoad for auth guards in @tanstack/react-router (all recent versions up to v1.167.4 as of 2026-03-19) is throw redirect({ to: '/login', ... }). This is consistently shown in official documentation and examples for authenticated routes, where beforeLoad checks authentication status and throws the redirect if the user is not authenticated, preventing child routes from loading. Returning the redirect object or using redirect({ throw: true, ... }) (which auto-throws) are alternatives mentioned in the redirect API docs, but throw redirect(...) without explicit throw: true is the standard, recommended pattern for auth guards specifically. return redirect(...) alone does not interrupt route loading and is not recommended for guards.
Citations:
- 1: https://tanstack.com/router/v1/docs/guide/authenticated-routes
- 2: https://tanstack.com/router/v1/docs/api/router/redirectFunction
- 3: https://tanstack.com/router/latest/docs/api/router/redirectFunction
- 4: https://tanstack.com/router/latest/docs/how-to/setup-rbac
- 5: Performance is horrible when using recommended Authentication patterns TanStack/router#3997
- 6: https://spin.atomicobject.com/authenticated-routes-tanstack-router/
Use the standard throw redirect(...) pattern in beforeLoad.
Line 12 uses redirect({ throw: true }), which auto-throws but deviates from the documented standard for auth guards. Use throw redirect(...) instead for consistency with @tanstack/react-router best practices.
Suggested fix
if (!session.data) {
- redirect({
- to: "/login",
- throw: true,
- });
+ throw redirect({
+ to: "/login",
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| redirect({ | |
| to: "/login", | |
| throw: true, | |
| }); | |
| throw redirect({ | |
| to: "/login", | |
| }); |
| beforeLoad: async () => { | ||
| const session = await getUser(); | ||
| return { session }; | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate get-user implementations:"
fd 'get-user\.(ts|tsx|js|mjs|cjs)$' packages/template-generator/templates -HI
echo
echo "Inspect getUser definitions and nearby logic:"
rg -nP -C4 --type=ts --type=tsx 'export\s+(async\s+)?function\s+getUser|const\s+getUser\s*=' packages/template-generator/templates
echo
echo "Inspect auth guarding in this route:"
rg -nP -C4 --type=ts --type=tsx 'createFileRoute\("/payment/success"\)|beforeLoad|redirect\(' packages/template-generator/templates/payments/dodo/web/react/tanstack-startRepository: AmanVarshney01/create-better-t-stack
Length of output: 204
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Reading the target file ==="
cat -n packages/template-generator/templates/payments/dodo/web/react/tanstack-start/src/routes/payment/success.tsx.hbs 2>/dev/null || echo "File not found or unreadable"
echo
echo "=== Search for getUser implementations (corrected) ==="
find packages/template-generator/templates -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.hbs" \) | xargs grep -l "getUser" 2>/dev/null | head -20
echo
echo "=== Inspect getUser definitions ==="
find packages/template-generator/templates -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.hbs" \) | xargs grep -B2 -A8 "function\s*getUser\|const\s*getUser\|export.*getUser" 2>/dev/null | head -80Repository: AmanVarshney01/create-better-t-stack
Length of output: 2177
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find authMiddleware definition ==="
find packages/template-generator/templates -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.hbs" \) | xargs grep -l "authMiddleware" | head -10
echo
echo "=== Inspect authMiddleware implementation ==="
find packages/template-generator/templates -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.hbs" \) | xargs grep -B3 -A15 "export.*authMiddleware\|const\s*authMiddleware" 2>/dev/null | head -100
echo
echo "=== Check dashboard route for comparison (likely protected) ==="
cat packages/template-generator/templates/auth/better-auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbs 2>/dev/null | head -50
echo
echo "=== Check if other payment routes have auth guards ==="
find packages/template-generator/templates/payments -type f -name "*.hbs" | xargs grep -l "beforeLoad\|redirect" | head -5Repository: AmanVarshney01/create-better-t-stack
Length of output: 7623
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full authMiddleware definition ==="
cat packages/template-generator/templates/auth/better-auth/web/react/tanstack-start/src/middleware/auth.ts.hbsRepository: AmanVarshney01/create-better-t-stack
Length of output: 875
Add explicit auth check in beforeLoad to prevent unauthenticated access to payment success page.
The beforeLoad fetches session via getUser() but does not check if it's valid before allowing access. While the dashboard route includes an explicit if (!context.session) throw redirect({ to: "/login" }) guard in its loader, this success route has no such protection. Unauthenticated users can reach /payment/success.
Suggested fix
-import { createFileRoute, useSearch } from "@tanstack/react-router";
+import { createFileRoute, redirect, useSearch } from "@tanstack/react-router";
@@
beforeLoad: async () => {
const session = await getUser();
+ if (!session) {
+ throw redirect({ to: "/login" });
+ }
return { session };
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| beforeLoad: async () => { | |
| const session = await getUser(); | |
| return { session }; | |
| }, | |
| beforeLoad: async () => { | |
| const session = await getUser(); | |
| if (!session) { | |
| throw redirect({ to: "/login" }); | |
| } | |
| return { session }; | |
| }, |
Add Dodo Payments as a first‑class provider across CLI, template generation, web stack builder, and docs, mirroring Polar integration. Includes Dodo-specific dependencies, env vars, Better Auth wiring, protected /payment/success routes, dashboard parity across frontends, MCP recommendation, and updated CLI/web docs.
new payment providers:
Summary by CodeRabbit
New Features
--payments dodo.Documentation
Tests