Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@BilalG1
Copy link
Contributor

@BilalG1 BilalG1 commented Dec 17, 2025

Screenshot 2026-01-09 at 1 46 38 PM Screenshot 2026-01-09 at 1 49 01 PM

<!

--

Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

Summary by CodeRabbit

  • New Features

    • Manage and set default payment methods (user & team) via Setup Intents; Payments page in Account Settings to view/update card and active plans.
    • Product listings now include type (one_time | subscription) and subscription details (period end, cancelable, cancel-at-period-end).
    • Client/SDK: new billing APIs and Customer methods to fetch billing, create setup intents, and apply default payment methods.
  • Tests

    • New end-to-end tests for billing flows, setup-intent, and access control.
  • Chores

    • Added Stripe frontend libraries.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 17, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 13, 2026 8:57pm
stack-dashboard Ready Ready Preview, Comment Jan 13, 2026 8:57pm
stack-demo Ready Ready Preview, Comment Jan 13, 2026 8:57pm
stack-docs Ready Ready Preview, Comment Jan 13, 2026 8:57pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

📝 Walkthrough

Walkthrough

Adds Stripe-backed billing: backend routes to read billing, create setup intents, and set default payment methods; new Stripe/customer helpers and schema expansions; client/server interface methods; frontend Payments UI with Stripe Elements; E2E tests and Stripe deps added.

Changes

Cohort / File(s) Summary
Backend billing & payment routes
apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts, apps/backend/src/app/api/latest/payments/payment-method/.../setup-intent/route.ts, apps/backend/src/app/api/latest/payments/payment-method/.../set-default/route.ts
New GET/POST handlers for billing, setup-intent creation, and setting default payment method. Enforce auth/ownership (client vs server, team-admin checks), use tenancy-scoped Prisma/Stripe, handle missing Stripe account/customer, and return structured responses. Review access-control branches and Stripe error paths.
Backend payments library
apps/backend/src/lib/payments.tsx
Adds ensureClientCanAccessCustomer, getStripeCustomerForCustomerOrNull, ensureStripeCustomerForCustomer, getDefaultCardPaymentMethodSummary, and StripeCardPaymentMethodSummary type; extends Subscription and OwnedProduct with cancelAtPeriodEnd and subscription context; updates subscription/product flows. Review tenancy Prisma/Stripe usage and eventual-consistency fallbacks.
Products endpoint + schema
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts, packages/stack-shared/src/interface/crud/products.ts
Adds type ("one_time"
Client & Server interfaces
packages/stack-shared/src/interface/client-interface.ts, packages/stack-shared/src/interface/server-interface.ts
Added getCustomerBilling, createCustomerPaymentMethodSetupIntent, setDefaultCustomerPaymentMethodFromSetupIntent with request/response contracts. Check API surface and types for consumers.
Frontend payments UI & client app wiring
packages/template/src/components-page/account-settings.tsx, .../payments/payments-page.tsx, .../payments/payments-panel.tsx, packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, packages/template/src/lib/stack-app/customers/index.ts
New PaymentsPage/PaymentsPanel (mock + real) using Stripe Elements and setup-intent flow; Customer gains getBilling/useBilling/createPaymentMethodSetupIntent/setDefaultPaymentMethodFromSetupIntent; caching/invalidation added. Review Stripe integration, dialog flow, and cache updates.
E2E tests & snapshot handling
apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts, apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts, apps/e2e/tests/snapshot-serializer.ts
New billing E2E tests covering access control and setup-intent flows; updated product tests and snapshot stripping for current_period_end. Validate stability and redaction adjustments.
Stripe dependencies
packages/react/package.json, packages/stack/package.json, packages/template/package.json, packages/template/package-template.json
Added @stripe/react-stripe-js and @stripe/stripe-js to relevant packages. Check bundling and version alignment.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client UI
    participant API as Backend API
    participant DB as Database (Prisma)
    participant Stripe as Stripe

    UI->>API: POST /payments/payment-method/{type}/{id}/setup-intent
    API->>DB: verify project & tenancy, fetch stripeAccountId
    API->>Stripe: create SetupIntent (off_session, card)
    Stripe-->>API: client_secret, id
    API-->>UI: { client_secret, stripe_account_id }

    UI->>Stripe: confirm Card setup with client_secret (CardElement)
    Stripe-->>UI: SetupIntent succeeded

    UI->>API: POST /payments/payment-method/{type}/{id}/set-default { setup_intent_id }
    API->>Stripe: retrieve SetupIntent -> payment_method
    API->>Stripe: update Customer.default_payment_method
    API->>DB: read/write billing/customer mapping as needed
    API-->>UI: { default_payment_method: { id, brand, last4, exp_month, exp_year } }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • dashboard grant product #939 — Modifies backend payments helpers and subscription/product handling; strong overlap with payments.tsx changes.
  • Payments UX update #863 — Overlapping Stripe integration and billing/setup-intent APIs; touches similar modules and routes.

Suggested reviewers

  • N2D4

Poem

🐰 I hopped through code with nimble feet,

Cards and intents made the dance complete.
Stripe threads stitched billing's gentle art,
Default set, subscriptions take part.
A tiny rabbit cheers this payment feat!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks substantive detail about the changes. It only includes two screenshots and a CONTRIBUTING.md link, with no textual explanation of what was implemented or why. Add a clear description of the changes made, including what features were added, how they work, and relevant technical details about the implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding products display to account settings. It is concise, clear, and directly reflects the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 17, 2025

Greptile Overview

Greptile Summary

This PR adds product listing functionality to account settings, enabling users and teams to view their purchased products (both subscriptions and one-time purchases) with metadata like renewal dates and cancellation status.

Key Changes

  • Backend API: Added GET endpoint at /api/v1/payments/products/{customer_type}/{customer_id} that fetches owned products via getOwnedProductsForCustomer(), filters server-only products for client access, and supports cursor-based pagination
  • Frontend Display: Updated PaymentsPanel to fetch and display products using customer.useProducts(), showing subscription renewal dates and cancel buttons for cancelable subscriptions
  • Type Extensions: Extended CustomerProduct type to include type (one_time/subscription) and subscription metadata (currentPeriodEnd, cancelAtPeriodEnd, isCancelable)
  • Comprehensive Tests: Added 15 test cases covering granting, listing, cancellation, pagination, server-only filtering, and error scenarios

Confidence Score: 5/5

  • Safe to merge - well-tested feature addition with proper authorization and comprehensive test coverage
  • Clean implementation following existing patterns with SmartRouteHandler, proper access control filtering server-only products from clients, comprehensive E2E tests covering edge cases, and correct integration with existing subscription cancellation flow
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts 5/5 Added GET endpoint to list customer products with pagination, filtering server-only products for client access
apps/backend/src/lib/payments.tsx 5/5 Added getOwnedProductsForCustomer function to fetch both subscription and one-time purchase products
packages/template/src/components-page/account-settings/payments/payments-panel.tsx 5/5 Updated to display products list with subscription details and cancellation functionality
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts 5/5 Updated product listing to map subscription fields from API response
apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts 5/5 Comprehensive test coverage for product listing, granting, and cancellation

Sequence Diagram

sequenceDiagram
    participant User as User/Browser
    participant Panel as PaymentsPanel
    participant App as StackClientApp
    participant API as Backend API
    participant DB as Database
    
    User->>Panel: View Account Settings
    Panel->>App: useProducts()
    App->>API: GET /api/v1/payments/products/{customer_type}/{customer_id}
    API->>DB: getOwnedProductsForCustomer()
    DB-->>API: Return subscriptions + one-time purchases
    API->>API: Filter server-only products (if client auth)
    API->>API: Sort by createdAt & apply pagination
    API-->>App: Return products list with subscription metadata
    App-->>Panel: Display products with details
    Panel-->>User: Show products with renewal dates & cancel buttons
    
    alt Cancel Subscription
        User->>Panel: Click "Cancel subscription"
        Panel->>App: cancelSubscription({productId})
        App->>API: DELETE /api/v1/payments/products/{customer_type}/{customer_id}
        API->>API: Verify user owns product & permissions
        API->>DB: Find active subscription
        alt Has Stripe subscription
            API->>Stripe: Cancel subscription
        end
        API->>DB: Update subscription status to canceled
        DB-->>API: Success
        API-->>App: {success: true}
        App->>App: Invalidate products cache
        App-->>Panel: Refresh products list
        Panel-->>User: Show updated products (removed)
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@BilalG1 BilalG1 requested a review from N2D4 December 18, 2025 02:40
@cmux-agent
Copy link

cmux-agent bot commented Jan 6, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
Contributor

@N2D4 N2D4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very exciting :]

