Skip to content

Conversation

@ben-fornefeld
Copy link
Member

@ben-fornefeld ben-fornefeld commented Feb 5, 2026

Note

Medium Risk
Touches billing navigation and payment/add-on purchase flows and changes the data-fetching mechanism to TRPC + client-side queries, so regressions could break billing/checkout UX despite being mostly a UI refactor.

Overview
Refactors the dashboard billing area to a TRPC-prefetch + HydrateClient model, replacing server-fetched billing/usage/invoice rendering with client components (SelectedPlan, Credits, Invoices) and new query hooks (useBillingItems, useUsage, useInvoices).

Adds dedicated routes for plan management (/billing/plan + /billing/plan/select) with new UI for plan selection and add-on purchase flow, and migrates the add-on purchase dialog from next-safe-action server actions to TRPC mutations with cache invalidation.

Renames the “Budget” section to “Limits” across routing, sidebar, and layout config (keeping tab=budget redirect compatibility), and removes the old budget page and legacy billing components/docs.

Written by Cursor Bugbot for commit 5085b45. This will update automatically on new commits. Configure here.

…etching

- Removed unused components and consolidated billing logic in the BillingPage and SelectedPlan components.
- Integrated TRPC for fetching billing items, enhancing data management and reducing server calls.
- Updated UI to include loading states and improved error handling.
- Deleted obsolete files related to billing credits and tier cards to clean up the codebase.
- Integrated a new `getUsage` query in the billing API to fetch usage data for teams.
- Updated the `BillingPage` to prefetch usage data alongside billing items.
- Refactored the `Credits` component to utilize the new `useUsage` hook for displaying credits.
- Enhanced the `useBillingItems` hook to maintain existing functionality while adding the new usage logic.
@vercel
Copy link

vercel bot commented Feb 5, 2026

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

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment Feb 6, 2026 0:24am
web-juliett Ready Ready Preview, Comment Feb 6, 2026 0:24am

Request Review

@ben-fornefeld ben-fornefeld added the design For changes related to UI/UX label Feb 5, 2026
@ben-fornefeld ben-fornefeld marked this pull request as ready for review February 5, 2026 17:36
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6b1f1aead

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +270 to +272
const { data: items, isLoading } = useQuery(
trpc.billing.getItems.queryOptions({ teamIdOrSlug })
)

Choose a reason for hiding this comment

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

P1 Badge Surface billing item query failures before rendering plan state

If billing.getItems errors (API outage, auth failure, etc.), useBillingItems drops the error and only returns items/isLoading, so downstream components treat missing data as a real state (!selectedTier is interpreted as base tier in selected-plan.tsx). This can render upgrade/downgrade controls against an unknown plan instead of showing an error, which is a regression from the prior server-rendered error handling.

Useful? React with 👍 / 👎.

<Skeleton className="h-5 w-16" />
) : (
<span className="prose-value-small">
{formatCurrency(credits ?? 0)}

Choose a reason for hiding this comment

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

P1 Badge Avoid defaulting failed credit fetches to $0

When billing.getUsage fails, credits is undefined and this view renders formatCurrency(credits ?? 0), so users see a valid-looking $0 balance instead of an error state. In failure scenarios this presents incorrect financial data and can mislead billing decisions; the component should differentiate "no data" from an actual zero-credit balance.

Useful? React with 👍 / 👎.

}

function formatHours(hours: number): string {
return `${hours}h`
Copy link

Choose a reason for hiding this comment

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

Duplicated utility functions across billing components

Medium Severity

formatMibToGb, formatHours, and MIB_TO_GB constant are duplicated in both select-plan.tsx and selected-plan.tsx. These should be extracted to the existing ./utils.ts file which already contains other billing-related formatting functions like formatAddonQuantity and generateTierLimitFeatures.

Fix in Cursor Fix in Web

}

function PlanAvatar({ selectedTier, isLoading }: PlanAvatarProps) {
const isBaseTier = !selectedTier || selectedTier.id.includes('base')
Copy link

Choose a reason for hiding this comment

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

Undefined tier data incorrectly treated as base tier

Medium Severity

The expression const isBaseTier = !selectedTier || selectedTier.id.includes('base') incorrectly treats undefined selectedTier as if the user is on the base tier. When the billing query fails (resulting in isLoading: false with selectedTier: undefined), the UI shows the base tier icon and an "Upgrade for higher concurrency" button. This could mislead Pro users into thinking they need to upgrade when their data simply failed to load. Compare this to select-plan.tsx which correctly uses selectedTierId === TIER_BASE_ID, where undefined doesn't match any tier.

Additional Locations (1)

Fix in Cursor Fix in Web

)

