-
Notifications
You must be signed in to change notification settings - Fork 51
Refactor: Billing User Interface #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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".
| const { data: items, isLoading } = useQuery( | ||
| trpc.billing.getItems.queryOptions({ teamIdOrSlug }) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| } | ||
|
|
||
| function PlanAvatar({ selectedTier, isLoading }: PlanAvatarProps) { | ||
| const isBaseTier = !selectedTier || selectedTier.id.includes('base') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
| ) | ||
|
|
||
| const displayName = isBaseTier ? 'Hobby' : 'Professional' | ||
| const priceDisplay = tier?.price_cents ? `$${tier.price_cents / 100}` : 'FREE' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| ) : ( | ||
| <span className="prose-value-small"> | ||
| {formatCurrency(credits ?? 0)} | ||
| </span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
- 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const currentAddon = addonData?.current | ||
| const availableAddon = addonData?.available | ||
| const currentConcurrentSandboxesLimit = | ||
| tierData?.selected?.limits?.sandbox_concurrency |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
| ) | ||
|
|
||
| const isProcessing = isLoading || isConfirming | ||
| const isProcessing = confirmOrderMutation.isPending || isConfirming |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
|
|
||
| function formatHours(hours: number): string { | ||
| return `${hours}h` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| ) | ||
| }, | ||
| }) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.


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 +
HydrateClientmodel, 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 fromnext-safe-actionserver actions to TRPC mutations with cache invalidation.Renames the “Budget” section to “Limits” across routing, sidebar, and layout config (keeping
tab=budgetredirect 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.