@BilalG1 BilalG1 changed the base branch from payments-update-billing-info to payouts-tab January 9, 2026 19:12
@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Base automatically changed from payouts-tab to dev January 9, 2026 20:04
@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
@apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts:
- Around line 10-31: The function ensureClientCanAccessCustomer is duplicated;
extract it into a single shared module (e.g., export a named function
ensureClientCanAccessCustomer) and have this file and the other duplicates
import it instead of redefining it. Move/keep its dependencies and types
(KnownErrors.UserAuthenticationRequired, StatusError, getPrismaClientForTenancy,
ensureUserTeamPermissionExists) imported by the new module, preserve the same
signature { customer_type: "user" | "team", customer_id: string }, fullReq,
tenancy and behavior, export it, then replace the duplicate implementations with
an import and call to the shared ensureClientCanAccessCustomer to remove
redundancy.

In @apps/backend/src/lib/payments.tsx:
- Around line 431-433: The file is missing the CustomerType enum import used by
customerTypeToStripeCustomerType; update the existing import from
"@/generated/prisma/client" to include CustomerType so
customerTypeToStripeCustomerType can reference CustomerType.USER and
CustomerType.TEAM without errors (add CustomerType to the import list alongside
PurchaseCreationSource and SubscriptionStatus).

In
@packages/template/src/components-page/account-settings/payments/payments-panel.tsx:
- Line 65: The direct access to document.documentElement.style when computing
the darkMode constant in payments-panel.tsx is SSR-unsafe; change this to
client-side detection by replacing the top-level const darkMode with state and
effect (e.g., useState for darkMode and useEffect to read
document.documentElement.style or matchMedia on mount), or use a SSR-safe hook
like a usePrefersColorScheme/useMediaQuery to set darkMode inside useEffect so
no document access happens during server render; update any uses of darkMode in
the component to read from the new state/hook.
- Around line 80-104: The Button's inline async onClick handler should be
replaced to call runAsynchronouslyWithAlert with an async callback that contains
the existing logic (the checks for stripe/elements, const card =
elements.getElement(CardElement), stripe.confirmCardSetup call, result/error
handling, and props.onSetupIntentSucceeded), so errors are surfaced via the
shared alert mechanism; preserve all setErrorMessage calls and early returns
inside that async callback and pass the async function (not a promise) to
runAsynchronouslyWithAlert where the onClick currently is.
🧹 Nitpick comments (7)
apps/backend/src/lib/payments.tsx (2)

456-458: Consider using parameterized query pattern for Stripe search.

The customerId is directly interpolated into the Stripe search query string. While the upstream ensureCustomerExists validates it's a UUID (which contains only safe characters), direct string interpolation in queries is generally discouraged. Stripe's search API doesn't support parameterized queries, but this is worth noting for awareness.


461-480: Fallback pagination could be slow for accounts with many customers.

The fallback loop iterates up to 10 pages (1000 customers) when Stripe's search returns no results due to eventual consistency. For accounts with many customers, this could introduce latency. Consider adding a log or metric when this fallback path is taken to monitor its frequency.

packages/template/src/components-page/account-settings/payments/payments-page.tsx (1)

8-36: Consider simplifying the conditional logic.

The component correctly handles both mock and real modes. However, the if (!user) check on lines 23-25 appears to be unreachable. When or: "redirect" is used (line 10 in non-mock mode), useUser will redirect and suspend rather than return null. Since mock mode is handled by the early return (lines 12-21), the null check is defensive but unnecessary.

♻️ Simplified implementation
 export function PaymentsPage(props: { mockMode?: boolean }) {
   const { t } = useTranslation();
-  const user = useUser({ or: props.mockMode ? "return-null" : "redirect" });
 
   if (props.mockMode) {
     return (
       <PageLayout>
         <PaymentsPanel
           title={t("Personal payments")}
           mockMode
         />
       </PageLayout>
     );
   }
 
-  if (!user) {
-    return null;
-  }
+  const user = useUser({ or: "redirect" });
 
   return (
     <PageLayout>
       <PaymentsPanel
         title={t("Personal payments")}
         customer={user}
         customerType="user"
       />
     </PageLayout>
   );
 }
apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts (1)

10-31: Avoid any types for fullReq and tenancy parameters.

The any types on fullReq and tenancy bypass type safety. Consider extracting proper types from the smart route handler's request schema or using the actual Tenancy type from prisma-client.

Also, this ensureClientCanAccessCustomer helper is duplicated across at least three route files (billing, setup-intent, set-default). Consider extracting it to a shared module (e.g., @/lib/payments or @/lib/request-checks).

apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts (1)

87-94: Redundant stripeAccountId check.

getStripeForAccount({ tenancy: auth.tenancy }) on line 69 already throws if stripeAccountId is missing (see apps/backend/src/lib/stripe.tsx lines 47-50). This check on lines 87-94 is unreachable—if payments weren't set up, line 69 would have already failed.

Consider either:

  1. Removing this redundant check, or
  2. Moving the check before the Stripe calls to fail fast with a cleaner 400 error
Option: Check early and avoid redundancy
   handler: async ({ auth, params }, fullReq) => {
     if (auth.type === "client") {
       await ensureClientCanAccessCustomer(
         { customer_type: params.customer_type, customer_id: params.customer_id },
         fullReq,
         auth.tenancy,
       );
     }

+    const project = await globalPrismaClient.project.findUnique({
+      where: { id: auth.tenancy.project.id },
+      select: { stripeAccountId: true },
+    });
+    const stripeAccountId = project?.stripeAccountId;
+    if (!stripeAccountId) {
+      throw new StatusError(400, "Payments are not set up in this Stack Auth project.");
+    }
+
     const prisma = await getPrismaClientForTenancy(auth.tenancy);
-    const stripe = await getStripeForAccount({ tenancy: auth.tenancy });
+    const stripe = await getStripeForAccount({ accountId: stripeAccountId });
     // ... rest of handler ...

-    const project = await globalPrismaClient.project.findUnique({
-      where: { id: auth.tenancy.project.id },
-      select: { stripeAccountId: true },
-    });
-    const stripeAccountId = project?.stripeAccountId;
-    if (!stripeAccountId) {
-      throw new StatusError(400, "Payments are not set up in this Stack Auth project.");
-    }

     return {
       statusCode: 200,
       bodyType: "json",
       body: {
         client_secret: setupIntent.client_secret,
-        stripe_account_id: stripeAccountId,
+        stripe_account_id: stripeAccountId,
       },
     };
   },
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)

12-56: Consider importing types from customers/index.ts instead of duplicating.

These type definitions (PaymentMethodSummary, CustomerBilling, CustomerPaymentMethodSetupIntent, CustomerLike) largely duplicate types from packages/template/src/lib/stack-app/customers/index.ts. Consider importing and reusing to avoid drift.

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

1625-1672: The cache inefficiency concern is valid but represents a minor design trade-off rather than a critical issue.

When session is null, a new effectiveSession object is created each time _createCustomer is called. Since the cache key includes effectiveSession and cache keys are compared by object identity (via WeakMap), each null-session call results in a separate cache entry. This means billing data won't be cached across multiple Customer instances for null sessions.

This is likely intentional:

  • Server-side usage (where null sessions occur): The Customer objects are created once per User/Team, so the caching inefficiency has minimal impact
  • Client-side usage: Always provides a real session, so this doesn't apply
  • Backend enforcement: Access control is enforced server-side regardless of session state

The pattern session ?? createSession(...) differs from the defensive ?? throwErr(...) pattern in the codebase, but creating a fallback session is acceptable here since billing operations require proper authentication at the backend layer.

Optional improvement: Cache a singleton anonymous session at the app level to reuse for all null cases, improving cache efficiency. However, this is a low-priority optimization.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 502963b and 3ddf71f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/lib/payments.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
  • apps/e2e/tests/snapshot-serializer.ts
  • packages/react/package.json
  • packages/stack-shared/src/interface/client-interface.ts
  • packages/stack-shared/src/interface/crud/products.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack/package.json
  • packages/template/package-template.json
  • packages/template/package.json
  • packages/template/src/components-page/account-settings.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/customers/index.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For blocking alerts and errors, never use toast; instead, use alerts as toasts are easily missed by the user

Files:

  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • packages/stack-shared/src/interface/crud/products.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts
  • packages/template/src/lib/stack-app/customers/index.ts
  • apps/e2e/tests/snapshot-serializer.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • apps/backend/src/lib/payments.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{tsx,css}: Keep hover/click animations snappy and fast; don't delay actions with pre-transitions (e.g., no fade-in on button hover) as it makes UI feel sluggish; instead apply transitions after the action like smooth fade-out when hover ends
When creating hover transitions, avoid hover-enter transitions and use only hover-exit transitions (e.g., transition-colors hover:transition-none)

Files:

  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if avoidable; prefer using client components instead to keep pages static (e.g., use usePathname instead of await params)

Files:

  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • packages/stack-shared/src/interface/crud/products.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts
  • packages/template/src/lib/stack-app/customers/index.ts
  • apps/e2e/tests/snapshot-serializer.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • apps/backend/src/lib/payments.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error) or similar; use loading indicators instead; if asynchronous handling is necessary, use runAsynchronously or runAsynchronouslyWithAlert instead
Use ES6 maps instead of records wherever possible

Files:

  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • packages/stack-shared/src/interface/crud/products.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts
  • packages/template/src/lib/stack-app/customers/index.ts
  • apps/e2e/tests/snapshot-serializer.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • apps/backend/src/lib/payments.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Code defensively; prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating violated assumptions
Avoid the any type; when necessary, leave a comment explaining why it's used, why the type system fails, and how errors would be caught at compile-, test-, or runtime

Files:

  • packages/template/src/components-page/account-settings/teams/team-payments-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-page.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • packages/stack-shared/src/interface/crud/products.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts
  • packages/template/src/lib/stack-app/customers/index.ts
  • apps/e2e/tests/snapshot-serializer.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • apps/backend/src/lib/payments.tsx
**/e2e/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

ALWAYS add new E2E tests when changing the API or SDK interface; err on the side of creating too many tests due to the critical nature of the authentication industry

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

When writing tests, prefer .toMatchInlineSnapshot() over other selectors if possible; check snapshot-serializer.ts to understand how snapshots are formatted and how non-deterministic values are handled

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
🧠 Learnings (6)
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to {packages/stack,packages/js}/**/*.{ts,tsx,js,jsx} : NEVER UPDATE packages/stack OR packages/js; instead, update packages/template as those packages are copies of it

Applied to files:

  • packages/template/package-template.json
  • packages/template/package.json
  • packages/stack/package.json
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: The project uses Next.js with App Router framework for the backend, dashboard, and dev-launchpad apps

Applied to files:

  • packages/template/package-template.json
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/e2e/**/*.{test,spec}.{ts,tsx,js,jsx} : ALWAYS add new E2E tests when changing the API or SDK interface; err on the side of creating too many tests due to the critical nature of the authentication industry

Applied to files:

  • apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/config/schema.ts,**/config/**/*.{ts,tsx} : Whenever making backwards-incompatible changes to the config schema, update the migration functions in `packages/stack-shared/src/config/schema.ts`

Applied to files:

  • packages/stack-shared/src/interface/crud/products.ts
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings.tsx
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : When writing tests, prefer `.toMatchInlineSnapshot()` over other selectors if possible; check snapshot-serializer.ts to understand how snapshots are formatted and how non-deterministic values are handled

Applied to files:

  • apps/e2e/tests/snapshot-serializer.ts
🧬 Code graph analysis (12)
packages/template/src/components-page/account-settings/teams/team-payments-section.tsx (4)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
  • useUser (1992-2034)
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)
  • PaymentsPanel (112-125)
packages/template/src/components-page/account-settings/section.tsx (1)
  • Section (3-22)
packages/template/src/components-page/account-settings/teams/team-page.tsx (1)
packages/template/src/components-page/account-settings/teams/team-payments-section.tsx (1)
  • TeamPaymentsSection (9-34)
apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts (2)
apps/e2e/tests/helpers.ts (1)
  • it (12-12)
apps/e2e/tests/backend/backend-helpers.ts (1)
  • niceBackendFetch (109-173)
apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts (7)
packages/stack-shared/src/utils/errors.tsx (1)
  • StatusError (152-261)
apps/backend/src/prisma-client.tsx (1)
  • getPrismaClientForTenancy (67-69)
apps/backend/src/lib/request-checks.tsx (1)
  • ensureUserTeamPermissionExists (82-115)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (216-301)
packages/stack-shared/src/schema-fields.ts (3)
  • clientOrHigherAuthTypeSchema (534-534)
  • adaptSchema (330-330)
  • yupString (187-190)
apps/backend/src/lib/stripe.tsx (1)
  • getStripeForAccount (29-54)
apps/backend/src/lib/payments.tsx (1)
  • ensureStripeCustomerForCustomer (493-511)
packages/template/src/components-page/account-settings/payments/payments-page.tsx (3)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
  • useUser (1992-2034)
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)
  • PaymentsPanel (112-125)
apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts (5)
apps/backend/src/prisma-client.tsx (1)
  • getPrismaClientForTenancy (67-69)
apps/backend/src/lib/request-checks.tsx (1)
  • ensureUserTeamPermissionExists (82-115)
packages/stack-shared/src/schema-fields.ts (3)
  • clientOrHigherAuthTypeSchema (534-534)
  • adaptSchema (330-330)
  • yupString (187-190)
apps/backend/src/lib/stripe.tsx (1)
  • getStripeForAccount (29-54)
apps/backend/src/lib/payments.tsx (2)
  • getStripeCustomerForCustomerOrNull (435-491)
  • getDefaultCardPaymentMethodSummary (521-540)
apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts (8)
packages/stack-shared/src/utils/errors.tsx (1)
  • StatusError (152-261)
