Skip to content

Billing & Usage: token usage instead of a token bank, plan changes in… - #137

Open
pavlo-flamingo wants to merge 5 commits into
mainfrom
billing-usage-redesign
Open

Billing & Usage: token usage instead of a token bank, plan changes in…#137
pavlo-flamingo wants to merge 5 commits into
mainfrom
billing-usage-redesign

Conversation

@pavlo-flamingo

Copy link
Copy Markdown
Contributor

… a modal

Implements the Figma redesign of the subscription view and folds the plan picker into it, so changing a plan no longer leaves the page.

Billing & Usage page

  • Top row is three stat cards: Device Usage (used/allocation + "Annual Prepaid" on a committed package, a bare count + "Pay as you go" otherwise), Free AI Tokens (remaining of the monthly allowance) and AI Usage.
  • Current Plan states Billing Cycle, Device Rate, Free AI Tokens, Next Payment and Next Billing Date; the Usage Overview block is gone with the design.
  • UsageStatCard is local: core's DashboardInfoCard puts its secondary text beside the value and reserves the right slot for a progress ring, and the design needs a caption on its own line and no ring.

AI tokens are consumption, not a balance

  • The AI package allocation and everything derived from it (percentage, AI overage, AI limit warnings) is removed — the paywall sells AI as pay-as-you-go only, so there is no bought bank to sit against.
  • deriveAiTokenUsage splits consumption against a free monthly allowance. That allowance is a MOCK: SubscriptionUsage exposes aiTokensUsed and nothing about what is included. TODO(backend) marks the single swap point.

Upgrade Plan modal

  • /settings/billing-usage/subscription is deleted. Its picker moved to DevicePlanPicker, shared by the modal and the subscription lock screen, so the two cannot drift.
  • The modal owns its query: the catalog, its prices and the device count are only fetched once someone opens it.
  • With no live subscription the submit creates a checkout session and folds every other product in as pay-as-you-go — a session describes the whole plan, not a diff, so leaving them out would activate with AI switched off.
  • Every entry point that pointed at the deleted route now opens the modal; routes.settings.billingSubscription is out of the registry.

Customer Portal

  • New menu item over createBillingPortalSession. Stripe mints the session per click, so it is a mutation plus a same-tab redirect rather than a link: the portal returns the user via return_url, and a window.open after an async response is what popup blockers kill.

