Billing & Usage: token usage instead of a token bank, plan changes in… - #137
Billing & Usage: token usage instead of a token bank, plan changes in…#137pavlo-flamingo wants to merge 5 commits into
Conversation
|
Warning Review limit reached
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (78)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
e34cbca to
afa39b7
Compare
… 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.
d233b12 to
ea20b31
Compare
… 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
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.UsageStatCardis local: core'sDashboardInfoCardputs 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
deriveAiTokenUsagesplits consumption against a free monthly allowance. That allowance is a MOCK:SubscriptionUsageexposesaiTokensUsedand nothing about what is included. TODO(backend) marks the single swap point.Upgrade Plan modal
/settings/billing-usage/subscriptionis deleted. Its picker moved toDevicePlanPicker, shared by the modal and the subscription lock screen, so the two cannot drift.routes.settings.billingSubscriptionis out of the registry.Customer Portal
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 viareturn_url, and awindow.openafter an async response is what popup blockers kill."Access restricted" overlay
isRouteAllowedInCurrentMode()no longer consultsisPaymentUiEnabled(). That reads the server-loadedbillingsflag, 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, whereloadingis distinguishable fromoff.