apps/backend/src/prisma-client.tsx (1)
  • getPrismaClientForTenancy (67-69)
apps/backend/src/lib/request-checks.tsx (1)
  • ensureUserTeamPermissionExists (82-115)
apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts (1)
  • POST (33-105)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (216-301)
packages/stack-shared/src/schema-fields.ts (5)
  • yupObject (247-251)
  • clientOrHigherAuthTypeSchema (534-534)
  • adaptSchema (330-330)
  • yupString (187-190)
  • yupNumber (191-194)
apps/backend/src/lib/stripe.tsx (1)
  • getStripeForAccount (29-54)
apps/backend/src/lib/payments.tsx (2)
  • ensureStripeCustomerForCustomer (493-511)
  • getDefaultCardPaymentMethodSummary (521-540)
packages/template/src/lib/stack-app/customers/index.ts (1)
packages/template/src/lib/stack-app/common.ts (1)
  • AsyncStoreProperty (9-11)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
  • createCacheBySession (36-46)
  • useAsyncCache (162-213)
packages/template/src/lib/stack-app/customers/index.ts (3)
  • CustomerBilling (72-75)
  • CustomerPaymentMethodSetupIntent (77-80)
  • CustomerDefaultPaymentMethod (64-70)
packages/stack-shared/src/interface/server-interface.ts (2)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (81-288)
packages/stack-shared/src/utils/urls.tsx (1)
  • urlString (314-316)
packages/stack-shared/src/interface/client-interface.ts (2)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (81-288)
packages/stack-shared/src/utils/urls.tsx (1)
  • urlString (314-316)
apps/backend/src/lib/payments.tsx (3)
apps/backend/src/prisma-client.tsx (1)
  • PrismaClientTransaction (25-27)
packages/template/src/lib/stack-app/customers/index.ts (1)
  • Customer (82-117)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
🪛 Biome (2.1.2)
packages/template/package-template.json

[error] 89-89: The key // was already declared.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

⏰ 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). (13)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
  • GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: docker
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: setup-tests-with-custom-base-port
  • GitHub Check: all-good
🔇 Additional comments (43)
apps/e2e/tests/snapshot-serializer.ts (1)

73-73: LGTM! Correctly strips non-deterministic subscription timestamp.

Adding current_period_end to the strip list is the right approach for handling this subscription period timestamp field, ensuring test snapshots remain deterministic and consistent with the existing pattern of stripping timestamp values.

Based on learnings, this aligns well with the preference for .toMatchInlineSnapshot() testing by ensuring non-deterministic values are properly serialized.

packages/react/package.json (1)

61-62: LGTM!

The Stripe dependencies are correctly added and align with the template configuration.

packages/template/package-template.json (1)

89-92: LGTM!

The Stripe dependencies are correctly scoped to react-like platforms using the established templating pattern. The static analysis warning about duplicate "//" keys is a false positive—this is the intentional platform-conditional templating mechanism used throughout this file.

packages/template/package.json (1)

66-67: LGTM!

Stripe dependencies are correctly added to the template package.

apps/backend/src/lib/payments.tsx (1)

759-771: LGTM!

The subscription field extension to OwnedProduct is well-designed. The isCancelable logic correctly identifies that only subscriptions with both a database ID and product ID can be canceled (excluding default catalog subscriptions and inline products).

packages/stack/package.json (1)

61-62: LGTM!

The Stripe dependencies are correctly propagated from the template. Based on learnings, this file is auto-generated from packages/template, so these changes should be the result of template generation.

packages/template/src/components-page/account-settings/teams/team-page.tsx (2)

10-10: LGTM!

The import is correctly added.


21-21: LGTM!

The TeamPaymentsSection is correctly integrated into the page layout with the appropriate key prop pattern.

packages/template/src/components-page/account-settings/teams/team-payments-section.tsx (2)

9-22: LGTM!

The component correctly checks for team admin permissions before rendering the PaymentsPanel. The permission check pattern using user.usePermission is consistent with the codebase's established patterns.


24-33: LGTM!

The fallback UI for non-admin users provides clear messaging about the required permissions, following the established Section component pattern.

apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts (1)

130-135: LGTM! Comprehensive test coverage for new subscription fields.

The test snapshots have been correctly updated to reflect the new API shape with type and subscription fields. The coverage properly distinguishes between subscription products (with subscription metadata) and one-time purchases (with subscription: null), and spans all critical scenarios including granting, listing, canceling, server-only products, stackable products, inline products, and pagination.

Based on learnings, this aligns with the requirement to add E2E tests when changing API interfaces.

Also applies to: 530-535, 666-671, 744-749, 1049-1054, 1070-1071, 1191-1196, 1229-1230, 1246-1247

apps/e2e/tests/backend/endpoints/api/v1/payments/billing.test.ts (4)

4-22: LGTM! Proper coverage for the no-customer scenario.

The test correctly verifies that when payments are not set up, the billing endpoint returns has_customer: false with a null default payment method.


24-68: LGTM! Comprehensive flow testing for authenticated billing access.

The test properly verifies the complete flow: initial billing status, setup-intent creation with proper field validation, and billing status after setup. The use of toMatchObject for the setup-intent response is appropriate since the exact values are non-deterministic.


70-92: LGTM! Proper authorization checks in place.

The test correctly verifies that a signed-in user cannot read another user's billing information (403), while being able to read their own (200). This validates the tenancy-based access control.


94-137: LGTM! Thorough team permission validation.

The test properly validates that team admins can manage team billing while regular members cannot, with appropriate error structure including permission_id, team_id, and user_id in the response details.

Based on learnings, this test suite provides excellent coverage for the new billing API endpoints.

packages/stack-shared/src/interface/crud/products.ts (1)

8-13: LGTM! Schema properly extended for subscription metadata.

The schema additions correctly introduce:

  • A type discriminator field to distinguish between one-time and subscription products
  • A subscription field (nullable) to carry subscription-specific metadata like cancellation status and period information

This aligns with the backend implementation and test expectations.

apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts (1)

57-62: LGTM! Backend implementation correctly transforms subscription data.

The implementation properly:

  • Adds the type field from the product model
  • Conditionally maps subscription metadata when present, with proper date conversion to ISO string
  • Returns null for the subscription field when the product has no subscription

This aligns with the shared schema and test expectations.

apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts (3)

64-71: Server/admin auth types bypass customer access checks.

The handler only validates access when auth.type === "client". Confirm this is intentional—server/admin keys can access any customer's billing info without ownership verification.


73-87: LGTM: Graceful handling when payments not configured.

Returning has_customer: false when stripeAccountId is missing is a clean approach that allows clients to handle this state gracefully.


89-123: LGTM: Stripe customer lookup and response.

The flow correctly handles the case where no Stripe customer exists, and the response structure aligns with the declared schema.

apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts (3)

78-84: Verify behavior: ensureStripeCustomerForCustomer creates customer if missing.

This endpoint will create a Stripe customer if one doesn't exist (per ensureStripeCustomerForCustomer in apps/backend/src/lib/payments.tsx). Confirm this is desired—it differs from the billing GET endpoint which returns has_customer: false when no customer exists.


86-95: LGTM: Thorough setup intent validation.

Good defensive checks: verifying customer ownership, succeeded status, and presence of payment method before proceeding.


103-106: LGTM: Handles deleted customer edge case.

Checking updatedCustomer.deleted after the update protects against a race condition where the customer is deleted between update and retrieve.

apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts (1)

78-85: LGTM: Setup intent creation.

Correctly creates setup intent with off_session usage for future charges and validates client_secret is returned.