const displayName = isBaseTier ? 'Hobby' : 'Professional'
const priceDisplay = tier?.price_cents ? `$${tier.price_cents / 100}` : 'FREE'
Copy link

Choose a reason for hiding this comment

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

Pro tier incorrectly displays as FREE when data unavailable

Medium Severity

When the billing query fails to load tier data, the expression tier?.price_cents ? ... : 'FREE' defaults to showing "FREE" for plans with undefined price data. This causes the Pro tier card to display "Professional" with price "FREE" when proTier is undefined. Since the Pro tier is a paid plan, displaying it as free is financially misleading and could confuse users about actual pricing on the plan selection page.

Fix in Cursor Fix in Web

) : (
<span className="prose-value-small">
{formatCurrency(credits ?? 0)}
</span>
Copy link

Choose a reason for hiding this comment

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

Credits display $0.00 when API call fails

Medium Severity

When the usage API call fails, the useUsage hook doesn't expose the error state (unlike useInvoices which does). The credits.tsx component then renders formatCurrency(credits ?? 0) which displays $0.00 instead of an error indicator. This is a regression from the deleted credits-content.tsx which showed "Failed to load credits" on errors. Users could be misled into thinking they have no credits when the actual balance is unknown due to a failed API call.

Additional Locations (1)

Fix in Cursor Fix in Web

- Updated billing-related components to utilize TRPC for API calls instead of the previous action-based approach.
- Replaced `useParams` with `useRouteParams` for better route handling.
- Removed obsolete billing actions and related files to streamline the codebase.
- Enhanced error handling and loading states in the usage limits and limits management components.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

)

const displayName = isBaseTier ? 'Hobby' : 'Professional'
const priceDisplay = tier?.price_cents ? `$${tier.price_cents / 100}` : 'FREE'
Copy link

Choose a reason for hiding this comment

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

Inconsistent price formatting in plan selection page

Low Severity

The price display uses a raw template literal `$${tier.price_cents / 100}` instead of formatCurrency(tier.price_cents / 100) used by all other billing components (selected-plan.tsx, addons.tsx, credits.tsx, invoices.tsx). formatCurrency uses Intl.NumberFormat with minimumFractionDigits: 2, so this produces "$150" instead of "$150.00" for whole-dollar amounts, creating a visual inconsistency across billing pages.

Fix in Cursor Fix in Web

const currentAddon = addonData?.current
const availableAddon = addonData?.available
const currentConcurrentSandboxesLimit =
tierData?.selected?.limits?.sandbox_concurrency
Copy link

Choose a reason for hiding this comment

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

Addon dialog shows base tier limit, not actual limit

Medium Severity

currentConcurrentSandboxesLimit is set to tierData?.selected?.limits?.sandbox_concurrency, which is the base tier limit (e.g., 100 for Pro). The old code used limits.concurrentInstances from the team_limits DB table, which reflected the team's actual effective limit including purchased addons. For users with existing addons, the purchase dialog now shows incorrect "from" and "to" values (e.g., "from 100 to 600" instead of "from 600 to 1100").

Additional Locations (1)

Fix in Cursor Fix in Web

)

const isProcessing = isLoading || isConfirming
const isProcessing = confirmOrderMutation.isPending || isConfirming
Copy link

Choose a reason for hiding this comment

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

Fallback payment success path doesn't invalidate query cache

Medium Severity

The primary payment success path (via usePaymentConfirmation) correctly calls queryClient.invalidateQueries to refresh billing data after purchase. But the fallback PaymentElementForm success path on line 353 only calls router.refresh(), which no longer triggers a data refetch since the billing page was refactored from server-side data fetching to client-side React Query. After a successful addon purchase via the manual payment form, the billing items cache remains stale, showing outdated addon counts and limits.

Additional Locations (1)

Fix in Cursor Fix in Web


function formatHours(hours: number): string {
return `${hours}h`
}
Copy link

Choose a reason for hiding this comment

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

Duplicated utility functions across two billing files

Medium Severity

MIB_TO_GB, formatMibToGb, and formatHours are identically defined in both selected-plan.tsx and select-plan.tsx. These should be extracted into a shared utility (e.g., utils.ts in the same billing directory) to avoid maintaining two copies.

Fix in Cursor Fix in Web

)
},
})
)
Copy link

Choose a reason for hiding this comment

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

Duplicated customer portal mutation across two components

Low Severity

The createCustomerPortalSession mutation with its onSuccess/onError handlers is duplicated verbatim in both selected-plan.tsx and select-plan.tsx. Consider extracting a shared useCustomerPortal hook in hooks.ts.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design For changes related to UI/UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant