mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
fix(billing): block downgrade requests at the component level
RequestUpgradeToBillingOwners now checks the requested plan against the viewer's current plan and renders a disabled state when the request would be a downgrade or no-op. This protects every callsite: the plan switcher (previously offered 'Request upgrade to Free' to higher-tier members), the log-drains empty state (hardcoded 'Pro' showing to Team members), the billing-metric paywall, and the custom-auth-providers fallback path. Addon-flavored requests (compute size, PITR, custom domain, IPv4, spend cap) bypass the guard because they are about feature add-ons, not plan tier changes.
This commit is contained in:
1 parent
e55411da5e
commit
3fc23db748
2 files changed
+36
-5
No files matched your search
+4
-1
@@ -308,7 +308,10 @@ export const PlanUpdateSidePanel = () => {
|
||||
Current plan
|
||||
</Button>
|
||||
) : !canUpdateSubscription ? (
|
||||
<RequestUpgradeToBillingOwners block plan={plan.name as 'Pro' | 'Team'} />
|
||||
<RequestUpgradeToBillingOwners
|
||||
block
|
||||
plan={plan.name as 'Free' | 'Pro' | 'Team'}
|
||||
/>
|
||||
) : (
|
||||
<ButtonTooltip
|
||||
block
|
||||
|
||||
@@ -25,12 +25,15 @@ import {
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import z from 'zod'
|
||||
|
||||
import { getPlanChangeType } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query'
|
||||
import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
|
||||
import {
|
||||
PlanRequest,
|
||||
useSendUpgradeRequestMutation,
|
||||
} from '@/data/organizations/request-upgrade-mutation'
|
||||
import type { PlanId } from '@/data/subscriptions/types'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useTrack } from '@/lib/telemetry/track'
|
||||
@@ -41,9 +44,11 @@ const FormSchema = z.object({
|
||||
|
||||
const formId = 'request-upgrade-form'
|
||||
|
||||
type DisplayPlan = 'Free' | PlanRequest
|
||||
|
||||
interface RequestUpgradeToBillingOwnersProps {
|
||||
block?: boolean
|
||||
plan?: PlanRequest
|
||||
plan?: DisplayPlan
|
||||
addon?: 'pitr' | 'customDomain' | 'ipv4' | 'spendCap' | 'computeSize'
|
||||
/** Used in the default message template, e.g: "Upgrade to ..." */
|
||||
featureProposition?: string
|
||||
@@ -68,6 +73,10 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
const currentPlan = organization?.plan?.id
|
||||
const isFreePlan = currentPlan === 'free'
|
||||
|
||||
const requestedPlanId = plan.toLowerCase() as PlanId
|
||||
const planChange = !addon ? getPlanChangeType(currentPlan, requestedPlanId) : 'upgrade'
|
||||
const isInvalidDirection = planChange === 'downgrade' || planChange === 'none'
|
||||
|
||||
const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
|
||||
const { data: roles } = useOrganizationRolesV2Query({ slug: organization?.slug })
|
||||
const orgRoles = roles?.org_scoped_roles ?? []
|
||||
@@ -75,7 +84,7 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
const { mutate: sendUpgradeRequest, isPending: isSubmitting } = useSendUpgradeRequestMutation({
|
||||
onSuccess: () => {
|
||||
track('request_upgrade_submitted', {
|
||||
requestedPlan: plan,
|
||||
requestedPlan: plan as PlanRequest,
|
||||
addon,
|
||||
currentPlan,
|
||||
})
|
||||
@@ -144,13 +153,13 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
||||
if (!slug) return console.error('Slug is required')
|
||||
sendUpgradeRequest({ slug, plan, note: values.note })
|
||||
sendUpgradeRequest({ slug, plan: plan as PlanRequest, note: values.note })
|
||||
}
|
||||
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
track('request_upgrade_modal_opened', {
|
||||
requestedPlan: plan,
|
||||
requestedPlan: plan as PlanRequest,
|
||||
addon,
|
||||
currentPlan,
|
||||
featureProposition,
|
||||
@@ -159,6 +168,25 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
setOpen(isOpen)
|
||||
}
|
||||
|
||||
if (isInvalidDirection) {
|
||||
return (
|
||||
<ButtonTooltip
|
||||
block={block}
|
||||
disabled
|
||||
type="default"
|
||||
className={className}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: 'Downgrades cannot be requested. Ask a billing owner to change the plan directly.',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Not available
|
||||
</ButtonTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
Reference in new issue
Block a user