packages/template/src/components-page/account-settings.tsx (3)

83-91: LGTM: Mock billing and conditional rendering setup.

Clean integration of billing data with proper fallback for mock mode and conditional rendering based on hasCustomer.


146-154: LGTM: Payments section integration.

Proper Suspense boundary with skeleton fallback, consistent with other sections like API Keys.


241-248: LGTM: PaymentsPageSkeleton.

Skeleton layout is consistent with other page skeletons in the file.

packages/template/src/lib/stack-app/customers/index.ts (3)

42-47: LGTM: Extended CustomerProduct type.

Clean extension with subscription metadata that enables proper UI rendering of renewal dates and cancel actions.


64-80: LGTM: New billing-related types.

Well-structured types that align with the backend API response schemas.


90-100: LGTM: Customer interface extensions.

New methods and billing store property follow existing patterns in the codebase.

packages/template/src/components-page/account-settings/payments/payments-panel.tsx (2)

189-194: Verify NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY is documented.

The Stripe publishable key is read from an environment variable. Ensure this is documented in setup instructions, as missing it will silently disable payment method updates (stripePromise becomes null).


304-326: LGTM: Subscription cancellation flow.

Proper confirmation dialog with danger styling and async handling inside okButton.onClick.

packages/stack-shared/src/interface/server-interface.ts (3)

61-81: LGTM: getCustomerBilling implementation.

Follows existing patterns in the interface, properly uses urlString for URL encoding, and return type matches the backend schema.


83-103: LGTM: createCustomerPaymentMethodSetupIntent implementation.

Correctly sends POST with empty JSON body and appropriate headers.


105-133: LGTM: setDefaultCustomerPaymentMethodFromSetupIntent implementation.

Properly serializes the setup_intent_id in the request body.

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (4)

272-285: LGTM: Billing cache properly structured.

The billing cache follows the established pattern using createCacheBySession with appropriate dependencies and type safety.


1163-1179: LGTM: Product mapping correctly extended with subscription data.

The new type and subscription fields are properly mapped with appropriate null handling and date conversion.


1181-1195: LGTM: Billing response mapper is correctly implemented.

The helper properly transforms the backend response format to the public CustomerBilling type.


1630-1651: LGTM: Billing methods correctly implemented with proper caching.

The new billing methods follow established patterns:

  • Cache usage for reads (getBilling, useBilling)
  • Proper cache refresh after mutations (setDefaultPaymentMethodFromSetupIntent)
  • Consistent use of effectiveSession across all billing operations
  • Type-safe return values matching public interfaces
packages/stack-shared/src/interface/client-interface.ts (4)

1851-1871: LGTM: Billing retrieval method correctly implemented.

The getCustomerBilling method follows established patterns with safe URL construction and appropriate return types.


1873-1893: LGTM: Setup intent creation properly implemented.

The method correctly creates a Stripe SetupIntent for adding payment methods. The empty request body is appropriate since all necessary information (customer type and ID) is in the URL path.


1895-1923: LGTM: Default payment method setter correctly implemented.

The method properly sets the default payment method from a SetupIntent with appropriate parameter passing and return type.


1851-1923: No changes needed. The customer type restriction is intentional and correctly enforced.

The billing methods (getCustomerBilling, createCustomerPaymentMethodSetupIntent, setDefaultCustomerPaymentMethodFromSetupIntent) properly restrict to "user" | "team" because billing/payment management requires authenticated customers. Custom customers are designed for external/unregistered access to products only. This design is consistently enforced at the backend layer with explicit access control checks ("Clients can only manage their own billing."). The type difference between billing methods and product operations is correct by design.

Copy link

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The CustomerType enum is used in the code but not imported, which will cause a compilation error.

View Details
📝 Patch Details
diff --git a/apps/backend/src/lib/payments.tsx b/apps/backend/src/lib/payments.tsx
index f4aa0958..a7b1a57c 100644
--- a/apps/backend/src/lib/payments.tsx
+++ b/apps/backend/src/lib/payments.tsx
@@ -1,5 +1,5 @@
 import { PrismaClientTransaction } from "@/prisma-client";
-import { PurchaseCreationSource, SubscriptionStatus } from "@/generated/prisma/client";
+import { PurchaseCreationSource, SubscriptionStatus, CustomerType } from "@/generated/prisma/client";
 import { KnownErrors } from "@stackframe/stack-shared";
 import type { inlineProductSchema, productSchema, productSchemaWithMetadata } from "@stackframe/stack-shared/dist/schema-fields";
 import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants";

Analysis

Missing CustomerType import in payments.tsx

What fails: TypeScript compilation error - CustomerType enum used but not imported in customerTypeToStripeCustomerType() function at lines 431-432

How to reproduce:

cd apps/backend
pnpm install
STACK_DATABASE_CONNECTION_STRING="placeholder" pnpm run prisma generate
pnpm run typecheck

Result:

src/lib/payments.tsx(431,75): error TS2304: Cannot find name 'CustomerType'.
src/lib/payments.tsx(432,36): error TS2552: Cannot find name 'CustomerType'. Did you mean 'customerType'?
src/lib/payments.tsx(432,56): error TS2552: Cannot find name 'CustomerType'. Did you mean 'customerType'?

Expected: TypeScript compilation should pass. The CustomerType enum is defined in the Prisma schema and generated in @/generated/prisma/client, but the import statement on line 2 does not include it despite importing PurchaseCreationSource and SubscriptionStatus from the same module.

Fix: Add CustomerType to the import statement on line 2:

import { PurchaseCreationSource, SubscriptionStatus, CustomerType } from "@/generated/prisma/client";

@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
@packages/template/src/components-page/account-settings/payments/payments-page.tsx:
- Around line 24-30: The hook calls are placed after an early return causing a
Rules of Hooks violation; move the call to user.useTeams() and the useState hook
for selectedTeam so they execute unconditionally before the if (!user) return
null; check (i.e., call user.useTeams(), compute hasTeams, and call
useState<Team | null>(...) for selectedTeam/setSelectedTeam above the
conditional return) to ensure all hooks run in the same order on every render.

