Billing: gate the pages on owner OR admin, not owner alone - #224
Merged
Conversation
Admins run the workspace day to day — they add the devices the plan is sized for and they hit the limits — so the section they cannot open is the one telling them why. Owners keep the money; admins need to see and change what it buys. Everyone else stays out: to a member, billing is neither useful nor theirs. `use-billing-access-gate.ts` answers that question for every billing surface: the Billing & Usage page, the plan picker, the Settings hub card and the subscription lock screen. Same three states as the feature flags, for the same reason — "not answered yet" is not "no", and `notFound()` throws, so a page closed on a guessed `denied` never reopens once `/me` lands. `use-owner-gate.ts` stays, narrowed to what ownership actually decides: self-deletion, where the point IS handing the workspace over. The refusal screen is renamed with its meaning — `BillingRestrictedScreen`, "Billing is restricted" — since "owner-only" is no longer true, and its copy now points at the owner AND the admins. The native wording still splits on `isBillingHidden()`, naming no subscription or payment (App Store Guideline 3.1.1). Ported from #137, which carries this alongside the Billing redesign.
romanivan-flamingo
approved these changes
Aug 13, 2026
romanivan-flamingo
approved these changes
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Billing was owner-only. It is now open to owners and admins — the role part of #137, ported on its own so it can land without the Billing redesign.
Admins run the workspace day to day: they add the devices the plan is sized for and they are the ones who hit the limits. Owners keep the money; admins need to see and change what it buys. Everyone else stays out — to a member, billing is neither useful nor theirs.
The gate
src/app/hooks/use-billing-access-gate.tsanswers "may this viewer manage billing?" for every billing surface, in three states rather than two:loading | allowed | denied. Same discipline as the feature flags, for the same reason — "not answered yet" is not "no", andnotFound()throws, so a page closed on a guesseddeniednever reopens once/melands. Roles arrive as plain strings from the gateway (OWNER,ADMIN, …) with no schema enum behind them, so the comparison is case-insensitive, matchingemployee-details-view.tsx.Callers switched: the Billing & Usage page, the plan picker page, the Settings hub card (
billings === 'on' && access === 'allowed'), and the subscription lock screen.use-owner-gate.tsstays, narrowed to what ownership actually decides — self-deletion, where handing the workspace over IS the point.The refusal screen
BillingOwnerOnlyScreen→BillingRestrictedScreen: "owner-only" is no longer true. Copy now points at the owner and the admins, and the lock screen says "Only the workspace owner or an admin can restore it." The native wording still splits onisBillingHidden()and names no subscription or payment (App Store Guideline 3.1.1). A role refusal still explains itself instead of 404ing — the hub hides the card, so whoever lands here followed a bookmark or a shared link; a definitivebillings === 'off'still 404s, since there the section genuinely does not exist for the tenant.Not ported from #137
That PR also moves the role check past the lazy boundary into
subscription-plan-lock-content.tsx, so that waiting on/merenders the paywall unpriced instead of the refusal. It depends onSubscriptionSettingsLoading, which arrives with the redesign; hereloadingstays grouped with the refusal, as the file already documents.npm run type-checkand Biome clean.