{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 (
+
+ )
+}
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 (