In
@packages/template/src/components-page/account-settings/payments/payments-panel.tsx:
- Line 65: The expression assigning darkMode accesses
document.documentElement.style during render which breaks SSR; update this by
guarding for a browser environment (e.g., check typeof window !== "undefined" or
typeof document !== "undefined") before reading document, or move the detection
into a useEffect and store the result in state (create a darkMode state and set
it inside useEffect) so no document access happens during server rendering;
adjust any consumers of darkMode accordingly (the variable name darkMode and its
assignment should be replaced with the guarded or effect-based logic).
- Around line 80-107: The Button's async onClick handler in PaymentsPanel is
being used directly; wrap the async logic with runAsynchronouslyWithAlert to
ensure consistent error handling and UI alerts. Replace the inline async arrow
passed to Button.onClick with a call to runAsynchronouslyWithAlert(() => { ...
}) containing the existing logic that references
elements.getElement(CardElement), stripe.confirmCardSetup(props.clientSecret,
...), setErrorMessage(...), and the final await
props.onSetupIntentSucceeded(result.setupIntent.id); ensure you return the
Promise from runAsynchronouslyWithAlert so Button.onClick receives a function
(not a bare async handler).
- Line 291: Replace the non-null assertion product.id! passed into
setCancelProductId with a defensive check that throws on null/undefined per
guidelines; e.g., use product.id ?? throwErr("Missing product id for
cancellation") when calling setCancelProductId so setCancelProductId receives a
guaranteed non-null value and avoids the `!` operator, referencing the
setCancelProductId call site and product.id.
- Around line 311-323: The dialog okButton's async onClick handler uses raw
await calls and should be wrapped with runAsynchronouslyWithAlert for consistent
error handling; replace the current onClick async function (which references
cancelProductId, props.customerType, stackApp.cancelSubscription, and
setCancelProductId) with a call like onClick: () =>
runAsynchronouslyWithAlert(async () => { const productId = cancelProductId; if
(!productId) return; if (props.customerType === "team") { await
stackApp.cancelSubscription({ teamId: props.customer.id, productId }); } else {
await stackApp.cancelSubscription({ productId }); } setCancelProductId(null);
}); and ensure runAsynchronouslyWithAlert is imported where payments-panel.tsx
defines okButton.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ddf71f and a6865f7.

📒 Files selected for processing (4)
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For blocking alerts and errors, never use toast; instead, use alerts as toasts are easily missed by the user

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{tsx,css}: Keep hover/click animations snappy and fast; don't delay actions with pre-transitions (e.g., no fade-in on button hover) as it makes UI feel sluggish; instead apply transitions after the action like smooth fade-out when hover ends
When creating hover transitions, avoid hover-enter transitions and use only hover-exit transitions (e.g., transition-colors hover:transition-none)

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if avoidable; prefer using client components instead to keep pages static (e.g., use usePathname instead of await params)

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error) or similar; use loading indicators instead; if asynchronous handling is necessary, use runAsynchronously or runAsynchronouslyWithAlert instead
Use ES6 maps instead of records wherever possible

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Code defensively; prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating violated assumptions
Avoid the any type; when necessary, leave a comment explaining why it's used, why the type system fails, and how errors would be caught at compile-, test-, or runtime

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
🧠 Learnings (2)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/account-settings.tsx
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error) or similar; use loading indicators instead; if asynchronous handling is necessary, use `runAsynchronously` or `runAsynchronouslyWithAlert` instead

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🧬 Code graph analysis (3)
packages/template/src/components-page/account-settings.tsx (2)
packages/template/src/components-page/account-settings/payments/payments-page.tsx (1)
  • PaymentsPage (10-52)
packages/template/src/components-page/account-settings/page-layout.tsx (1)
  • PageLayout (1-7)
apps/backend/src/lib/payments.tsx (3)
apps/backend/src/prisma-client.tsx (1)
  • PrismaClientTransaction (25-27)
packages/template/src/lib/stack-app/customers/index.ts (1)
  • Customer (82-117)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
packages/template/src/components-page/account-settings/payments/payments-page.tsx (3)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
  • useUser (1992-2034)
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)
  • PaymentsPanel (112-125)
🪛 Biome (2.1.2)
packages/template/src/components-page/account-settings/payments/payments-page.tsx

[error] 28-28: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 30-30: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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). (13)
  • GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
  • GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests-with-custom-base-port
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: restart-dev-and-test
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: all-good
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (2)
packages/template/src/components-page/account-settings.tsx (1)

4-4: LGTM!

The integration of the Payments section follows the existing patterns correctly, with proper Suspense boundaries, skeleton loading states, and mock data support.

Also applies to: 16-16, 28-29, 85-85, 90-90, 145-153, 240-247

apps/backend/src/lib/payments.tsx (1)

3-3: LGTM!

The backend payment utilities are well-implemented with proper error handling, defensive coding, and type safety. The Stripe customer resolution logic correctly handles eventual consistency with a search-then-paginate fallback strategy.

Also applies to: 269-269, 306-306, 335-335, 354-354, 432-541, 767-772, 819-823, 836-836

@cmux-agent
Copy link

cmux-agent bot commented Jan 9, 2026

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @apps/backend/src/lib/payments.tsx:
- Around line 561-564: The code calls
options.stripe.paymentMethods.retrieve(defaultPaymentMethodId) which will throw
if the payment method was deleted; wrap that call in a try/catch around the
retrieval in the same function, catch Stripe errors (or any error) and return
null when retrieval fails, otherwise continue to check pm.type and pm.card as
before; reference the retrieve call (options.stripe.paymentMethods.retrieve),
the defaultPaymentMethodId variable, and the subsequent pm.type/pm.card check so
you modify the exact block to handle missing/deleted payment methods gracefully.

In
@packages/template/src/components-page/account-settings/payments/payments-panel.tsx:
- Around line 312-324: The okButton.onClick async handler currently calls
stackApp.cancelSubscription directly; update it to wrap the cancellation logic
in runAsynchronouslyWithAlert by passing an async function that grabs
cancelProductId, returns early if missing, calls stackApp.cancelSubscription
with { teamId: props.customer.id, productId } when props.customerType === "team"
or { productId } otherwise, and on success calls setCancelProductId(null); this
preserves existing behavior but ensures errors are surfaced via
runAsynchronouslyWithAlert.
- Around line 81-108: The onClick handler on the Save payment method Button uses
an inline async function with manual error handling; replace it by wrapping the
async flow in runAsynchronouslyWithAlert from
@stackframe/stack-shared/dist/utils/promises so errors surface via the app alert
system. Move the existing logic (checks for stripe/elements,
elements.getElement(CardElement), calling stripe.confirmCardSetup, setting
inline validation via setErrorMessage, and calling props.onSetupIntentSucceeded)
into a function passed to runAsynchronouslyWithAlert, keeping setErrorMessage
for client-side validation but removing manual catch/alert logic so
runAsynchronouslyWithAlert handles exceptions and displays alerts consistently.
- Line 66: The line that reads const darkMode = "color-scheme" in
document.documentElement.style && document.documentElement.style["color-scheme"]
=== "dark" is SSR-unsafe because it accesses document during render; change it
to a client-side-only value by introducing a React state (e.g., darkMode via
useState) and set its value inside a useEffect (or compute it in useMemo inside
useEffect) so the document access runs only on the client; update any usages of
darkMode in the component to read from that state (reference the constant and
component payments-panel.tsx and the darkMode symbol to locate the change).
🧹 Nitpick comments (4)
apps/backend/src/lib/payments.tsx (2)

482-486: Clarify the backward compatibility behavior with a comment.

The matchesCustomer function returns true when storedType is missing, which appears to be intentional for backward compatibility with customers created before customerType was added to metadata. Consider adding a brief comment to explain this.

📝 Suggested clarification
   const matchesCustomer = (customer: Stripe.Customer) => {
     const storedType = customer.metadata.customerType;
-    if (!storedType) return true;
+    if (!storedType) return true; // Backward compatibility: match customers created before customerType was added
     return storedType === stripeCustomerType;
   };

494-512: Consider logging when fallback to listing is used.

The fallback loop handles Stripe's eventual consistency, but silently iterating through customer lists could mask issues in production. Consider adding observability (e.g., a structured log or metric) when the search fails and the fallback is triggered.

apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts (1)

49-66: Consider using consistent Stripe initialization pattern.

This route fetches stripeAccountId manually and passes it to getStripeForAccount, while the setup-intent route uses getStripeForAccount({ tenancy: auth.tenancy }). The current approach is slightly more efficient but creates inconsistency. Consider standardizing on one pattern for maintainability.

packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)

30-38: Import types instead of duplicating them.

The CustomerBilling and CustomerPaymentMethodSetupIntent types are duplicated from packages/template/src/lib/stack-app/customers/index.ts. Import them directly to avoid maintenance issues if the source types change.

♻️ Refactor to import types

Add to imports:

+import { CustomerBilling, CustomerPaymentMethodSetupIntent } from "../../../lib/stack-app/customers";

Remove the duplicate type definitions:

