unify stripe envs + properly differentiate stripe plans#1200
unify stripe envs + properly differentiate stripe plans#1200Brendonovich merged 4 commits intomainfrom
Conversation
WalkthroughAdds a React StripeContext providing plan IDs, integrates it into app layout, refactors checkout/upgrade flows to use React Query mutations and the new context, consolidates Stripe env vars to unified keys, and uses a single webhook secret source for Stripe webhook verification. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as Client UI
participant Ctx as StripeContext
participant API as /api/billing/subscribe
participant Auth as Auth Service
participant Stripe as Stripe Checkout
User->>UI: Click "Upgrade"
UI->>Ctx: Read planId (yearly/monthly)
UI->>API: planCheckout.mutate(planId)
API->>Auth: Verify session
alt Not authenticated
API-->>UI: { auth: false }
UI->>API: guestCheckout.mutate(planId)
API-->>UI: { url }
UI->>User: Redirect to url
else Authenticated
API-->>UI: { status, url? }
alt url provided
UI->>User: Redirect to url (Stripe)
else
UI-->>User: Show subscription status message
end
end
sequenceDiagram
autonumber
participant Stripe as Stripe
participant Webhook as /api/webhooks/stripe
participant Env as serverEnv
participant Handler as Event Handler
Stripe->>Webhook: POST (payload + signature)
Webhook->>Env: Read STRIPE_WEBHOOK_SECRET
Webhook->>Webhook: Verify signature with secret
alt valid
Webhook->>Handler: process event
Handler-->>Webhook: processed
Webhook-->>Stripe: 200 OK
else invalid
Webhook-->>Stripe: 400/401 Error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
65-91: Consider extracting nested mutation logic.The plan checkout mutation works correctly, but calling
guestCheckout.mutateAsyncinside theplanCheckoutmutation creates coupling between the two mutations.Consider extracting the authentication check and routing logic into a helper function or composing the mutations differently to improve testability and separation of concerns. The current implementation is functional but could be cleaner.
Example refactor:
const planCheckout = useMutation({ mutationFn: async () => { const planId = stripeCtx.plans[isAnnually ? "yearly" : "monthly"]; const response = await fetch(`/api/settings/billing/subscribe`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priceId: planId, quantity: users }), }); return { data: await response.json(), planId }; }, onSuccess: async ({ data, planId }) => { if (data.auth === false) { await guestCheckout.mutateAsync(planId); return; } if (data.subscription === true) { toast.success("You are already on the Cap Pro plan"); } if (data.url) { window.location.href = data.url; } }, });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/app/Layout/StripeContext.tsx(1 hunks)apps/web/app/api/webhooks/stripe/route.ts(1 hunks)apps/web/app/layout.tsx(3 hunks)apps/web/components/UpgradeModal.tsx(5 hunks)apps/web/components/pages/HomePage/Pricing/ProCard.tsx(4 hunks)apps/web/components/pages/_components/ComparePlans.tsx(4 hunks)packages/env/server.ts(1 hunks)packages/utils/src/constants/plans.ts(1 hunks)packages/utils/src/lib/stripe/stripe.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by runningpnpm format.Use strict TypeScript and avoid any; leverage shared types
Files:
packages/utils/src/lib/stripe/stripe.tsapps/web/app/layout.tsxapps/web/app/api/webhooks/stripe/route.tsapps/web/components/pages/HomePage/Pricing/ProCard.tsxpackages/env/server.tspackages/utils/src/constants/plans.tsapps/web/app/Layout/StripeContext.tsxapps/web/components/pages/_components/ComparePlans.tsxapps/web/components/UpgradeModal.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g.,user-menu.tsx).
Use PascalCase for React/Solid components.
Files:
packages/utils/src/lib/stripe/stripe.tsapps/web/app/layout.tsxapps/web/app/api/webhooks/stripe/route.tsapps/web/components/pages/HomePage/Pricing/ProCard.tsxpackages/env/server.tspackages/utils/src/constants/plans.tsapps/web/app/Layout/StripeContext.tsxapps/web/components/pages/_components/ComparePlans.tsxapps/web/components/UpgradeModal.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
On the client, always use
useEffectQueryoruseEffectMutationfrom@/lib/EffectRuntime; never callEffectRuntime.run*directly in components.
Files:
apps/web/app/layout.tsxapps/web/app/api/webhooks/stripe/route.tsapps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/pages/_components/ComparePlans.tsxapps/web/components/UpgradeModal.tsx
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for all client-side server state and fetching in the web app
Mutations should call Server Actions directly and perform targeted cache updates with setQueryData/setQueriesData
Run server-side effects via the ManagedRuntime from apps/web/lib/server.ts using EffectRuntime.runPromise/runPromiseExit; do not create runtimes ad hoc
Client code should use helpers from apps/web/lib/EffectRuntime.ts (useEffectQuery, useEffectMutation, useRpcClient); never call ManagedRuntime.make inside components
Files:
apps/web/app/layout.tsxapps/web/app/api/webhooks/stripe/route.tsapps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/pages/_components/ComparePlans.tsxapps/web/components/UpgradeModal.tsx
apps/web/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Server components needing Effect services must call EffectRuntime.runPromise(effect.pipe(provideOptionalAuth))
Files:
apps/web/app/layout.tsxapps/web/app/api/webhooks/stripe/route.tsapps/web/app/Layout/StripeContext.tsx
apps/web/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/app/api/**/*.{ts,tsx}: Prefer Server Actions for API surface; when routes are necessary, implement under app/api and export only the handler from apiToHandler(ApiLive)
Construct API routes with @effect/platform HttpApi/HttpApiBuilder, declare contracts with Schema, and only export the handler
Use HttpAuthMiddleware for required auth and provideOptionalAuth for guests; avoid duplicating session lookups
Map domain errors to transport with HttpApiError.* and keep translation exhaustive (catchTags/tapErrorCause)
Inside HttpApiBuilder.group, acquire services with Effect.gen and provide dependencies via Layer.provide instead of manual provideService
Files:
apps/web/app/api/webhooks/stripe/route.ts
🧬 Code graph analysis (6)
packages/utils/src/lib/stripe/stripe.ts (1)
packages/env/server.ts (1)
serverEnv(82-86)
apps/web/app/layout.tsx (4)
apps/web/app/Layout/StripeContext.tsx (1)
StripeContextProvider(8-17)packages/env/server.ts (1)
serverEnv(82-86)packages/utils/src/constants/plans.ts (1)
STRIPE_PLAN_IDS(3-12)apps/web/utils/public-env.tsx (1)
PublicEnvContext(11-17)
apps/web/app/api/webhooks/stripe/route.ts (1)
packages/env/server.ts (1)
serverEnv(82-86)
apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
apps/web/app/Layout/StripeContext.tsx (1)
useStripeContext(19-27)
apps/web/components/pages/_components/ComparePlans.tsx (1)
apps/web/app/Layout/StripeContext.tsx (1)
useStripeContext(19-27)
apps/web/components/UpgradeModal.tsx (1)
apps/web/app/Layout/StripeContext.tsx (1)
useStripeContext(19-27)
🔇 Additional comments (16)
packages/utils/src/lib/stripe/stripe.ts (1)
4-4: LGTM! Simplified key resolution.The change from a fallback chain to a single
STRIPE_SECRET_KEYlookup simplifies the logic and aligns with the environment consolidation. The fallback to empty string preserves the existingSTRIPE_AVAILABLE()behavior.packages/env/server.ts (1)
40-41: LGTM! Consolidated Stripe environment variables.Consolidating from four environment-specific keys to two unified keys (
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRET) simplifies configuration and reduces potential for misconfiguration.apps/web/app/api/webhooks/stripe/route.ts (1)
118-118: LGTM! Unified webhook secret source.The change to a single
STRIPE_WEBHOOK_SECRETsource simplifies the webhook verification logic and aligns with the environment consolidation.Ensure that
STRIPE_WEBHOOK_SECRETis properly configured in all deployment environments (development, staging, production) with the appropriate webhook signing secrets from your Stripe dashboard.apps/web/components/pages/_components/ComparePlans.tsx (3)
16-16: LGTM! Added Stripe context integration.Importing
useStripeContextenables context-based plan ID resolution, aligning with the new centralized approach.
97-97: LGTM! Stripe context consumption.Calling
useStripeContext()provides access to centralized plan IDs, replacing the previousgetProPlanIdhelper.
252-252: LGTM! Context-based default plan ID.Using
stripeCtx.plans.yearlyas the default plan ID is a sensible default and aligns with the new centralized configuration.apps/web/app/layout.tsx (2)
24-25: LGTM! Added Stripe context imports.Importing
STRIPE_PLAN_IDSandStripeContextProviderenables centralized plan ID management throughout the app.
116-137: Confirm VERCEL_ENV fallback and preview behavior
LocallyVERCEL_ENVis unset, so the code defaults to development plans. Verify that on Vercel preview deploymentsVERCEL_ENV="preview"and that using the development plan in both preview and local is intended. If you need a distinct plan for preview, adjust this conditional.apps/web/app/Layout/StripeContext.tsx (2)
5-6: LGTM! Clear context type definition.The
StripeContexttype clearly defines the expected structure with yearly and monthly plan IDs.
8-17: LGTM! Standard provider pattern.The provider implementation follows React best practices, accepting both children and plans through props, and conditionally providing context value.
apps/web/components/pages/HomePage/Pricing/ProCard.tsx (5)
18-19: LGTM! Added necessary imports for context and mutations.Importing
useStripeContextanduseMutationenables the migration to context-based plan IDs and mutation-driven billing flows.
22-22: LGTM! Stripe context consumption.Calling
useStripeContext()provides access to centralized plan IDs.
43-63: LGTM! Guest checkout as mutation.Converting
guestCheckoutto auseMutationhook provides better loading state management and error handling compared to the previous ad-hoc async function.
300-301: LGTM! Mutation-based loading state.Using
planCheckout.isPending || guestCheckout.isPendingfor the disabled state correctly reflects the loading status of either mutation.
305-307: LGTM! Mutation-based button text.Conditionally rendering "Loading..." based on mutation pending state provides clear user feedback during checkout operations.
packages/utils/src/constants/plans.ts (1)
3-12: Verification needed: Confirm Stripe price IDs
The script returned “NOT FOUND” for all IDs—ensureSTRIPE_SECRET_KEYis set correctly and rerun, or verify these IDs directly in your Stripe Dashboard or via the Stripe CLI.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
43-91: UseuseEffectMutationinstead ofuseMutation.Per coding guidelines for
apps/web/**/*.{ts,tsx,js,jsx}, client components must useuseEffectMutationfrom@/lib/EffectRuntimerather thanuseMutationdirectly. This ensures execution within the managed runtime and consistent error handling.Based on learnings
Refactor both mutations to use
useEffectMutation:-import { useMutation } from "@tanstack/react-query"; +import { useEffectMutation } from "@/lib/EffectRuntime"; -const guestCheckout = useMutation({ +const guestCheckout = useEffectMutation({ mutationFn: async (planId: string) => { // ... existing logic }, onError: () => { toast.error("An error occurred. Please try again."); }, }); -const planCheckout = useMutation({ +const planCheckout = useEffectMutation({ mutationFn: async () => { // ... existing logic }, });
♻️ Duplicate comments (2)
apps/web/components/UpgradeModal.tsx (2)
134-163: UseuseEffectMutationinstead ofuseMutation.Per coding guidelines for
apps/web/**/*.{ts,tsx,js,jsx}, client components must useuseEffectMutationfrom@/lib/EffectRuntimerather thanuseMutationdirectly.Based on learnings
-import { useMutation } from "@tanstack/react-query"; +import { useEffectMutation } from "@/lib/EffectRuntime"; -const planCheckout = useMutation({ +const planCheckout = useEffectMutation({ mutationFn: async () => { // ... existing logic }, });
306-307: Fix the conditional export to preserve TypeScript props.The conditional export
() => nulldrops theUpgradeModalPropssignature, breaking type checking at call sites. Type the disabled branch to accept props or explicitly annotate the export.export const UpgradeModal = - buildEnv.NEXT_PUBLIC_IS_CAP !== "true" ? () => null : UpgradeModalImpl; + buildEnv.NEXT_PUBLIC_IS_CAP !== "true" + ? (_props: UpgradeModalProps) => null + : UpgradeModalImpl;
🧹 Nitpick comments (3)
apps/web/app/Layout/StripeContext.tsx (1)
8-17: Consider makingplansrequired instead ofPartial.The provider accepts
Partial<StripeContext>but the hook throws an error if context is undefined, effectively makingplansa required prop. Since all usage sites provideplans(as seen inlayout.tsx), consider simplifying the type to make this requirement explicit:export function StripeContextProvider({ children, plans, -}: PropsWithChildren & Partial<StripeContext>) { +}: PropsWithChildren & StripeContext) { return ( - <StripeContext.Provider value={plans ? { plans } : undefined}> + <StripeContext.Provider value={{ plans }}> {children} </StripeContext.Provider> ); }apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
65-91: Add error handling toplanCheckoutmutation.The
guestCheckoutmutation includes anonErrorhandler, butplanCheckoutdoes not. Consider adding consistent error handling to provide better user feedback when the subscription flow fails.const planCheckout = useEffectMutation({ mutationFn: async () => { // ... existing logic }, + onError: () => { + toast.error("Failed to process subscription. Please try again."); + }, });apps/web/components/UpgradeModal.tsx (1)
134-163: Add error handling to the mutation.The mutation lacks error handling. Consider adding an
onErrorcallback to provide user feedback when the upgrade flow fails.const planCheckout = useEffectMutation({ mutationFn: async () => { // ... existing logic }, + onError: () => { + toast.error("Failed to process upgrade. Please try again."); + }, });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/Layout/StripeContext.tsx(1 hunks)apps/web/app/layout.tsx(3 hunks)apps/web/components/UpgradeModal.tsx(6 hunks)apps/web/components/pages/HomePage/Pricing/ProCard.tsx(4 hunks)apps/web/components/pages/_components/ComparePlans.tsx(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/components/pages/_components/ComparePlans.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by runningpnpm format.Use strict TypeScript and avoid any; leverage shared types
Files:
apps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/layout.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/UpgradeModal.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g.,user-menu.tsx).
Use PascalCase for React/Solid components.
Files:
apps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/layout.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/UpgradeModal.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
On the client, always use
useEffectQueryoruseEffectMutationfrom@/lib/EffectRuntime; never callEffectRuntime.run*directly in components.
Files:
apps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/layout.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/UpgradeModal.tsx
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for all client-side server state and fetching in the web app
Mutations should call Server Actions directly and perform targeted cache updates with setQueryData/setQueriesData
Run server-side effects via the ManagedRuntime from apps/web/lib/server.ts using EffectRuntime.runPromise/runPromiseExit; do not create runtimes ad hoc
Client code should use helpers from apps/web/lib/EffectRuntime.ts (useEffectQuery, useEffectMutation, useRpcClient); never call ManagedRuntime.make inside components
Files:
apps/web/components/pages/HomePage/Pricing/ProCard.tsxapps/web/app/layout.tsxapps/web/app/Layout/StripeContext.tsxapps/web/components/UpgradeModal.tsx
apps/web/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Server components needing Effect services must call EffectRuntime.runPromise(effect.pipe(provideOptionalAuth))
Files:
apps/web/app/layout.tsxapps/web/app/Layout/StripeContext.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components.
Applied to files:
apps/web/components/UpgradeModal.tsx
📚 Learning: 2025-10-14T10:15:44.003Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-14T10:15:44.003Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Client code should use helpers from apps/web/lib/EffectRuntime.ts (useEffectQuery, useEffectMutation, useRpcClient); never call ManagedRuntime.make inside components
Applied to files:
apps/web/components/UpgradeModal.tsx
🧬 Code graph analysis (3)
apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
apps/web/app/Layout/StripeContext.tsx (1)
useStripeContext(19-27)
apps/web/app/layout.tsx (4)
apps/web/app/Layout/StripeContext.tsx (1)
StripeContextProvider(8-17)packages/env/server.ts (1)
serverEnv(82-86)packages/utils/src/constants/plans.ts (1)
STRIPE_PLAN_IDS(3-12)apps/web/utils/public-env.tsx (1)
PublicEnvContext(11-17)
apps/web/components/UpgradeModal.tsx (2)
apps/web/app/Layout/StripeContext.tsx (1)
useStripeContext(19-27)packages/env/build.ts (1)
buildEnv(33-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
- GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
- GitHub Check: Analyze (rust)
🔇 Additional comments (4)
apps/web/app/layout.tsx (1)
116-137: LGTM! Provider hierarchy correctly structured.The StripeContextProvider integration is well-implemented:
- Plans are correctly selected based on
serverEnv().VERCEL_ENV- Provider nesting allows child components to access Stripe context
- Server-side plan resolution is appropriate for this layout component
apps/web/app/Layout/StripeContext.tsx (1)
19-27: LGTM! Hook implementation is correct.The error handling and use of React's
useAPI is appropriate for React 19.apps/web/components/pages/HomePage/Pricing/ProCard.tsx (1)
67-67: LGTM! Plan ID resolution using StripeContext.The switch from
getProPlanIdtostripeCtx.planscentralizes plan resolution and aligns with the broader refactoring.apps/web/components/UpgradeModal.tsx (1)
136-136: LGTM! Plan ID resolution using StripeContext.The centralized plan resolution through
stripeCtx.plansis consistent with the broader refactoring effort.
Summary by CodeRabbit
New Features
Refactor
Chores