PlanUpdateSidePanel() — supabase Function Reference
Architecture documentation for the PlanUpdateSidePanel() function in PlanUpdateSidePanel.tsx from the supabase codebase.
Entity Profile
Dependency Diagram
graph TD 05fe9ae6_77f0_016f_7024_66677c9473c4["PlanUpdateSidePanel()"] a9b272ae_c3c3_a426_6bab_b0a7511bc990["getPartnerManagedResourceCta()"] 05fe9ae6_77f0_016f_7024_66677c9473c4 -->|calls| a9b272ae_c3c3_a426_6bab_b0a7511bc990 style 05fe9ae6_77f0_016f_7024_66677c9473c4 fill:#6366f1,stroke:#818cf8,color:#fff
Relationship Graph
Source Code
apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx lines 52–374
export const PlanUpdateSidePanel = () => {
const router = useRouter()
const { slug } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { mutate: sendEvent } = useSendEventMutation()
const originalPlanRef = useRef<string>()
const [showExitSurvey, setShowExitSurvey] = useState(false)
const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false)
const [showDowngradeError, setShowDowngradeError] = useState(false)
const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>()
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const { data: orgProjectsData } = useOrgProjectsInfiniteQuery({ slug })
const orgProjects =
useMemo(
() => orgProjectsData?.pages.flatMap((page) => page.projects),
[orgProjectsData?.pages]
) || []
const { data } = useOrganizationQuery({ slug })
const hasOrioleProjects = !!data?.has_oriole_project
const snap = useOrgSettingsPageStateSnapshot()
const visible = snap.panelKey === 'subscriptionPlan'
const onClose = () => {
const { panel, ...queryWithoutPanel } = router.query
router.push({ pathname: router.pathname, query: queryWithoutPanel }, undefined, {
shallow: true,
})
snap.setPanelKey(undefined)
}
const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({
orgSlug: slug,
})
const { data: plans, isPending: isLoadingPlans } = useOrgPlansQuery({ orgSlug: slug })
const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug })
const {
data: subscriptionPreview,
error: subscriptionPreviewError,
isPending: subscriptionPreviewIsLoading,
isSuccess: subscriptionPreviewInitialized,
} = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug })
const availablePlans: OrgPlan[] = plans?.plans ?? []
const hasMembersExceedingFreeTierLimit =
(membersExceededLimit || []).length > 0 &&
// [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects
// Although I don't foresee this affecting a majority of users. Ideally perhaps we could return
// this data from the organization query
orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0
useEffect(() => {
if (visible) {
setSelectedTier(undefined)
const source = Array.isArray(router.query.source)
? router.query.source[0]
: router.query.source
const properties: StudioPricingSidePanelOpenedEvent['properties'] = {
currentPlan: subscription?.plan?.name,
}
if (source) {
properties.origin = source
}
sendEvent({
action: 'studio_pricing_side_panel_opened',
properties,
groups: { organization: slug ?? 'Unknown' },
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible])
useEffect(() => {
if (visible && isSuccessSubscription) {
originalPlanRef.current = subscription.plan.id
}
}, [visible, isSuccessSubscription])
const onConfirmDowngrade = () => {
setSelectedTier(undefined)
if (hasMembersExceedingFreeTierLimit) {
setShowDowngradeError(true)
} else {
setShowExitSurvey(true)
}
}
const planMeta = selectedTier
? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1])
: null
return (
<>
<SidePanel
hideFooter
size="xxlarge"
visible={visible}
onCancel={() => onClose()}
header={
<div className="flex items-center justify-between">
<h4>Change subscription plan for {selectedOrganization?.name}</h4>
<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.com/pricing" target="_blank" rel="noreferrer">
Pricing
</a>
</Button>
</div>
}
>
{selectedOrganization && selectedOrganization.managed_by !== MANAGED_BY.SUPABASE && (
<PartnerManagedResource
managedBy={selectedOrganization.managed_by}
resource="Organization plans"
cta={getPartnerManagedResourceCta(selectedOrganization)}
/>
)}
<SidePanel.Content>
<div className="py-6 grid grid-cols-12 gap-3">
{subscriptionsPlans.map((plan) => {
const planMeta = availablePlans.find((p) => p.id === plan.id.split('tier_')[1])
const price = planMeta?.price ?? 0
const isDowngradeOption =
getPlanChangeType(subscription?.plan.id, plan?.planId) === 'downgrade'
const isCurrentPlan = planMeta?.id === subscription?.plan?.id
const features = plan.features
const footer = plan.footer
const source = Array.isArray(router.query.source)
? router.query.source[0]
: router.query.source
const shouldHighlight = source === 'log-drains-empty-state' && plan.id === 'tier_team'
if (plan.id === 'tier_enterprise') {
return <EnterpriseCard key={plan.id} plan={plan} isCurrentPlan={isCurrentPlan} />
}
return (
<div
key={plan.id}
className={cn(
'px-4 py-4 flex flex-col items-start justify-between',
'border rounded-md col-span-12 md:col-span-4 bg-surface-200',
shouldHighlight &&
'ring-4 ring-brand animate-[pulse_1.5s_ease-in-out_1] shadow-md shadow-brand/40'
)}
>
<div className="w-full">
<div className="flex items-center space-x-2">
<p className="text-brand-link text-sm uppercase">{plan.name}</p>
{isCurrentPlan ? (
<div className="text-xs bg-surface-300 text-foreground-light rounded px-2 py-0.5">
Current plan
</div>
) : plan.nameBadge ? (
<div className="text-xs bg-brand-300 dark:bg-brand-400 text-brand-600 rounded px-2 py-0.5">
{plan.nameBadge}
</div>
) : null}
</div>
<div className="mt-4 flex items-center space-x-1 mb-4">
{(price ?? 0) > 0 && <p className="text-foreground-light text-sm">From</p>}
{isLoadingPlans ? (
<div className="h-[28px] flex items-center justify-center">
<ShimmeringLoader className="w-[30px] h-[24px]" />
</div>
) : (
<p className="text-foreground text-lg" translate="no">
{formatCurrency(price)}
</p>
)}
<p className="text-foreground-light text-sm">{plan.costUnit}</p>
</div>
{isCurrentPlan ? (
<Button block disabled type="default">
Current plan
</Button>
) : !canUpdateSubscription ? (
<RequestUpgradeToBillingOwners block plan={plan.name as 'Pro' | 'Team'} />
) : (
<ButtonTooltip
block
type={isDowngradeOption ? 'default' : 'primary'}
disabled={
subscription?.plan?.id === 'enterprise' ||
subscription?.plan?.id === 'platform' ||
// Downgrades to free are still allowed through the dashboard given we have much better control about showing customers the impact + any possible issues with downgrading to free
(selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE &&
plan.id !== 'tier_free') ||
// Orgs managed by AWS marketplace are not allowed to change the plan
selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ||
hasOrioleProjects
}
onClick={() => {
setSelectedTier(plan.id as any)
sendEvent({
action: 'studio_pricing_plan_cta_clicked',
properties: {
selectedPlan: plan.name,
currentPlan: subscription?.plan?.name,
},
groups: { organization: slug ?? 'Unknown' },
})
}}
tooltip={{
content: {
side: 'bottom',
className: hasOrioleProjects ? 'w-96 text-center' : '',
text:
subscription?.plan?.id === 'enterprise' ||
subscription?.plan?.id === 'platform'
? 'Reach out to us via support to update your plan'
: hasOrioleProjects
? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.'
: selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
? 'You cannot change the plan for an organization managed by AWS Marketplace'
: undefined,
},
}}
>
{isDowngradeOption ? 'Downgrade' : 'Upgrade'} to {plan.name}
</ButtonTooltip>
)}
<div className="border-t my-4" />
<ul role="list">
{features.map((feature) => (
<li
key={typeof feature === 'string' ? feature : feature[0]}
className="flex py-2"
>
<div className="w-[12px]">
<Check
className="h-3 w-3 text-brand translate-y-[2.5px]"
aria-hidden="true"
strokeWidth={3}
/>
</div>
<div>
<p className="ml-3 text-xs text-foreground-light">
{typeof feature === 'string' ? feature : feature[0]}
</p>
{isArray(feature) && (
<p className="ml-3 text-xs text-foreground-lighter">{feature[1]}</p>
)}
</div>
</li>
))}
</ul>
</div>
{footer && (
<div className="border-t pt-4 mt-4">
<p className="text-foreground-light text-xs">{footer}</p>
</div>
)}
</div>
)
})}
</div>
</SidePanel.Content>
</SidePanel>
<DowngradeModal
visible={selectedTier === 'tier_free'}
subscription={subscription}
onClose={() => setSelectedTier(undefined)}
onConfirm={onConfirmDowngrade}
projects={orgProjects}
/>
<SubscriptionPlanUpdateDialog
selectedTier={selectedTier}
onClose={() => setSelectedTier(undefined)}
planMeta={planMeta}
subscriptionPreviewError={subscriptionPreviewError}
subscriptionPreviewIsLoading={subscriptionPreviewIsLoading}
subscriptionPreviewInitialized={subscriptionPreviewInitialized}
subscriptionPreview={subscriptionPreview}
subscription={subscription}
projects={orgProjects}
currentPlanMeta={{
...availablePlans.find((p) => p.id === subscription?.plan?.id),
features:
subscriptionsPlans.find((plan) => plan.id === `tier_${subscription?.plan?.id}`)
?.features || [],
}}
/>
<MembersExceedLimitModal
visible={showDowngradeError}
onClose={() => setShowDowngradeError(false)}
/>
<ExitSurveyModal
visible={showExitSurvey}
projects={orgProjects}
onClose={(success?: boolean) => {
setShowExitSurvey(false)
if (success) onClose()
}}
/>
<UpgradeSurveyModal
visible={showUpgradeSurvey}
originalPlan={originalPlanRef.current}
subscription={subscription}
onClose={(success?: boolean) => {
setShowUpgradeSurvey(false)
if (success) onClose()
}}
/>
</>
)
}
Domain
Subdomains
Source
Frequently Asked Questions
What does PlanUpdateSidePanel() do?
PlanUpdateSidePanel() is a function in the supabase codebase.
What does PlanUpdateSidePanel() call?
PlanUpdateSidePanel() calls 1 function(s): getPartnerManagedResourceCta.
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free