-type CustomerBilling = {
-  hasCustomer: boolean,
-  defaultPaymentMethod: PaymentMethodSummary,
-};
-
-type CustomerPaymentMethodSetupIntent = {
-  clientSecret: string,
-  stripeAccountId: string,
-};
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6865f7 and 744bed1.

📒 Files selected for processing (6)
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
  • apps/backend/src/lib/payments.tsx
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/template/src/components-page/account-settings/payments/payments-page.tsx
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For blocking alerts and errors, never use toast; instead, use alerts as toasts are easily missed by the user

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/lib/payments.tsx
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{tsx,css}: Keep hover/click animations snappy and fast; don't delay actions with pre-transitions (e.g., no fade-in on button hover) as it makes UI feel sluggish; instead apply transitions after the action like smooth fade-out when hover ends
When creating hover transitions, avoid hover-enter transitions and use only hover-exit transitions (e.g., transition-colors hover:transition-none)

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • apps/backend/src/lib/payments.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if avoidable; prefer using client components instead to keep pages static (e.g., use usePathname instead of await params)

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/lib/payments.tsx
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error) or similar; use loading indicators instead; if asynchronous handling is necessary, use runAsynchronously or runAsynchronouslyWithAlert instead
Use ES6 maps instead of records wherever possible

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/lib/payments.tsx
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Code defensively; prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating violated assumptions
Avoid the any type; when necessary, leave a comment explaining why it's used, why the type system fails, and how errors would be caught at compile-, test-, or runtime

Files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts
  • apps/backend/src/lib/payments.tsx
  • apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts
🧠 Learnings (3)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error) or similar; use loading indicators instead; if asynchronous handling is necessary, use `runAsynchronously` or `runAsynchronouslyWithAlert` instead

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2026-01-07T00:55:19.871Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T00:55:19.871Z
Learning: Applies to **/*.{ts,tsx} : Code defensively; prefer `?? throwErr(...)` over non-null assertions with good error messages explicitly stating violated assumptions

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🧬 Code graph analysis (2)
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (4)
packages/template/src/lib/stack-app/customers/index.ts (2)
  • CustomerBilling (72-75)
  • CustomerPaymentMethodSetupIntent (77-80)
packages/template/src/components-page/account-settings/section.tsx (1)
  • Section (3-22)
packages/stack-shared/src/utils/promises.tsx (1)
  • runAsynchronouslyWithAlert (312-328)
apps/e2e/tests/js/convex.test.ts (1)
  • open (31-33)
apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts (7)
apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/setup-intent/route.ts (1)
  • POST (8-82)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (216-301)
packages/stack-shared/src/schema-fields.ts (5)
  • yupObject (247-251)
  • clientOrHigherAuthTypeSchema (534-534)
  • adaptSchema (330-330)
  • yupString (187-190)
  • yupNumber (191-194)
apps/backend/src/lib/payments.tsx (3)
  • ensureClientCanAccessCustomer (25-52)
  • ensureStripeCustomerForCustomer (525-543)
  • getDefaultCardPaymentMethodSummary (553-572)
apps/backend/src/prisma-client.tsx (1)
  • getPrismaClientForTenancy (67-69)
apps/backend/src/lib/stripe.tsx (1)
  • getStripeForAccount (29-54)
packages/stack-shared/src/utils/errors.tsx (1)
  • StatusError (152-261)
⏰ 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). (13)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: setup-tests-with-custom-base-port
  • GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
  • GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: setup-tests
🔇 Additional comments (9)
apps/backend/src/lib/payments.tsx (2)

25-52: LGTM! Access control logic is well-structured.

The ensureClientCanAccessCustomer function correctly enforces user authentication and authorization. For user customers, it validates ownership; for team customers, it delegates to the existing permission check with team_admin requirement.


798-803: LGTM! Well-designed subscription metadata in OwnedProduct.

The subscription field cleanly encapsulates period end, cancel status, and cancelability. The isCancelable logic correctly requires both id and productId to be non-null, preventing cancellation of default/synthetic subscriptions.

Also applies to: 850-854

apps/backend/src/app/api/latest/payments/billing/[customer_type]/[customer_id]/route.ts (2)

38-47: LGTM! Proper access control for client requests.

The handler correctly enforces that clients can only access their own billing information, with appropriate forbidden message.


75-98: LGTM! Clear response structure with proper state distinction.

The response correctly distinguishes between "no Stripe customer exists" (has_customer: false) and "customer exists but has no default payment method" (has_customer: true, default_payment_method: null).

apps/backend/src/app/api/latest/payments/payment-method/[customer_type]/[customer_id]/set-default/route.ts (4)

42-51: LGTM! Consistent access control with other payment routes.

The client access validation correctly mirrors the pattern used in the setup-intent and billing routes.


63-72: LGTM! Robust setup intent validation.

The handler correctly validates: (1) the setup intent belongs to this customer, (2) the intent succeeded, and (3) a payment method is attached. This prevents IDOR attacks and ensures data integrity.


55-66: Verify the customer creation behavior is intentional.

ensureStripeCustomerForCustomer creates a new Stripe customer if one doesn't exist. In the normal flow (setup-intent → set-default), this is fine since setup-intent already creates the customer. However, if called out-of-order with a mismatched setup intent, the validation at line 64 will correctly fail.


80-97: LGTM! Proper handling of edge cases in response generation.

The handler correctly checks for unexpected customer deletion and returns the payment method summary. The nullable default_payment_method in the response schema correctly handles non-card payment methods.

packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)

190-204: Good use of defensive coding and async handling.

The null checks for setupIntentStripeAccountId and publishableKey, combined with proper use of runAsynchronouslyWithAlert for the async operation, demonstrate good defensive programming practices.

@BilalG1 BilalG1 requested a review from N2D4 January 9, 2026 22:37
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Jan 9, 2026
@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Jan 13, 2026
@cmux-agent
Copy link

cmux-agent bot commented Jan 13, 2026

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @packages/template/package-template.json:
- Around line 90-93: The package-template.json pins outdated Stripe packages;
update the dependency versions for "@stripe/react-stripe-js" to ^5.4.0 and
"@stripe/stripe-js" to ^8.6.0 in the package-template.json so the template uses
the current compatible releases (replace the existing version strings for
"@stripe/react-stripe-js" and "@stripe/stripe-js" entries).

In
@packages/template/src/components-page/account-settings/payments/payments-panel.tsx:
- Line 66: The inline read of document.documentElement.style into the darkMode
variable in the PaymentsPanel component causes DOM access during render and can
hydrate mismatches; change this to a client-safe pattern by replacing the direct
access with a state value (e.g., darkMode state) and populate it inside a
useEffect (or consume your theme context) so the DOM is only read on the client
after mount; update references to the darkMode variable accordingly and ensure
initial server-rendered value is stable (false or undefined) to avoid hydration
issues.
- Around line 30-38: The local type CustomerPaymentMethodSetupIntent duplicates
the exported type from the customers module; remove the local declaration and
import CustomerPaymentMethodSetupIntent from the customers module instead
(replace the local type declaration with an import). Also review CustomerBilling
vs the customers' CustomerDefaultPaymentMethod/PaymentMethodSummary usage:
confirm whether CustomerBilling should continue using PaymentMethodSummary or be
aligned to CustomerDefaultPaymentMethod and update the type or import
accordingly to avoid drift.
🧹 Nitpick comments (2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

272-285: Consider extracting the response type to avoid duplication.

The billing response type is defined inline here and again in _customerBillingFromResponse (lines 1181-1190). Consider extracting it to a shared type alias for maintainability.

♻️ Suggested refactor
+type CustomerBillingResponse = {
+  has_customer: boolean,
+  default_payment_method: {
+    id: string,
+    brand: string | null,
+    last4: string | null,
+    exp_month: number | null,
+    exp_year: number | null,
+  } | null,
+};
+
-private readonly _customerBillingCache = createCacheBySession<["user" | "team", string], {
-  has_customer: boolean,
-  default_payment_method: {
-    id: string,
-    brand: string | null,
-    last4: string | null,
-    exp_month: number | null,
-    exp_year: number | null,
-  } | null,
-}>(
+private readonly _customerBillingCache = createCacheBySession<["user" | "team", string], CustomerBillingResponse>(

Then update _customerBillingFromResponse to use CustomerBillingResponse as its parameter type.

packages/template/src/components-page/account-settings/payments/payments-panel.tsx (1)

310-318: Consider using throwErr for defensive coding.

Per coding guidelines, use ?? throwErr(...) instead of silent returns. The if (!productId) return; pattern could mask bugs.

💡 Suggested fix
 okButton={{
   label: t("Cancel subscription"),
   onClick: async () => {
-    const productId = cancelProductId;
-    if (!productId) return;
+    const productId = cancelProductId ?? throwErr("cancelProductId should not be null when okButton is clicked");
     if (props.customerType === "team") {
       await stackApp.cancelSubscription({ teamId: props.customer.id, productId });
     } else {
       await stackApp.cancelSubscription({ productId });
     }
     setCancelProductId(null);
   },
 }}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 744bed1 and 868a16b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • packages/react/package.json
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack/package.json
  • packages/template/package-template.json
  • packages/template/package.json
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/stack/package.json
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/template/package.json
  • packages/react/package.json
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: For blocking alerts and errors, never use toast, as they are easily missed by the user. Instead, use alerts
Keep hover/click transitions snappy and fast without pre-transition delays (e.g., no fade-in when hovering a button). Apply transitions after the action, like smooth fade-out when hover ends
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use loading indicators for async operations. Use runAsynchronously or runAsynchronouslyWithAlert instead of general try-catch error handling
When creating hover transitions, avoid hover-enter transitions and use only hover-exit transitions (e.g., transition-colors hover:transition-none)
Don't use Date.now() for measuring elapsed (real) time; instead use performance.now()
Use ES6 maps instead of records wherever possible

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: NEVER use Next.js dynamic functions if you can avoid them. Prefer using client components to keep pages static (e.g., use usePathname instead of await params)
Code defensively using ?? throwErr(...) instead of non-null assertions, with good error messages explicitly stating violated assumptions
Try to avoid the any type. When using any, leave a comment explaining why and how the type system fails or how errors would still be caught

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
{.env*,**/*.{ts,tsx,js,jsx}}

