-
Notifications
You must be signed in to change notification settings - Fork 1.1k
unify stripe envs + properly differentiate stripe plans #1200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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