Files
supabase/apps/studio/hooks/misc/useOrganizationRestrictions.ts
Sean OliverandJoshen Lim b14ecb5b1e fix: align grace-period banner copy on org usage page (#45652)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Bug fix — copy + visibility logic on the org Usage page.

## What is the current behavior?

On `/org/<slug>/usage` during a grace period, customers see two banners
that read as contradictory:

1. *"Organization plan has exceeded its quota — grace period until
{date}."*
2. *"You have not exceeded your Pro Plan quota in this billing cycle."*

<img width="1680" height="372" alt="image"
src="https://github.com/user-attachments/assets/13826260-55dd-4b55-a3dc-5afc51e6436e"
/>

Both are individually correct. The first is sticky from the previous
cycle's overage (`org.restriction_status`); the second is a live scan
over the current cycle. Neither anchors to which cycle it's talking
about, so together they read like the dashboard contradicting itself.
Surfaced by support off SU-368527 and SU-368395.

## What is the new behavior?

- Top chrome banner copy: *"Organization exceeded its quota in the
previous billing cycle / You have a grace period until {date} to bring
usage back under quota."*
- Inline `<Restriction />` grace-period alert switches from "is over its
quota" to "went over its quota in the previous billing cycle." Same
temporal anchor.
- The "…in this billing cycle" summary line in `<TotalUsage>` is hidden
whenever `restriction_status` is set. Mirrors the precedence rule
`<Restriction />` already applies internally — backend status flag wins
over the live cycle scan.

<img width="1678" height="937" alt="CleanShot 2026-05-06 at 12 58 02"
src="https://github.com/user-attachments/assets/df55eaed-1029-4f39-bea0-df77bcc5151e"
/>


## Additional context

Left the `gracePeriodOver` copy alone on purpose — it doesn't make a
current-overage claim, so there's nothing to contradict, and adding
"previous cycle" would muddy which cycle "previous" refers to.

**Verified**
- Lint and typecheck pass on `apps/studio`.

**Before merge**
- [ ] Load a grace-period org locally: confirm new copy on top banner
and inline `<Restriction />`, and that the "not exceeded in this billing
cycle" line is gone.
- [ ] Copy review with support — happy to workshop wording.

GROWTH-823

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Updates**
* Updated grace period alert messaging to clarify organization quota
status
  * Refined date formatting in billing restriction notifications
* Modified usage display to conditionally hide certain information when
account restrictions are active

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-05-07 15:46:22 -07:00

95 lines
3.1 KiB
TypeScript

import type { ReactNode } from 'react'
import { useIsFeatureEnabled } from './useIsFeatureEnabled'
import { RESTRICTION_MESSAGES } from '@/components/interfaces/Organization/restriction.constants'
import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
export type WarningBannerProps = {
variant: 'danger' | 'warning' | 'note'
title: string
description: ReactNode
}
/**
* Compute billing-related restriction banners for the currently selected organization.
*
* The hook examines billing feature availability, overdue invoices, organization billing data,
* and restriction status to produce an ordered list of warning/danger banner props.
*
* @returns An object containing:
* - `warnings`: an array of `WarningBannerProps` to display for the selected organization (may be empty).
* - `org`: the selected organization data (may be `undefined`).
*/
export function useOrganizationRestrictions() {
const { data: org } = useSelectedOrganizationQuery()
const { data: overdueInvoices } = useOverdueInvoicesQuery()
const { data: organizations } = useOrganizationsQuery()
const warnings: WarningBannerProps[] = []
const billingEnabled = useIsFeatureEnabled('billing:all')
if (!billingEnabled) {
return { warnings, org }
}
const overdueInvoicesFromOtherOrgs = overdueInvoices?.filter(
(invoice) => invoice.organization_id !== org?.id
)
const thisOrgHasOverdueInvoices = overdueInvoices?.filter(
(invoice) => invoice.organization_id === org?.id
)
if (thisOrgHasOverdueInvoices?.length) {
warnings.push({
variant: 'danger',
title: RESTRICTION_MESSAGES.OVERDUE_INVOICES.title,
description: RESTRICTION_MESSAGES.OVERDUE_INVOICES.description(org?.slug ?? 'default'),
})
}
if (overdueInvoicesFromOtherOrgs?.length) {
const otherOrgSlug = organizations?.find(
(o) => o.id === overdueInvoicesFromOtherOrgs[0].organization_id
)?.slug
warnings.push({
variant: 'danger',
title: RESTRICTION_MESSAGES.OVERDUE_INVOICES_FROM_OTHER_ORGS.title,
description: RESTRICTION_MESSAGES.OVERDUE_INVOICES_FROM_OTHER_ORGS.description(
otherOrgSlug ?? org?.slug ?? 'default'
),
})
}
if (org?.restriction_status === 'grace_period') {
warnings.push({
variant: 'warning',
title: RESTRICTION_MESSAGES.GRACE_PERIOD.title,
description: RESTRICTION_MESSAGES.GRACE_PERIOD.description(
org?.restriction_data?.['grace_period_end'] ?? '',
org.slug
),
})
}
if (org?.restriction_status === 'grace_period_over') {
warnings.push({
variant: 'warning',
title: RESTRICTION_MESSAGES.GRACE_PERIOD_OVER.title,
description: RESTRICTION_MESSAGES.GRACE_PERIOD_OVER.description(org.slug),
})
}
if (org?.restriction_status === 'restricted') {
warnings.push({
variant: 'danger',
title: RESTRICTION_MESSAGES.RESTRICTED.title,
description: RESTRICTION_MESSAGES.RESTRICTED.description(org.slug),
})
}
return { warnings, org }
}