Chore/request upgrade plan modal (#40889)

* init request upgrade plan modal

* Consolidate UpgradeToPro components across UI + use UpgradePlanButton, UpgradePlanButton determines whether to show request upgrade CTA

* Allow to pass icon to Admonition

* Tiny fix on upgrade plan button CTA to consider addons

* Hook up upgrade request endpoint

* Update API types

* remove hardcode

* Add request upgrade CTA in plan side panel

* Fix disk compute

* Show request upgrade button for change to large compute

* Nit
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-12-04 16:02:01 +08:00
1 parent 4a93272682
commit de50740b04
31 files changed
+708 -412

No files matched your search

@@ -8,7 +8,7 @@ import { boolean, number, object, string } from 'yup'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -505,8 +505,10 @@ export const MfaAuthSettingsForm = () => {
{promptProPlanUpgrade && (
<UpgradeToPro
fullWidth
primaryText="Configuring Advanced MFA is only available on the Pro plan and above"
secondaryText="Upgrade to the Pro plan to configure Advanced MFA settings"
source="authSmsMfa"
featureProposition="configure settings for SMS MFA"
primaryText="SMS MFA is only available on the Pro Plan and above"
secondaryText="Upgrade to the Pro plan to configure settings for SMS MFA"
/>
)}
@@ -8,7 +8,7 @@ import * as z from 'zod'
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useMaxConnectionsQuery } from 'data/database/max-connections-query'
@@ -194,6 +194,8 @@ export const PerformanceSettingsForm = () => {
<ScaffoldSection isFullWidth>
{promptProPlanUpgrade && (
<UpgradeToPro
source="authPerformance"
featureProposition="configure advanced Auth server settings"
primaryText="Configuring Auth server performance is only available on the Pro plan and above"
secondaryText="Upgrade to the Pro plan to configure settings for your Auth server"
/>
@@ -8,7 +8,7 @@ import * as z from 'zod'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -368,6 +368,8 @@ export const SessionsAuthSettingsForm = () => {
{promptProPlanUpgrade && (
<UpgradeToPro
fullWidth
source="authSessions"
featureProposition="configure user sessions"
primaryText="Configuring user sessions is only available on the Pro Plan and above"
secondaryText="Upgrade to the Pro plan to configure settings for user sessions"
/>
@@ -17,7 +17,7 @@ import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
@@ -27,6 +27,7 @@ import { projectKeys } from 'data/projects/keys'
import { DesiredInstanceSize, instanceSizeSpecs } from 'data/projects/new-project.constants'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -60,7 +61,6 @@ import {
estimateDiskCost,
estimateRestoreTime,
} from './BranchManagement.utils'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
export const CreateBranchModal = () => {
const { ref } = useParams()
@@ -268,14 +268,14 @@ export const CreateBranchModal = () => {
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
{promptPlanUpgrade && (
<>
<UpgradeToPro
primaryText="Upgrade to unlock branching"
secondaryText="Create and test schema changes, functions, and more in a separate, temporary instance without affecting production"
source="create-branch"
/>
<DialogSectionSeparator />
</>
<UpgradeToPro
fullWidth
layout="vertical"
source="create-branch"
featureProposition="enable branching"
primaryText="Upgrade to unlock branching"
secondaryText="Create and test schema changes, functions, and more in a separate, temporary instance without affecting production"
/>
)}
<DialogSection
@@ -6,7 +6,7 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation'
import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query'
import { useSetProjectStatus } from 'data/projects/project-detail-query'
@@ -57,10 +57,11 @@ export const BackupsList = () => {
return (
<UpgradeToPro
addon="pitr"
icon={<Clock size={20} />}
source="backups"
featureProposition="have up to 7 days of scheduled backups"
icon={<Clock size={20} strokeWidth={1.5} />}
primaryText="Free Plan does not include project backups."
secondaryText="Upgrade to the Pro Plan for up to 7 days of scheduled backups."
source="backups"
/>
)
}
@@ -147,10 +147,9 @@ export const Destinations = () => {
{isPaidPlan ? (
<EnableReplicationModal />
) : (
<UpgradePlanButton type="primary" plan="Pro" />
<UpgradePlanButton source="replication" featureProposition="use replication" />
)}
{/* [Joshen] Placeholder for when we have documentation */}
<DocsButton href={`${DOCS_URL}`} />
<DocsButton href={`${DOCS_URL}/guides/database/replication#replication`} />
</div>
</div>
) : hasDestinations ? (
@@ -14,7 +14,7 @@ import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import Panel from 'components/ui/Panel'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { useCloneBackupsQuery } from 'data/projects/clone-query'
import { useCloneStatusQuery } from 'data/projects/clone-status-query'
@@ -106,6 +106,7 @@ export const RestoreToNewProject = () => {
<UpgradeToPro
buttonText="Upgrade"
source="backupsRestoreToNewProject"
featureProposition="enable restoring to new project"
primaryText="Restore to a new project requires a pro plan or above."
secondaryText="To restore to a new project, you need to upgrade to a Pro plan and have physical backups enabled."
/>
@@ -9,7 +9,8 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold'
import { DocsButton } from 'components/ui/DocsButton'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { RequestUpgradeToBillingOwners } from 'components/ui/RequestUpgradeToBillingOwners'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import {
useDiskAttributesQuery,
useRemainingDurationForDiskAttributeUpdate,
@@ -211,7 +212,7 @@ export function DiskManagementForm() {
const usedPercentage = (usedSize / totalSize) * 100
const disableIopsThroughputConfig =
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
!isSpendCapEnabled && RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
const isBranch = project?.parent_project_ref !== undefined
@@ -321,103 +322,121 @@ export function DiskManagementForm() {
if (isDiskAttributesSuccess || isSuccess) {
form.reset(defaultValues, {})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess, isDiskAttributesSuccess])
return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
<NoticeBar
type="default"
visible={isEntitlementsLoaded && isPlanUpgradeRequired}
title="Compute and Disk configuration is not available on the Free Plan"
actions={<UpgradePlanButton source="diskManagementConfigure" plan="Pro" />}
description="You will need to upgrade to at least the Pro Plan to configure compute and disk"
<>
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
{isEntitlementsLoaded && isPlanUpgradeRequired && (
<UpgradeToPro
featureProposition="configure compute and disk"
primaryText="Compute and Disk configuration is not available on the Free Plan"
secondaryText="You will need to upgrade to at least the Pro Plan to configure compute and disk"
/>
)}
{isProjectResizing ||
isProjectRequestingDiskChanges ||
(noPermissions && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
/>
<NoticeBar
type="default"
visible={isProjectRequestingDiskChanges}
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
<NoticeBar
type="default"
visible={noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
</div>
))}
<Separator />
<ComputeSizeField form={form} disabled={disableComputeInputs} />
{!(isAws || isAwsNimbus) && <Separator />}
<div className="flex flex-col gap-y-2">
<SpendCapDisabledSection />
{(isProjectResizing ||
isProjectRequestingDiskChanges ||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
/>
<NoticeBar
type="default"
visible={!(isAws || isAwsNimbus)}
title="Disk configuration is only available for projects in the AWS cloud provider"
description={
isAwsK8s
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
: isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
}
visible={isProjectRequestingDiskChanges}
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
<NoticeBar
type="default"
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
</div>
)}
<Separator />
</ScaffoldContainer>
<Form_Shadcn_ {...form}>
<form
id="disk-compute-form"
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-8"
>
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
<ComputeSizeField form={form} disabled={disableComputeInputs} />
{!(isAws || isAwsNimbus) && <Separator />}
<SpendCapDisabledSection />
<div className="flex flex-col gap-y-4">
<NoticeBar
type="default"
visible={!(isAws || isAwsNimbus)}
title="Disk configuration is only available for projects in the AWS cloud provider"
description={
isAwsK8s
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
: isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
}
/>
{isAws && (
<>
<div className="flex flex-col gap-y-3">
<DiskCountdownRadial />
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
<Admonition
type="destructive"
className="mb-0"
title="Database size is currently over 90% of disk size"
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
/>
</Admonition>
)}
{isReadOnlyMode && (
<Admonition
type="destructive"
className="mb-0"
title="Project is currently in read-only mode"
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
/>
</Admonition>
)}
</div>
<DiskSizeField
form={form}
disableInput={disableDiskInputs}
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
/>
</>
)}
</div>
{isAws && (
<>
<div className="flex flex-col gap-y-3">
<DiskCountdownRadial />
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
<Admonition
type="destructive"
title="Database size is currently over 90% of disk size"
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
/>
</Admonition>
)}
{isReadOnlyMode && (
<Admonition
type="destructive"
title="Project is currently in read-only mode"
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
/>
</Admonition>
)}
</div>
<DiskSizeField
form={form}
disableInput={disableDiskInputs}
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
/>
<Separator />
<Collapsible_Shadcn_
// TO DO: wrap component into pattern
className="-space-y-px"
@@ -440,90 +459,100 @@ export function DiskManagementForm() {
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_
className={cn(
'flex flex-col gap-8 py-8 transition-all',
'transition-all rounded-b',
'data-[state=open]:border data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down'
)}
>
<div className="px-8 flex flex-col gap-y-8">
<AutoScaleFields form={form} />
</div>
<Separator />
<div className="px-8 flex flex-col gap-y-8">
<NoticeBar
type="default"
visible={disableIopsThroughputConfig}
title="Adjusting disk configuration requires LARGE Compute size or above"
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
actions={
<Button
type="default"
onClick={() => {
form.setValue('computeSize', 'ci_large')
}}
>
Change to LARGE Compute
</Button>
}
/>
<StorageTypeField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
<IOPSField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
<ThroughputField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
<div className="flex flex-col gap-y-8 py-8">
<div className="px-8 flex flex-col gap-y-8">
<AutoScaleFields form={form} />
</div>
<Separator />
<div className="px-8 flex flex-col gap-y-8">
<NoticeBar
type="default"
visible={disableIopsThroughputConfig}
title="Adjusting disk configuration requires LARGE Compute size or above"
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
actions={
canUpdateDiskConfiguration ? (
<Button
type="default"
onClick={() => {
form.setValue('computeSize', 'ci_large')
}}
>
Change to LARGE Compute
</Button>
) : (
<RequestUpgradeToBillingOwners
addon="computeSize"
featureProposition="adjust disk configuration"
/>
)
}
/>
<StorageTypeField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
<IOPSField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
<ThroughputField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
/>
</div>
</div>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
</>
)}
</div>
</ScaffoldContainer>
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
>
<div
className={cn(
MAX_WIDTH_CLASSES,
PADDING_CLASSES,
'flex items-center gap-3 justify-end'
)}
</ScaffoldContainer>
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
>
<FormFooterChangeBadge formState={formState} />
<Button
type="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
<div
className={cn(
MAX_WIDTH_CLASSES,
PADDING_CLASSES,
'flex items-center gap-3 justify-end'
)}
>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</Form_Shadcn_>
<FormFooterChangeBadge formState={formState} />
<Button
type="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</Form_Shadcn_>
</>
)
}
@@ -1,15 +1,15 @@
import { AnimatePresence, motion } from 'framer-motion'
import { LucideIcon } from 'lucide-react'
import { ComponentProps } from 'react'
import { type ComponentProps, type ReactNode } from 'react'
import { cn } from 'ui'
import { Admonition } from 'ui-patterns'
interface NoticeBarProps extends Omit<ComponentProps<typeof Admonition>, 'description'> {
title?: string
description?: string
icon?: LucideIcon
icon?: ReactNode
visible: boolean
actions?: React.ReactNode
actions?: ReactNode
}
export function NoticeBar({ visible, description, actions, ...props }: NoticeBarProps) {
return (
@@ -21,7 +21,7 @@ export function NoticeBar({ visible, description, actions, ...props }: NoticeBar
exit={{ opacity: 0, height: 0, y: 4 }}
transition={{ duration: 0.15 }}
>
<Admonition {...props}>
<Admonition {...props} className={cn(props.className, 'mb-0')}>
{description}
<div className="flex flex-col gap-2">
{actions && <div className="mt-2">{actions}</div>}
@@ -27,7 +27,7 @@ export function SpendCapDisabledSection() {
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<Admonition type="default">
<Admonition type="default" className="mb-0">
<AlertTitle>Disable spend cap to manage Disk configuration</AlertTitle>
<AlertDescription>
To access disk management features, you need to disable your spend cap. These features
@@ -36,9 +36,11 @@ export const LogDrainsEmpty = () => {
<p className="text-foreground-light mb-6">
Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform
</p>
<UpgradePlanButton type="primary" plan="Team" source="log-drains-empty-state">
Upgrade plan
</UpgradePlanButton>
<UpgradePlanButton
plan="Team"
source="log-drains-empty-state"
featureProposition="use Log Drains"
/>
</div>
<Card
className={cn('grid grid-cols-1 bg divide-x mb-8', IS_PLATFORM && '@xl:grid-cols-2')}
@@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { FilterPopover } from 'components/ui/FilterPopover'
import NoPermission from 'components/ui/NoPermission'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
import {
AuditLog,
@@ -153,31 +153,13 @@ export const AuditLogs = () => {
return (
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<Alert_Shadcn_
variant="default"
title="Organization Audit Logs are not available on Free or Pro plans"
>
<WarningIcon />
<div className="flex flex-col md:flex-row pt-1 gap-4">
<div className="grow">
<AlertTitle_Shadcn_>
Organization Audit Logs are not available on Free or Pro plans
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
<p>
Upgrade to Team or Enterprise to view up to 62 days of Audit Logs for your
organization.
</p>
</AlertDescription_Shadcn_>
</div>
<div className="flex items-center">
<UpgradePlanButton source="auditLogs" plan="Team" type="primary">
Upgrade subscription
</UpgradePlanButton>
</div>
</div>
</Alert_Shadcn_>
<UpgradeToPro
plan="Team"
source="organizationAuditLogs"
primaryText="Organization Audit Logs are not available on Free or Pro plans"
secondaryText="Upgrade to Team or Enterprise to view up to 62 days of Audit Logs for your organization."
featureProposition="enable audit logs"
/>
</ScaffoldSection>
</ScaffoldContainer>
)
@@ -157,7 +157,10 @@ export const BillingMetric = ({
</div>
) : (
<div>
<UpgradePlanButton source={`billingBreakdownUsage${metric.anchor}`}>
<UpgradePlanButton
source={`billingBreakdownUsage${metric.anchor}`}
featureProposition={`to use ${metric.name}`}
>
Upgrade
</UpgradePlanButton>
</div>
@@ -135,7 +135,7 @@ const DowngradeModal = ({
<strong>Before you downgrade to the {selectedPlan?.name} plan, consider:</strong>
<ul className="space-y-2 mt-2">
<li className="list-disc ml-6 text-foreground-light">
Your projects no longer require their respective add ons.
Your projects no longer require their respective add-ons.
</li>
<li className="list-disc ml-6 text-foreground-light">
Your resource consumption are well within the {selectedPlan?.name} plan's quota.
@@ -9,6 +9,7 @@ import { StudioPricingSidePanelOpenedEvent } from 'common/telemetry-constants'
import { getPlanChangeType } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import { RequestUpgradeToBillingOwners } from 'components/ui/RequestUpgradeToBillingOwners'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview'
@@ -232,6 +233,8 @@ export const PlanUpdateSidePanel = () => {
<Button block disabled type="default">
Current plan
</Button>
) : !canUpdateSubscription ? (
<RequestUpgradeToBillingOwners block plan={plan.name as 'Pro' | 'Team'} />
) : (
<ButtonTooltip
block
@@ -243,8 +246,7 @@ export const PlanUpdateSidePanel = () => {
plan.id !== 'tier_free') ||
// Orgs managed by AWS marketplace are not allowed to change the plan
selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ||
hasOrioleProjects ||
!canUpdateSubscription
hasOrioleProjects
}
onClick={() => {
setSelectedTier(plan.id as any)
@@ -266,12 +268,9 @@ export const PlanUpdateSidePanel = () => {
? 'Reach out to us via support to update your plan from Enterprise'
: 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.'
: !canUpdateSubscription
? 'You do not have permission to change the subscription plan'
: selectedOrganization?.managed_by ===
MANAGED_BY.AWS_MARKETPLACE
? 'You cannot change the plan for an organization managed by AWS Marketplace'
: undefined,
: selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
? 'You cannot change the plan for an organization managed by AWS Marketplace'
: undefined,
},
}}
>
@@ -7,16 +7,13 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useSSOConfigCreateMutation } from 'data/sso/sso-config-create-mutation'
import { useOrgSSOConfigQuery } from 'data/sso/sso-config-query'
import { useSSOConfigUpdateMutation } from 'data/sso/sso-config-update-mutation'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { DOCS_URL } from 'lib/constants'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Card,
CardContent,
@@ -25,7 +22,6 @@ import {
FormField_Shadcn_,
Form_Shadcn_,
Switch,
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { AttributeMapping } from './AttributeMapping'
@@ -159,30 +155,13 @@ export const SSOConfig = () => {
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
{!!plan && !canSetupSSOConfig ? (
<Alert_Shadcn_
variant="default"
title="Organization MFA enforcement is not available on Free plan"
>
<WarningIcon />
<div className="flex flex-col md:flex-row pt-1 gap-4">
<div className="grow">
<AlertTitle_Shadcn_>
Organization Single Sign-on (SSO) is available from Team plan and above
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
<p className="max-w-3xl">
SSO as a login option provides additional acccount security for your team by
enforcing the use of an identity provider when logging into Supabase. Upgrade to
Team or above to set up SSO for your organization.
</p>
</AlertDescription_Shadcn_>
</div>
<div className="flex items-center">
<UpgradePlanButton type="primary" source="sso" plan="Team" />
</div>
</div>
</Alert_Shadcn_>
<UpgradeToPro
plan="Team"
source="organizationSso"
primaryText="Organization Single Sign-on (SSO) is available from Team plan and above"
secondaryText="SSO as a login option provides additional acccount security for your team by enforcing the use of an identity provider when logging into Supabase. Upgrade to Team or above to set up SSO for your organization."
featureProposition="enable Single Sign-on (SSO)"
/>
) : (
<>
{isLoadingSSOConfig && (
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
import { useOrganizationMfaToggleMutation } from 'data/organizations/organization-mfa-mutation'
import { useOrganizationMfaQuery } from 'data/organizations/organization-mfa-query'
@@ -20,9 +20,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useProfile } from 'lib/profile'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
Card,
CardContent,
@@ -34,7 +31,6 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
@@ -112,28 +108,12 @@ export const SecuritySettings = () => {
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
{!isPaidPlan ? (
<Alert_Shadcn_
variant="default"
title="Organization MFA enforcement is not available on Free plan"
>
<WarningIcon />
<div className="flex flex-col md:flex-row pt-1 gap-4">
<div className="grow">
<AlertTitle_Shadcn_>
Organization MFA enforcement is not available on Free plan
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
<p>Upgrade to Pro or above to enforce MFA requirements for your organization.</p>
</AlertDescription_Shadcn_>
</div>
<div className="flex items-center">
<UpgradePlanButton type="primary" source="mfa" plan="Pro">
Upgrade subscription
</UpgradePlanButton>
</div>
</div>
</Alert_Shadcn_>
<UpgradeToPro
source="organizationMfa"
primaryText="Organization MFA enforcement is not available on Free plan"
secondaryText="Upgrade to Pro or above to enforce MFA requirements for your organization."
featureProposition="enforce MFA requirements"
/>
) : (
<>
{isLoadingMfa || isLoadingPermissions ? (
@@ -311,7 +311,11 @@ export const InviteMemberButton = () => {
</Link>
</Button>
{(currentPlan?.id === 'free' || currentPlan?.id === 'pro') && (
<UpgradePlanButton source="inviteMemberSSO" plan="Team" />
<UpgradePlanButton
plan="Team"
source="inviteMemberSSO"
featureProposition="enable Single Sign-on (SSO)"
/>
)}
</div>
</div>
@@ -32,7 +32,10 @@ export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLim
projects before you're able to create a free project within this organization.
</p>
<UpgradePlanButton source="freeProjectLimitWarning">Upgrade plan</UpgradePlanButton>
<UpgradePlanButton
source="freeProjectLimitWarning"
featureProposition="create more projects"
/>
</div>
}
/>
@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
@@ -20,7 +21,6 @@ import {
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import Link from 'next/link'
import {
Button,
Card,
@@ -399,7 +399,11 @@ export const RealtimeSettings = () => {
</div>
<div className="flex-grow flex items-center justify-end">
{isFreePlan ? (
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
<UpgradePlanButton
source="realtimeSettings"
addon="spendCap"
featureProposition="configure the max events per second parameter of realtime settings"
/>
) : (
<ToggleSpendCapButton />
)}
@@ -457,7 +461,11 @@ export const RealtimeSettings = () => {
</div>
<div className="flex-grow flex items-center justify-end">
{isFreePlan ? (
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
<UpgradePlanButton
source="realtimeSettings"
addon="spendCap"
featureProposition="configure the max presence events per second parameter of realtime settings"
/>
) : (
<ToggleSpendCapButton />
)}
@@ -515,7 +523,11 @@ export const RealtimeSettings = () => {
</div>
<div className="flex-grow flex items-center justify-end">
{isFreePlan ? (
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
<UpgradePlanButton
addon="spendCap"
source="realtimeSettings"
featureProposition="configure the max payload size parameter of realtime settings"
/>
) : (
<ToggleSpendCapButton />
)}
@@ -8,13 +8,14 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation'
import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import type { AddonVariantId } from 'data/subscriptions/types'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -22,7 +23,6 @@ import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { formatCurrency } from 'lib/helpers'
import { useAddonsPagePanel } from 'state/addons-page'
import {
Alert,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
@@ -32,7 +32,6 @@ import {
SidePanel,
cn,
} from 'ui'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
const PITR_CATEGORY_OPTIONS: {
id: 'off' | 'on'
@@ -272,32 +271,21 @@ const PITRSidePanel = () => {
{selectedCategory === 'on' && (
<div className="!mt-8 pb-4">
{!hasAccessToPitrVariants ? (
<Alert
withIcon
variant="info"
<UpgradeToPro
className="mb-4"
title="Changing your Point-In-Time-Recovery is only available on the Pro Plan"
actions={<UpgradePlanButton source="pitrSidePanel" plan="Pro" />}
>
Upgrade your plan to change PITR for your project
</Alert>
addon="pitr"
primaryText="Changing your Point-In-Time-Recovery is only available on the Pro Plan"
secondaryText="Upgrade your plan to change PITR for your project"
featureProposition="enable PITR"
/>
) : !hasSufficientCompute ? (
<Alert
withIcon
variant="warning"
<UpgradeToPro
className="mb-4"
title="Your project is required to minimally be on a Small compute size to enable PITR"
actions={[
<Button asChild key="change-compute" type="default">
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
Change compute size
</Link>
</Button>,
]}
>
This is to ensure that your project has enough resources to execute PITR
successfully
</Alert>
addon="computeSize"
primaryText="Project needs to be at least on a Small compute size to enable PITR"
secondaryText="This ensures enough resources to execute PITR successfully"
featureProposition="enable PITR"
/>
) : null}
<Radio.Group
@@ -6,7 +6,7 @@ import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { InlineLinkClassName } from 'components/ui/InlineLink'
import Panel from 'components/ui/Panel'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import {
useCustomDomainsQuery,
type CustomDomainsData,
@@ -63,7 +63,6 @@ export const CustomDomainConfig = () => {
</Panel>
) : !hasCustomDomainAddon ? (
<UpgradeToPro
icon={<AlertCircle size={18} strokeWidth={1.5} />}
primaryText={
customDomainsDisabledDueToQuota
? 'New custom domains are temporarily disabled'
@@ -74,10 +73,11 @@ export const CustomDomainConfig = () => {
? 'We are working with our upstream DNS provider before we are able to sign up new custom domains. Please check back in a few hours.'
: plan === 'free'
? 'Paid Plans come with free vanity subdomains or Custom Domains for an additional $10/month per domain.'
: 'To configure a custom domain for your project, please enable the add-on. Each Custom Domains costs $10 per month.'
: 'To configure a custom domain for your project, please enable the add-on. Each Custom Domain costs $10 per month.'
}
addon="customDomain"
source="customDomain"
featureProposition="enable custom domains"
disabled={customDomainsDisabledDueToQuota}
/>
) : isCustomDomainsLoading ? (
@@ -9,7 +9,7 @@ import {
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -71,18 +71,20 @@ const GitHubSection = () => {
branch, keep your production branch in sync, and automatically create preview
branches for every pull request.
</p>
{promptProPlanUpgrade && (
{promptProPlanUpgrade ? (
<div className="mb-6">
<UpgradeToPro
source="github-integration"
featureProposition="use GitHub integrations"
primaryText="Upgrade to unlock GitHub integration"
secondaryText="Connect your GitHub repository to automatically sync preview branches and deploy changes."
source="github-integration"
/>
</div>
) : (
<div className={cn(promptProPlanUpgrade && 'opacity-25 pointer-events-none')}>
<GitHubIntegrationConnectionForm connection={existingConnection} />
</div>
)}
<div className={cn(promptProPlanUpgrade && 'opacity-25 pointer-events-none')}>
<GitHubIntegrationConnectionForm connection={existingConnection} />
</div>
</div>
</div>
)}
@@ -29,7 +29,10 @@ export const BucketsUpgradePlan = ({ type }: { type: 'analytics' | 'vector' }) =
description={`Upgrade to Pro to use ${type} buckets for your project`}
>
<div className="flex items-center gap-x-2">
<UpgradePlanButton type="primary" plan="Pro" />
<UpgradePlanButton
source={`${type}Buckets`}
featureProposition={`use ${type} buckets`}
/>
</div>
</EmptyStatePresentational>
</PageSectionContent>
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import { useBucketsQuery } from 'data/storage/buckets-query'
@@ -408,22 +408,25 @@ export const StorageSettings = () => {
{isFreeTier && (
<UpgradeToPro
fullWidth
source="storageSizeLimit"
featureProposition="configure upload file size limits in Storage"
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
secondaryText={`Upgrade to Pro Plan for a configurable upload file size limit of ${formatBytes(
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_UNCAPPED
)} and unlock image transformations.`}
source="storageSizeLimit"
/>
)}
{isSpendCapOn && (
<UpgradeToPro
fullWidth
addon="spendCap"
source="storageSizeLimit"
featureProposition="increase the file upload size limits in Storage"
buttonText="Disable Spend Cap"
primaryText="Reduced max upload file size limit due to Spend Cap"
secondaryText={`Disable your Spend Cap to allow file uploads of up to ${formatBytes(
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_UNCAPPED
)}.`}
source="storageSizeLimit"
/>
)}
@@ -0,0 +1,223 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PropsWithChildren, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
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 { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
Badge,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
TextArea_Shadcn_,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const FormSchema = z.object({
note: z.string().optional(),
})
const formId = 'request-upgrade-form'
interface RequestUpgradeToBillingOwnersProps {
block?: boolean
plan?: PlanRequest
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
/** Used in the default message template, e.g: "Upgrade to ..." */
featureProposition?: string
}
export const RequestUpgradeToBillingOwners = ({
block = false,
plan = 'Pro',
addon,
featureProposition,
children,
}: PropsWithChildren<RequestUpgradeToBillingOwnersProps>) => {
const [open, setOpen] = useState(false)
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const slug = organization?.slug
const isFreePlan = organization?.plan?.id === 'free'
const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
const { data: roles } = useOrganizationRolesV2Query({ slug: organization?.slug })
const orgRoles = roles?.org_scoped_roles ?? []
const { mutate: sendUpgradeRequest, isPending: isSubmitting } = useSendUpgradeRequestMutation({
onSuccess: () => {
toast.success('Successfully sent request to billing owners!')
setOpen(false)
},
})
const formattedAddonName =
addon === 'pitr' ? 'PITR' : addon === 'customDomain' ? 'Custom Domain' : ''
const target = !!project
? `for the project "${project?.name}"`
: !!organization
? `for the organization "${organization.name}"`
: ''
const action =
addon === 'spendCap'
? `disable spend cap`
: addon === 'computeSize'
? `change the compute size`
: `enable the ${formattedAddonName} add-on`
const titleText = !!addon
? addon === 'spendCap'
? `Request to disable spend cap`
: addon === 'computeSize'
? 'Request to change compute size'
: `Request to enable the ${formattedAddonName} add-on`
: `Request an upgrade for the ${plan} Plan`
const buttonText = !!children
? children
: !!addon
? addon === 'spendCap'
? 'Request to disable spend cap'
: addon === 'computeSize'
? 'Request to change compute'
: 'Request to enable addon'
: `Request upgrade to ${plan}`
const defaultValues = {
note: !!addon
? addon === 'spendCap'
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
: `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? ` to ${featureProposition} ` : ''}${target}`,
}
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues,
values: defaultValues,
})
// [Joshen] This is a pretty naive way of checking billing owners by raw role names
// Ideally we derive billing owners using permissions checking - but the current permissions
// logic is only contextualized to that of the current user, not other members
const billingOwners = members.filter((member) => {
const roles = member.role_ids
.map((x) => orgRoles.find((role) => role.id === x)?.name)
.filter(Boolean)
return !member.invited_id && (roles.includes('Owner') || roles.includes('Administrator'))
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
if (!slug) return console.error('Slug is required')
sendUpgradeRequest({ slug, plan, note: values.note })
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button block={block} type="primary">
{buttonText}
</Button>
</DialogTrigger>
<DialogContent>
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>{titleText}</DialogTitle>
<DialogDescription>
Let your organization's billing owners know your interest in this
</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-6">
<div className="flex flex-col gap-y-2">
<p className="text-sm">
Your request will be sent to the following emails, who are billing owners of your
organization:
</p>
<div className="text-sm flex gap-x-2">
<p>
{billingOwners
.slice(0, 2)
.map((x) => x.primary_email)
.join(', ')}
</p>
{billingOwners.length > 2 && (
<Tooltip>
<TooltipTrigger tabIndex={-1}>
<Badge>+1 others</Badge>
</TooltipTrigger>
<TooltipContent side="bottom">
<ul className="">
{billingOwners.slice(2).map((x) => (
<li key={x.gotrue_id}>{x.primary_email}</li>
))}
</ul>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
<FormField_Shadcn_
control={form.control}
name="note"
render={({ field }) => (
<FormItemLayout
name="note"
label="Add a note to your request (optional)"
layout="vertical"
>
<FormControl_Shadcn_>
<TextArea_Shadcn_
id="note"
{...field}
rows={3}
placeholder={
!!addon
? addon === 'spendCap'
? 'e.g. We need to disabled spend cap on this project to do something'
: 'e.g. We need to enable this add-on to do something with the project'
: 'e.g. We need to upgrade to the Pro plan to use this feature'
}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</DialogSection>
<DialogFooter>
<Button type="default" disabled={isSubmitting} onClick={() => setOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" form={formId} loading={isSubmitting}>
Submit request
</Button>
</DialogFooter>
</form>
</Form_Shadcn_>
</DialogContent>
</Dialog>
)
}
+65 -10
View File
@@ -1,43 +1,69 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { useFlag, useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { Button } from 'ui'
import { ButtonTooltip } from './ButtonTooltip'
import { RequestUpgradeToBillingOwners } from './RequestUpgradeToBillingOwners'
export const PLAN_REQUEST_EMPTY_PLACEHOLDER =
'<Specify which plan to upgrade to: Pro | Team | Enterprise>'
interface UpgradePlanButtonProps {
source?: string
/** Stick to camel case for consistency */
source: string
type?: 'default' | 'primary'
plan?: 'Pro' | 'Team' | 'Enterprise'
href?: string // [Joshen] As an override if needed (Used in UpgradeToPro)
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
/** Used in the default message template for request upgrade dialog, e.g: "Upgrade to ..." */
featureProposition?: string
disabled?: boolean
}
/**
* If `billingAll` is enabled, links to upgrade paths (e.g organization settings, addons).
*
* Otherwise, links to support form instead
*/
export const UpgradePlanButton = ({
source,
type = 'default',
plan,
href: propsHref,
type = 'primary',
plan = 'Pro',
addon,
featureProposition,
disabled,
children,
}: PropsWithChildren<UpgradePlanButtonProps>) => {
const { ref } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const isFreePlan = organization?.plan?.id === 'free'
const slug = organization?.slug ?? '_'
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const { billingAll } = useIsFeatureEnabled(['billing:all'])
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const subject = `Enquiry to upgrade ${!!plan ? `to ${plan} ` : ''}plan for organization`
const message = `Name: ${organization?.name}\nSlug: ${organization?.slug}\nRequested plan: ${plan ?? PLAN_REQUEST_EMPTY_PLACEHOLDER}`
const message = `Name: ${organization?.name}\nSlug: ${slug}\nRequested plan: ${plan ?? PLAN_REQUEST_EMPTY_PLACEHOLDER}`
const href = billingAll
? propsHref ??
`/org/${slug}/billing?panel=subscriptionPlan${!!source ? `&source=${source}` : ''}`
: ''
const linkChildren = children || `Upgrade to ${plan}`
? isFreePlan
? `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
: addon === 'spendCap'
? `/org/${slug ?? '_'}/billing?panel=costControl&source=${source}`
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
: '/'
const linkChildren = children || (!!addon ? 'Enable add-on' : `Upgrade to ${plan}`)
const link = billingAll ? (
<Link href={href}>{linkChildren}</Link>
) : (
@@ -46,6 +72,35 @@ export const UpgradePlanButton = ({
</SupportLink>
)
if (!canUpdateSubscription) {
return (
<RequestUpgradeToBillingOwners
plan={plan}
addon={addon}
featureProposition={featureProposition}
>
{children}
</RequestUpgradeToBillingOwners>
)
}
if (projectUpdateDisabled) {
return (
<ButtonTooltip
disabled
type="primary"
tooltip={{
content: {
side: 'bottom',
text: 'Plan changes are currently disabled, our engineers are working on a fix',
},
}}
>
{linkChildren}
</ButtonTooltip>
)
}
return (
<Button asChild type={type} disabled={disabled}>
{link}
+35 -70
View File
@@ -1,100 +1,65 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ReactNode } from 'react'
import { useFlag } from 'common'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { cn } from 'ui'
import { ButtonTooltip } from './ButtonTooltip'
import { Admonition } from 'ui-patterns'
import { UpgradePlanButton } from './UpgradePlanButton'
interface UpgradeToProProps {
icon?: ReactNode
primaryText: string
secondaryText: string
addon?: 'pitr' | 'customDomain'
plan?: 'Pro' | 'Team' | 'Enterprise'
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
/** Used in the default message template for request upgrade dialog, e.g: "Upgrade to ..." */
featureProposition?: string
/** As an override for the button text in both upgrade + request to upgrade scenario */
buttonText?: string
/** Where the upgrade interest is coming from */
source?: string
disabled?: boolean
fullWidth?: boolean
className?: string
layout?: 'vertical' | 'horizontal'
}
const UpgradeToPro = ({
export const UpgradeToPro = ({
icon,
primaryText,
secondaryText,
plan: planToUpgrade = 'Pro',
addon,
featureProposition,
buttonText,
source = 'upgrade',
disabled = false,
fullWidth = false,
layout = 'horizontal',
className,
}: UpgradeToProProps) => {
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const plan = organization?.plan?.id
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
return (
<div
<Admonition
type="default"
icon={icon}
layout={layout}
title={primaryText}
description={secondaryText}
className={cn(
'block w-full py-4 px-6 bg-surface-200',
fullWidth ? 'border-b' : 'border rounded'
'bg-surface-100 mb-0',
fullWidth &&
'border-0 rounded-none border-b px-6 [&>svg]:left-6 [&>div>button]:w-min [&>div>button]:mt-3 [&>div>a]:w-min [&>div>a]:mt-3',
className
)}
>
<div className="flex gap-x-3">
{icon && <div className="mt-1">{icon}</div>}
<div className="flex flex-col @md:flex-row w-full @md:items-center justify-between gap-4 @md:gap-x-8">
<div className="space-y-1 flex-1 max-w-2xl">
<p className="text-sm">{primaryText}</p>
<div>
<p className="text-sm text-foreground-light">{secondaryText}</p>
</div>
</div>
{!canUpdateSubscription || projectUpdateDisabled ? (
<ButtonTooltip
disabled
type="primary"
tooltip={{
content: {
side: 'bottom',
text: projectUpdateDisabled
? 'Subscription changes are currently disabled, our engineers are working on a fix'
: !canUpdateSubscription
? 'You need additional permissions to amend subscriptions'
: undefined,
},
}}
>
{buttonText || (plan === 'free' ? 'Upgrade to Pro' : 'Enable add on')}
</ButtonTooltip>
) : (
<UpgradePlanButton
type="primary"
plan="Pro"
source={source}
disabled={disabled}
href={
plan === 'free'
? `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
: addon == null
? `/org/${organization?.slug ?? '_'}/billing?panel=costControl&source=${source}`
: `/project/${project?.ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
}
>
{buttonText || (plan === 'free' ? 'Upgrade to Pro' : 'Enable add on')}
</UpgradePlanButton>
)}
</div>
</div>
</div>
actions={
<UpgradePlanButton
plan={planToUpgrade}
addon={addon}
source={source}
featureProposition={featureProposition}
disabled={disabled}
>
{buttonText}
</UpgradePlanButton>
}
/>
)
}
export default UpgradeToPro
@@ -0,0 +1,52 @@
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
import { components } from 'api-types'
import { handleError, post } from 'data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from 'types'
export type PlanRequest = components['schemas']['RequestUpgradeBody']['requested_plan']
type SendUpgradeRequestVariables = {
slug?: string
plan: PlanRequest
note?: string
}
async function sendUpgradeRequest({ slug, plan, note }: SendUpgradeRequestVariables) {
if (!slug) throw new Error('Slug is required')
const { data, error } = await post('/platform/organizations/{slug}/billing/upgrade-request', {
params: { path: { slug } },
body: { requested_plan: plan, note },
})
if (error) handleError(error)
return data
}
type SendUpgradeRequestData = Awaited<ReturnType<typeof sendUpgradeRequest>>
export const useSendUpgradeRequestMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<SendUpgradeRequestData, ResponseError, SendUpgradeRequestVariables>,
'mutationFn'
> = {}) => {
return useMutation<SendUpgradeRequestData, ResponseError, SendUpgradeRequestVariables>({
mutationFn: (vars) => sendUpgradeRequest(vars),
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to send upgrade request: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useBackupsQuery } from 'data/database/backups-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -104,6 +104,7 @@ const PITR = () => {
<UpgradeToPro
addon="pitr"
source="pitr"
featureProposition="enable Point in Time Recovery"
primaryText="Point in Time Recovery is a Pro Plan add-on."
secondaryText={
plan === 'free'
+5 -1
View File
@@ -22,6 +22,7 @@ export interface AdmonitionProps {
}
layout?: 'horizontal' | 'vertical'
actions?: ReactNode
icon?: ReactNode
}
const admonitionToAlertMapping: Record<
@@ -104,6 +105,7 @@ export const Admonition = forwardRef<
layout = 'vertical',
actions,
childProps = {},
icon,
...props
},
ref
@@ -122,7 +124,9 @@ export const Admonition = forwardRef<
props.className
)}
>
{(showIcon && typeMapped === 'warning') || typeMapped === 'destructive' ? (
{!!icon ? (
icon
) : (showIcon && typeMapped === 'warning') || typeMapped === 'destructive' ? (
<WarningIcon />
) : showIcon ? (
<InfoIcon />