📄 CodeRabbit inference engine (AGENTS.md)

All environment variables should be prefixed with STACK_ (or NEXT_PUBLIC_STACK_ if public) to ensure Turborepo picks up changes and improve readability

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🧠 Learnings (5)
📚 Learning: 2026-01-11T06:44:57.542Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 1069
File: packages/template/src/components-page/onboarding.tsx:59-61
Timestamp: 2026-01-11T06:44:57.542Z
Learning: In the stack-auth codebase, Button components from stackframe/stack-ui (including those used via MessageCard props like primaryAction and secondaryAction) can accept async functions directly and automatically handle loading states with indicators. No need to wrap these with runAsynchronouslyWithAlert.

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2026-01-13T18:14:29.974Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T18:14:29.974Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use loading indicators for async operations. Use `runAsynchronously` or `runAsynchronouslyWithAlert` instead of general try-catch error handling

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2026-01-13T18:14:29.974Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T18:14:29.974Z
Learning: Applies to **/*.{ts,tsx} : Code defensively using `?? throwErr(...)` instead of non-null assertions, with good error messages explicitly stating violated assumptions

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
📚 Learning: 2026-01-13T18:14:29.974Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T18:14:29.974Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : For blocking alerts and errors, never use `toast`, as they are easily missed by the user. Instead, use alerts

Applied to files:

  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
🧬 Code graph analysis (1)
packages/template/src/components-page/account-settings/payments/payments-panel.tsx (5)
packages/template/src/lib/stack-app/customers/index.ts (2)
  • CustomerBilling (72-75)
  • CustomerPaymentMethodSetupIntent (77-80)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/template/src/components-page/account-settings/section.tsx (1)
  • Section (3-22)
packages/stack-shared/src/utils/promises.tsx (1)
  • runAsynchronouslyWithAlert (312-328)
apps/e2e/tests/js/convex.test.ts (1)
  • open (31-33)
🪛 Biome (2.1.2)
packages/template/package-template.json

[error] 90-90: The key // was already declared.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

⏰ 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). (13)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
  • GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: setup-tests
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: setup-tests-with-custom-base-port
🔇 Additional comments (12)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (6)

45-45: LGTM!

The new billing-related type imports align with the new methods introduced in _createCustomer.


1171-1176: LGTM!

The subscription field mapping correctly handles the optional current_period_end with a null check before creating the Date object.


1181-1195: LGTM!

The mapping is straightforward and consistent with other response transformation helpers in this file.


1632-1653: LGTM!

The new billing methods follow the established patterns in this file:

  • getBilling/useBilling pair matches other async getter/React hook pairs
  • createPaymentMethodSetupIntent and setDefaultPaymentMethodFromSetupIntent correctly interact with the interface layer
  • Cache refresh after mutation in setDefaultPaymentMethodFromSetupIntent ensures data consistency

1670-1672: LGTM!

Consistent update to use effectiveSession for billing operations.


1629-1629: No action needed. The pattern is correct and intentional. All billing endpoints (getCustomerBilling, createCustomerPaymentMethodSetupIntent, setDefaultCustomerPaymentMethodFromSetupIntent, createCheckoutUrl) explicitly accept session: InternalSession | null, confirming they are designed to support both authenticated and unauthenticated requests. Creating a session with refreshToken: null is the standard fallback pattern used throughout the codebase (e.g., in sendClientRequest) for operations that don't require authentication.

packages/template/src/components-page/account-settings/payments/payments-panel.tsx (6)

1-11: LGTM!

Imports are well-organized and appropriate. Good use of runAsynchronouslyWithAlert and throwErr as per coding guidelines.


81-108: LGTM!

Good use of async onClick handler - the Button component from @stackframe/stack-ui automatically handles loading states. The defensive null checks for stripe, elements, and card are appropriate. Based on learnings, this pattern is correct.


113-126: LGTM!

Clean conditional rendering logic with appropriate fallbacks for mock mode and missing customer.


128-175: LGTM!

Mock panel provides appropriate static data for preview mode with properly disabled interactive elements.


260-297: LGTM!

Good implementation of the active plans list with:

  • Proper filtering by customer type
  • Well-formatted quantity suffix and renewal dates using Intl.DateTimeFormat
  • Appropriate conditional rendering for the cancel button based on isCancelable
  • Reasonable fallback key using index when product.id is null

299-322: LGTM!

The cancel subscription dialog is well-implemented with proper confirmation flow using ActionDialog. The async onClick handler is correctly handled by the ActionDialog component per the codebase patterns.

@BilalG1 BilalG1 enabled auto-merge (squash) January 13, 2026 21:48
@BilalG1 BilalG1 merged commit 570249f into dev Jan 13, 2026
33 of 35 checks passed
@BilalG1 BilalG1 deleted the payments-shown-subscriptions-in-settings branch January 13, 2026 21:49
This was referenced Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants