From de50740b040b6932f2314b445ff9eea425cfdbb8 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 4 Dec 2025 16:02:01 +0800 Subject: [PATCH] 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 --- .../MfaAuthSettingsForm.tsx | 8 +- .../Auth/PerformanceSettingsForm.tsx | 4 +- .../SessionsAuthSettingsForm.tsx | 4 +- .../BranchManagement/CreateBranchModal.tsx | 20 +- .../Database/Backups/BackupsList.tsx | 7 +- .../Database/Replication/Destinations.tsx | 5 +- .../RestoreToNewProject.tsx | 3 +- .../DiskManagement/DiskManagementForm.tsx | 353 ++++++++++-------- .../DiskManagement/ui/NoticeBar.tsx | 10 +- .../ui/SpendCapDisabledSection.tsx | 2 +- .../interfaces/LogDrains/LogDrainsEmpty.tsx | 8 +- .../Organization/AuditLogs/AuditLogs.tsx | 34 +- .../BillingBreakdown/BillingMetric.tsx | 5 +- .../Subscription/DowngradeModal.tsx | 2 +- .../Subscription/PlanUpdateSidePanel.tsx | 15 +- .../interfaces/Organization/SSO/SSOConfig.tsx | 37 +- .../Organization/SecuritySettings.tsx | 34 +- .../TeamSettings/InviteMemberButton.tsx | 6 +- .../FreeProjectLimitWarning.tsx | 5 +- .../interfaces/Realtime/RealtimeSettings.tsx | 20 +- .../Settings/Addons/PITRSidePanel.tsx | 40 +- .../CustomDomainConfig/CustomDomainConfig.tsx | 6 +- .../GithubIntegration/GithubSection.tsx | 14 +- .../interfaces/Storage/BucketsUpgradePlan.tsx | 5 +- .../StorageSettings/StorageSettings.tsx | 9 +- .../ui/RequestUpgradeToBillingOwners.tsx | 223 +++++++++++ .../components/ui/UpgradePlanButton.tsx | 75 +++- apps/studio/components/ui/UpgradeToPro.tsx | 105 ++---- .../organizations/request-upgrade-mutation.ts | 52 +++ .../project/[ref]/database/backups/pitr.tsx | 3 +- packages/ui-patterns/src/admonition.tsx | 6 +- 31 files changed, 708 insertions(+), 412 deletions(-) create mode 100644 apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx create mode 100644 apps/studio/data/organizations/request-upgrade-mutation.ts diff --git a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx index d9c5fd1355e..0182b046085 100644 --- a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx @@ -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 && ( )} diff --git a/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx index 193e16a850b..34069c2a392 100644 --- a/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx @@ -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 = () => { {promptProPlanUpgrade && ( diff --git a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx index e2cf14b42a3..857a2a910cb 100644 --- a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx @@ -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 && ( diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index a374ead67ee..e89367188e1 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -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 = () => {
{promptPlanUpgrade && ( - <> - - - + )} { return ( } + source="backups" + featureProposition="have up to 7 days of scheduled backups" + icon={} primaryText="Free Plan does not include project backups." secondaryText="Upgrade to the Pro Plan for up to 7 days of scheduled backups." - source="backups" /> ) } diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index 5ccee96a937..90ce68a08c0 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -147,10 +147,9 @@ export const Destinations = () => { {isPaidPlan ? ( ) : ( - + )} - {/* [Joshen] Placeholder for when we have documentation */} - + ) : hasDestinations ? ( diff --git a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx index ebffb13fd31..30b151161d5 100644 --- a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx +++ b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx @@ -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 = () => { diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 6a3b5a34bd2..492c8d443db 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -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 ( - - - - } - description="You will need to upgrade to at least the Pro Plan to configure compute and disk" + <> + + {isEntitlementsLoaded && isPlanUpgradeRequired && ( + + )} - {isProjectResizing || - isProjectRequestingDiskChanges || - (noPermissions && ( -
- - - -
- ))} - - - - - - {!(isAws || isAwsNimbus) && } - -
- - + {(isProjectResizing || + isProjectRequestingDiskChanges || + (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && ( +
+ + +
+ )} + + + + + + + + + + {!(isAws || isAwsNimbus) && } + + + +
+ + + {isAws && ( + <> +
+ + {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && ( + + + + )} + {isReadOnlyMode && ( + + + + )} +
+ + + + )} +
{isAws && ( <> -
- - {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && ( - - - - )} - {isReadOnlyMode && ( - - - - )} -
- + -
- -
- -
- { - form.setValue('computeSize', 'ci_large') - }} - > - Change to LARGE Compute - - } - /> - - - +
+
+ +
+ +
+ { + form.setValue('computeSize', 'ci_large') + }} + > + Change to LARGE Compute + + ) : ( + + ) + } + /> + + + +
)} -
-
- - {isDirty ? ( - -
+ + + {isDirty ? ( + - - - -
-
- ) : null} -
- -
+ + + +
+ + ) : null} + + +
+ ) } diff --git a/apps/studio/components/interfaces/DiskManagement/ui/NoticeBar.tsx b/apps/studio/components/interfaces/DiskManagement/ui/NoticeBar.tsx index 89ce0071671..7fb1252a2a7 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/NoticeBar.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/NoticeBar.tsx @@ -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, '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 }} > - + {description}
{actions &&
{actions}
} diff --git a/apps/studio/components/interfaces/DiskManagement/ui/SpendCapDisabledSection.tsx b/apps/studio/components/interfaces/DiskManagement/ui/SpendCapDisabledSection.tsx index 58fb36352a6..6388a156ab6 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/SpendCapDisabledSection.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/SpendCapDisabledSection.tsx @@ -27,7 +27,7 @@ export function SpendCapDisabledSection() { exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.2 }} > - + Disable spend cap to manage Disk configuration To access disk management features, you need to disable your spend cap. These features diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx index b3bc7742186..c9f6ab3af98 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx @@ -36,9 +36,11 @@ export const LogDrainsEmpty = () => {

Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform

- - Upgrade plan - +
{ return ( - - -
-
- - Organization Audit Logs are not available on Free or Pro plans - - -

- Upgrade to Team or Enterprise to view up to 62 days of Audit Logs for your - organization. -

-
-
- -
- - Upgrade subscription - -
-
-
+
) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx index 17a8c017113..a2ad298b2a4 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx @@ -157,7 +157,10 @@ export const BillingMetric = ({ ) : (
- + Upgrade
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx index 1f347dbb398..198c988faa2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx @@ -135,7 +135,7 @@ const DowngradeModal = ({ Before you downgrade to the {selectedPlan?.name} plan, consider:
  • - Your projects no longer require their respective add ons. + Your projects no longer require their respective add-ons.
  • Your resource consumption are well within the {selectedPlan?.name} plan's quota. diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 75d8248812c..932eec495a2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -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 = () => { + ) : !canUpdateSubscription ? ( + ) : ( { 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, }, }} > diff --git a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx index 373db6d7bea..7a8d79abacc 100644 --- a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx +++ b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx @@ -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 = () => { {!!plan && !canSetupSSOConfig ? ( - - -
    -
    - - Organization Single Sign-on (SSO) is available from Team plan and above - - -

    - 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. -

    -
    -
    - -
    - -
    -
    -
    + ) : ( <> {isLoadingSSOConfig && ( diff --git a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx index f9dc86a1ff6..d437619a73a 100644 --- a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx +++ b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx @@ -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 = () => { {!isPaidPlan ? ( - - -
    -
    - - Organization MFA enforcement is not available on Free plan - - -

    Upgrade to Pro or above to enforce MFA requirements for your organization.

    -
    -
    - -
    - - Upgrade subscription - -
    -
    -
    + ) : ( <> {isLoadingMfa || isLoadingPermissions ? ( diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx index 792f2c06a41..4e57195e7f1 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx @@ -311,7 +311,11 @@ export const InviteMemberButton = () => { {(currentPlan?.id === 'free' || currentPlan?.id === 'pro') && ( - + )} diff --git a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx index 00b5e33da99..b2168a3c4b0 100644 --- a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx @@ -32,7 +32,10 @@ export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLim projects before you're able to create a free project within this organization.

    - Upgrade plan + } /> diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index 19ecef90236..713454d1ba7 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -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 = () => {
    {isFreePlan ? ( - + ) : ( )} @@ -457,7 +461,11 @@ export const RealtimeSettings = () => {
    {isFreePlan ? ( - + ) : ( )} @@ -515,7 +523,11 @@ export const RealtimeSettings = () => {
    {isFreePlan ? ( - + ) : ( )} diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 768ec5fa6bc..e8510c3e88d 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -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' && (
    {!hasAccessToPitrVariants ? ( - } - > - Upgrade your plan to change PITR for your project - + 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 ? ( - - - Change compute size - - , - ]} - > - This is to ensure that your project has enough resources to execute PITR - successfully - + 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} { ) : !hasCustomDomainAddon ? ( } 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 ? ( diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx index 2d538b3a805..fd74b7ada5c 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx @@ -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.

    - {promptProPlanUpgrade && ( + {promptProPlanUpgrade ? (
    + ) : ( +
    + +
    )} -
    - -
    )} diff --git a/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx b/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx index 623d4faa10c..f43c2c9ef5c 100644 --- a/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx +++ b/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx @@ -29,7 +29,10 @@ export const BucketsUpgradePlan = ({ type }: { type: 'analytics' | 'vector' }) = description={`Upgrade to Pro to use ${type} buckets for your project`} >
    - +
    diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx index a7d7e281af1..698208a3e89 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx @@ -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 && ( )} {isSpendCapOn && ( )} diff --git a/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx new file mode 100644 index 00000000000..ad1f0a74f32 --- /dev/null +++ b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx @@ -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) => { + 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>({ + 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> = async (values) => { + if (!slug) return console.error('Slug is required') + sendUpgradeRequest({ slug, plan, note: values.note }) + } + + return ( + + + + + + +
    + + {titleText} + + Let your organization's billing owners know your interest in this + + + + + + +
    +

    + Your request will be sent to the following emails, who are billing owners of your + organization: +

    +
    +

    + {billingOwners + .slice(0, 2) + .map((x) => x.primary_email) + .join(', ')} +

    + {billingOwners.length > 2 && ( + + + +1 others + + +
      + {billingOwners.slice(2).map((x) => ( +
    • {x.primary_email}
    • + ))} +
    +
    +
    + )} +
    +
    + ( + + + + + + )} + /> +
    + + + + + + +
    +
    +
    + ) +} diff --git a/apps/studio/components/ui/UpgradePlanButton.tsx b/apps/studio/components/ui/UpgradePlanButton.tsx index 976bdb575b5..ca6cc0af6dc 100644 --- a/apps/studio/components/ui/UpgradePlanButton.tsx +++ b/apps/studio/components/ui/UpgradePlanButton.tsx @@ -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 = '' 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) => { + 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 ? ( {linkChildren} ) : ( @@ -46,6 +72,35 @@ export const UpgradePlanButton = ({ ) + if (!canUpdateSubscription) { + return ( + + {children} + + ) + } + + if (projectUpdateDisabled) { + return ( + + {linkChildren} + + ) + } + return (