"Access restricted" overlay

  • isRouteAllowedInCurrentMode() no longer consults isPaymentUiEnabled(). That reads the server-loaded billings flag, which is merely unanswered on a cold load, and the guard read "not yet" as "not allowed" — throwing the refusal over billing routes until the flags query landed. The /checkout/* pages already 404 on their own, where loading is distinguishable from off.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@pavlo-flamingo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 517d028a-6608-4a4a-9209-f61778c5f90b

📥 Commits

Reviewing files that changed from the base of the PR and between 2918eea and ea20b31.

📒 Files selected for processing (78)
  • CLAUDE.md
  • src/app/(app)/checkout/cancel/.page.md
  • src/app/(app)/checkout/cancel/page.tsx
  • src/app/(app)/checkout/success/.page.md
  • src/app/(app)/checkout/success/page.tsx
  • src/app/(app)/settings/billing-usage/BACKEND_REQUIREMENTS.md
  • src/app/(app)/settings/billing-usage/components/.billing-usage-skeleton.md
  • src/app/(app)/settings/billing-usage/components/ai-spend-limit-fields.tsx
  • src/app/(app)/settings/billing-usage/components/ai-tokens-limit-modal.tsx
  • src/app/(app)/settings/billing-usage/components/billing-restricted-screen.tsx
  • src/app/(app)/settings/billing-usage/components/billing-usage-content.tsx
  • src/app/(app)/settings/billing-usage/components/billing-usage-skeleton.tsx
  • src/app/(app)/settings/billing-usage/components/cancel-offer-modal.tsx
  • src/app/(app)/settings/billing-usage/components/cancel-subscription-modal.tsx
  • src/app/(app)/settings/billing-usage/components/invoices-history.tsx
  • src/app/(app)/settings/billing-usage/components/upgrade-plan-modal.tsx
  • src/app/(app)/settings/billing-usage/components/usage-stat-card.tsx
  • src/app/(app)/settings/billing-usage/components/usage-view.tsx
  • src/app/(app)/settings/billing-usage/hooks/.use-billing-summary.md
  • src/app/(app)/settings/billing-usage/hooks/use-ai-spend-limit.ts
  • src/app/(app)/settings/billing-usage/hooks/use-billing-portal-session.ts
  • src/app/(app)/settings/billing-usage/hooks/use-billing-summary.ts
  • src/app/(app)/settings/billing-usage/hooks/use-cancellation-impact.ts
  • src/app/(app)/settings/billing-usage/hooks/use-update-ai-spend-cap.ts
  • src/app/(app)/settings/billing-usage/lib/ai-free-tokens.ts
  • src/app/(app)/settings/billing-usage/lib/ai-token-price.ts
  • src/app/(app)/settings/billing-usage/lib/format.ts
  • src/app/(app)/settings/billing-usage/lib/invoices-table-columns.ts
  • src/app/(app)/settings/billing-usage/lib/stripe-window.ts
  • src/app/(app)/settings/billing-usage/page.tsx
  • src/app/(app)/settings/billing-usage/subscription/.page.md
  • src/app/(app)/settings/billing-usage/subscription/components/.plan-change-summary.md
  • src/app/(app)/settings/billing-usage/subscription/components/.product-subscription-card.md
  • src/app/(app)/settings/billing-usage/subscription/components/.subscription-settings-skeleton.md
  • src/app/(app)/settings/billing-usage/subscription/components/ai-assistants-included-note.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/ai-tokens-usage-card.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/device-management-card.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/device-plan-picker.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/model-token-rates.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/plan-change-summary.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/plan-total-summary.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/product-subscription-card.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/quantity-stepper.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/subscription-settings-skeleton.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/subscription-settings-view.tsx
  • src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx
  • src/app/(app)/settings/billing-usage/subscription/hooks/.use-product-selection.md
  • src/app/(app)/settings/billing-usage/subscription/hooks/use-create-checkout-session.ts
  • src/app/(app)/settings/billing-usage/subscription/hooks/use-device-plan-selection.ts
  • src/app/(app)/settings/billing-usage/subscription/hooks/use-product-selection.ts
  • src/app/(app)/settings/billing-usage/subscription/hooks/use-update-subscription.ts
  • src/app/(app)/settings/billing-usage/subscription/page.tsx
  • src/app/(app)/settings/billing-usage/subscription/types/subscription.types.ts
  • src/app/(app)/settings/billing-usage/subscription/utils/.build-package-radio-options.md
  • src/app/(app)/settings/billing-usage/subscription/utils/build-package-radio-options.tsx
  • src/app/(app)/settings/billing-usage/subscription/utils/subscription.utils.ts
  • src/app/(app)/settings/components/settings-hub.tsx
  • src/app/components/app-layout.tsx
  • src/app/components/shared/locked-screen.tsx
  • src/app/components/subscription-lock/.subscription-guard.md
  • src/app/components/subscription-lock/.subscription-lock-context.md
  • src/app/components/subscription-lock/.trial-ended-banner.md
  • src/app/components/subscription-lock/paywall-header.tsx
  • src/app/components/subscription-lock/subscription-guard.tsx
  • src/app/components/subscription-lock/subscription-lock-content.tsx
  • src/app/components/subscription-lock/subscription-lock-context.tsx
  • src/app/components/subscription-lock/subscription-lock-copy.ts
  • src/app/components/subscription-lock/subscription-plan-lock-content.tsx
  • src/app/components/subscription-lock/trial-ended-banner.tsx
  • src/app/components/subscription-lock/workspace-inactive-screen.tsx
  • src/app/error.tsx
  • src/app/hooks/use-billing-access-gate.ts
  • src/components/feature-flags-loader.tsx
  • src/lib/api-client.ts
  • src/lib/app-mode.ts
  • src/lib/relay/environment.ts
  • src/lib/routes.ts
  • src/lib/subscription-gate.ts

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.

@pavlo-flamingo
pavlo-flamingo force-pushed the billing-usage-redesign branch from e34cbca to afa39b7 Compare August 6, 2026 11:14
… a modal

Implements the Figma redesign of the subscription view and folds the plan
picker into it, so changing a plan no longer leaves the page.

Billing & Usage page
- Top row is three stat cards: Device Usage (`used/allocation` + "Annual
  Prepaid" on a committed package, a bare count + "Pay as you go" otherwise),
  Free AI Tokens (remaining of the monthly allowance) and AI Usage.
- Current Plan states Billing Cycle, Device Rate, Free AI Tokens, Next Payment
  and Next Billing Date; the Usage Overview block is gone with the design.
- `UsageStatCard` is local: core's `DashboardInfoCard` puts its secondary text
  beside the value and reserves the right slot for a progress ring, and the
  design needs a caption on its own line and no ring.

AI tokens are consumption, not a balance
- The AI package allocation and everything derived from it (percentage, AI
  overage, AI limit warnings) is removed — the paywall sells AI as
  pay-as-you-go only, so there is no bought bank to sit against.
- `deriveAiTokenUsage` splits consumption against a free monthly allowance.
  That allowance is a MOCK: `SubscriptionUsage` exposes `aiTokensUsed` and
  nothing about what is included. TODO(backend) marks the single swap point.

Upgrade Plan modal
- `/settings/billing-usage/subscription` is deleted. Its picker moved to
  `DevicePlanPicker`, shared by the modal and the subscription lock screen, so
  the two cannot drift.
- The modal owns its query: the catalog, its prices and the device count are
  only fetched once someone opens it.
- With no live subscription the submit creates a checkout session and folds
  every other product in as pay-as-you-go — a session describes the whole plan,
  not a diff, so leaving them out would activate with AI switched off.
- Every entry point that pointed at the deleted route now opens the modal;
  `routes.settings.billingSubscription` is out of the registry.

Customer Portal
- New menu item over `createBillingPortalSession`. Stripe mints the session per
  click, so it is a mutation plus a same-tab redirect rather than a link: the
  portal returns the user via `return_url`, and a `window.open` after an async
  response is what popup blockers kill.

"Access restricted" overlay
- `isRouteAllowedInCurrentMode()` no longer consults `isPaymentUiEnabled()`.
  That reads the server-loaded `billings` flag, which is merely unanswered on a
  cold load, and the guard read "not yet" as "not allowed" — throwing the
  refusal over billing routes until the flags query landed. The `/checkout/*`
  pages already 404 on their own, where `loading` is distinguishable from `off`.
…e module

A locked workspace used to reach the app before anyone knew it was locked: the
status query raced every other query, and the SUBSCRIPTION_TRIAL_EXPIRED refusal
that came back nulled non-null payloads and took the page down with it.

Gate. `subscription-gate.ts` parks every Relay and REST request until the status
resolves, and keeps them parked while it locks. Fail-open after 10s so a stalled
answer cannot brick the app; a real answer re-engages the gate. The status query
and the paywall's own queries bypass it — they are what gets the user out.

No fallbacks. Three dates, three fields, each null exactly when it does not
apply: `currentPeriodEnd` (next renewal), `cancellationEffectiveAt`
(termination), and the active package's `endDate` (read only while a change is
pending). The old single value chained through all three and printed the same
day under two different labels. Overage cost now comes from
`currentInvoice.estimatedOverage` instead of multiplying by a rate the surplus
is not billed at. The locally-derived free-token allowance is gone — those
figures are the backend's to serve.

Plan changes. `/settings/billing-usage/subscription` is gone; the plan is
changed in an Upgrade Plan modal over its own query, sharing `DevicePlanPicker`
with the lock screen. `useUpdateSubscription` no longer navigates on success —
it took the caller to the page it was already on, so the modal sat open over
stale data; callers pass `onSuccess` instead. Customer Portal (mutation, same
tab) and Activate Subscription (trial) join the header.

UI. Usage cards state usage: devices as used/allocation over its plan, or a bare
count on PAYG, or a bare count over "Trial Period ends <date>" in trial. One
warning survives — devices over a committed package — and it recolours the whole
card. Invoices load through the shared `TableSkeleton` off a shared column
layout, so the loading table matches every other table's.

Boundaries. Model token rates and the paywall catalog each carry their own
ErrorBoundary: a refused query degrades to a panel that says so, not a blank
app. `error.tsx` renders a centered LockedScreen. The app shell no longer waits
on onboarding while locked, which stranded it on skeleton chrome forever.

Simplification. `SubscriptionGuard` was three components and a separate context
file for one decision; it is one component and one hook now. Dead code removed:
the Custom Amount branch nothing could reach, `disabled` nothing passed, hook
and query fields nothing read.
The billing work replaced this hook with `useBillingAccessGate` (owner OR admin,
because admins run the workspace day to day) and deleted it, correctly: at that
commit nothing else asked the question.

Main has since landed self-deletion with owner role transfer, and that flow asks
the OTHER question — is this user the owner, who must hand the role over before
they can delete themselves. Two different questions, two hooks; the deletion only
looked safe from inside the billing branch.
…ing states

The AI product is metered against two server figures — the period's free grant
and the ceiling the customer may put on what is billed beyond it — and neither
had a surface. This adds both, and fixes what the page was getting wrong about
an annual plan.

AI spending limit
- One set of controls (AiSpendLimitFields) over one state hook, shared by the
  billing page's AI Tokens Limit modal and the paywall's AI card. They differ
  only in WHEN they write: the modal saves on its own button, the paywall as
  part of "Proceed to Payment".
- The paywall no longer writes per click. Unticking the box to look at the
  options used to change the subscription, with no way back but retyping the old
  figure. The cap now rides with the submit and is stored before checkout —
  Stripe takes the user off-site, so there is no afterwards to save it in.
- Token counts are converted to the stored USD cap through the catalog's unit
  price. `price` is quoted per unitSize block, not per token; multiplying a
  token count by it printed "~$20,000,000.00" under a 2M-token limit.

Usage row and plan blocks
- Three cards: devices, free AI tokens (used/granted), paid AI tokens against
  the cap. UsageStatCard takes a tone (default | warning | error) so a cap being
  approached or reached recolours the whole card, suffix included.
- A banner when AI is about to pause or has, and an "AI Usage Beyond Free
  Tokens" block beside Current Plan once any tokens have actually been billed.
- Current Plan gains AI Tokens Rate and Free AI Tokens; the device-overage strip
  states each figure above what it counts, as designed.

Annual plan
- Device Rate is read from the catalog FOR THE PERIOD the plan is on. A
  committed option leaves its own price empty, and its price bands answer with
  the undiscounted monthly rate — so an annual plan showed no rate at all, then
  the wrong one.
- A plan that is ending is detected by the active package's endDate rather than
  by a scheduled replacement. Downgrading to pay-as-you-go schedules no package
  (pay-as-you-go is not one), so PENDING_ACTIVATION stays empty and only the
  date appears; the Updated Plan column describes the metered billing it falls
  back to.
- The header offers one secondary action and the menu carries the other, never
  both: an existing AI limit wins the header, otherwise a monthly plan's upgrade
  to annual. An annual plan has nothing above it, so its plan change is a menu
  item.

Also: schema refresh (aiSpendCapUsd, the aiTokens* usage fields, ScriptPlatform
renamed to OsType) and the two script call sites that rename touched.
Checkout, the customer portal and a hosted invoice all replaced the current tab.
That loses whatever was on screen — a paywall mid-selection, a billing page
mid-read — and an abandoned Stripe flow ends nowhere, with the back button
landing on a re-issued session.

Checkout and portal URLs are minted per click by a mutation, so a tab opened
once one arrives is a popup and gets blocked. `openDeferredTab` opens it while
the user gesture is still live and pushes the URL in when the response lands;
the blank tab is closed again if the mutation fails or answers without a URL.
Where no handle comes back (blocked anyway) it navigates this tab rather than
leaving a button that does nothing.

The paywall's submit opens the tab itself and carries it through, because it
stores the AI spending cap first and reaches checkout from that mutation's
callback — by then the gesture is gone.

The invoices table already opened its rows in a new tab; the "Pay Overage" CTA
now does too.
@pavlo-flamingo
pavlo-flamingo force-pushed the billing-usage-redesign branch from d233b12 to ea20b31 Compare August 7, 2026 07:52
@pavlo-flamingo
pavlo-flamingo marked this pull request as ready for review August 7, 2026 07:53
@pavlo-flamingo
pavlo-flamingo requested review from a team as code owners August 7, 2026 07:53
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.

1 participant