// @ts-nocheck import { PermissionAction } from '@supabase/shared-types/out/constants' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import { StudioPricingSidePanelOpenedEvent } from 'common/telemetry-constants' import { isArray } from 'lodash' import { Check, ExternalLink } from 'lucide-react' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { plans as subscriptionsPlans } from 'shared-data/plans' import { Button, cn, SidePanel } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import DowngradeModal from './DowngradeModal' import { EnterpriseCard } from './EnterpriseCard' import { ExitSurveyModal } from './ExitSurveyModal' import MembersExceedLimitModal from './MembersExceedLimitModal' import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog' import UpgradeSurveyModal from './UpgradeModal' import { STRIPE_PROJECTS_DOCS_URL } from '@/components/interfaces/Billing/Payment/PaymentMethods/StripePaymentConnection' 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 { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query' import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils' import { useOrganizationBillingSubscriptionPreview } from '@/data/organizations/organization-billing-subscription-preview' import { useOrganizationQuery } from '@/data/organizations/organization-query' import type { CustomerAddress, CustomerTaxId } from '@/data/organizations/types' import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' import { useOrgPlansQuery } from '@/data/subscriptions/org-plans-query' import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' import type { OrgPlan } from '@/data/subscriptions/types' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { MANAGED_BY } from '@/lib/constants/infrastructure' import { formatCurrency } from '@/lib/helpers' import { useOrgSettingsPageStateSnapshot } from '@/state/organization-settings' import { Organization } from '@/types/base' const getPartnerManagedResourceCta = (selectedOrganization: Organization) => { if (selectedOrganization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE) { return { installationId: selectedOrganization?.partner_id, path: '/settings', message: 'Change plan on Vercel Marketplace', } } if (selectedOrganization.managed_by === MANAGED_BY.AWS_MARKETPLACE) { return { organizationSlug: selectedOrganization?.slug, } } } const getStripeProjectsUpgradeCommand = (planId: string | null | undefined) => { const currentTier = planId ?? 'free' const action = currentTier === 'team' ? 'downgrade' : 'upgrade' return `stripe projects ${action} briven/${currentTier}` } export const PlanUpdateSidePanel = () => { const router = useRouter() const { slug } = useParams() const { data: selectedOrganization } = useSelectedOrganizationQuery() const isPartnerBilledOrganization = isPartnerBillingOrganization( selectedOrganization?.billing_partner ) const isStripeManagedOrganization = selectedOrganization?.managed_by === MANAGED_BY.STRIPE_PROJECTS const { mutate: sendEvent } = useSendEventMutation() const originalPlanRef = useRef(undefined) const [showExitSurvey, setShowExitSurvey] = useState(false) const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false) const [showDowngradeError, setShowDowngradeError] = useState(false) const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>() const [latestAddress, setLatestAddress] = useState() const [latestTaxId, setLatestTaxId] = useState() const [useAsDefaultBillingAddress, setUseAsDefaultBillingAddress] = useState(true) const billingAddress = useAsDefaultBillingAddress ? latestAddress : undefined const billingTaxId = useAsDefaultBillingAddress ? latestTaxId : null const debouncedAddress = useDebounce(billingAddress, 1000) const debouncedTaxId = useDebounce(billingTaxId, 1000) const handleAddressChange = useCallback( (address: CustomerAddress) => setLatestAddress(address), [] ) const handleTaxIdChange = useCallback((taxId: CustomerTaxId | null) => setLatestTaxId(taxId), []) const handleUseAsDefaultBillingAddressChange = useCallback( (useAsDefault: boolean) => setUseAsDefaultBillingAddress(useAsDefault), [] ) const { can: canUpdateSubscription } = useAsyncCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) const snap = useOrgSettingsPageStateSnapshot() const visible = snap.panelKey === 'subscriptionPlan' const { data: orgProjectsData } = useOrgProjectsInfiniteQuery({ slug }, { enabled: visible }) const orgProjects = useMemo( () => orgProjectsData?.pages.flatMap((page) => page.projects), [orgProjectsData?.pages] ) || [] const { data } = useOrganizationQuery({ slug }) const hasOrioleProjects = !!data?.has_oriole_project const onClose = () => { const { panel, ...queryWithoutPanel } = router.query router.push({ pathname: router.pathname, query: queryWithoutPanel }, undefined, { shallow: true, }) snap.setPanelKey(undefined) } const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ orgSlug: slug, }) const { data: plans, isPending: isLoadingPlans } = useOrgPlansQuery( { orgSlug: slug }, { enabled: visible } ) const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( { slug }, { enabled: visible } ) const subscriptionPreviewData = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug, address: debouncedAddress, taxId: debouncedTaxId ?? undefined, }) const availablePlans: OrgPlan[] = plans?.plans ?? [] const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 && // [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects // Although I don't foresee this affecting a majority of users. Ideally perhaps we could return // this data from the organization query orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0 useEffect(() => { if (visible) { setSelectedTier(undefined) setLatestAddress(undefined) setLatestTaxId(undefined) setUseAsDefaultBillingAddress(true) const source = Array.isArray(router.query.source) ? router.query.source[0] : router.query.source const properties: StudioPricingSidePanelOpenedEvent['properties'] = { currentPlan: subscription?.plan?.name, } if (source) { properties.origin = source } sendEvent({ action: 'studio_pricing_side_panel_opened', properties, groups: { organization: slug ?? 'Unknown' }, }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]) useEffect(() => { if (visible && isSuccessSubscription && subscription.plan.id) { originalPlanRef.current = subscription.plan.id } }, [visible, isSuccessSubscription, subscription?.plan.id]) const onConfirmDowngrade = () => { setSelectedTier(undefined) if (hasMembersExceedingFreeTierLimit) { setShowDowngradeError(true) } else { setShowExitSurvey(true) } } const planMeta = selectedTier ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) : null const currentPlanMeta = { ...availablePlans.find((p) => p.id === subscription?.plan?.id), features: subscriptionsPlans.find((plan) => plan.id === `tier_${subscription?.plan?.id}`)?.features || [], } const stripeProjectsUpgradeCommand = getStripeProjectsUpgradeCommand( selectedOrganization?.plan?.id ?? subscription?.plan?.id ) return ( <> onClose()} header={

Change subscription plan for {selectedOrganization?.name}

} > {selectedOrganization && (isStripeManagedOrganization ? ( Run {stripeProjectsUpgradeCommand} in your project directory. } cta={{ overrideUrl: `${STRIPE_PROJECTS_DOCS_URL}#upgrade-a-service-tier`, message: 'Stripe Projects docs', }} /> ) : isPartnerBilledOrganization ? ( ) : null)}
{subscriptionsPlans.map((plan) => { const planMeta = availablePlans.find((p) => p.id === plan.id.split('tier_')[1]) const price = planMeta?.price ?? 0 const isDowngradeOption = getPlanChangeType(subscription?.plan.id, plan?.planId) === 'downgrade' const isCurrentPlan = planMeta?.id === subscription?.plan?.id const features = plan.features const footer = plan.footer const source = Array.isArray(router.query.source) ? router.query.source[0] : router.query.source // TODO this panel should allow direct configuration of the highlighting rather than indirectly via the source param const shouldHighlight = source === 'log-drains-empty-state' && plan.id === 'tier_pro' if (plan.id === 'tier_enterprise') { return } return (

{plan.name}

{isCurrentPlan ? (
Current plan
) : plan.nameBadge ? (
{plan.nameBadge}
) : null}
{(price ?? 0) > 0 &&

From

} {isLoadingPlans ? (
) : (

{formatCurrency(price)}

)}

{plan.costUnit}

{isCurrentPlan ? ( ) : !canUpdateSubscription && !isDowngradeOption ? ( ) : ( { setSelectedTier(plan.id as 'tier_free' | 'tier_pro' | 'tier_team') sendEvent({ action: 'studio_pricing_plan_cta_clicked', properties: { selectedPlan: plan.name, currentPlan: subscription?.plan?.name, }, groups: { organization: slug ?? 'Unknown' }, }) }} tooltip={{ content: { side: 'bottom', className: hasOrioleProjects ? 'w-96 text-center' : '', text: !canUpdateSubscription && isDowngradeOption ? "You need additional permissions to change your organization's plan" : subscription?.plan?.id === 'enterprise' || subscription?.plan?.id === 'platform' ? 'Reach out to us via support to update your plan' : hasOrioleProjects ? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.' : selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ? 'You cannot change the plan for an organization managed by AWS Marketplace' : undefined, }, }} > {isDowngradeOption ? 'Downgrade' : 'Upgrade'} to {plan.name} )}
    {features.map((feature) => (
  • {typeof feature === 'string' ? feature : feature[0]}

    {isArray(feature) && (

    {feature[1]}

    )}
  • ))}
{footer && (

{footer}

)}
) })}
setSelectedTier(undefined)} onConfirm={onConfirmDowngrade} projects={orgProjects} /> setSelectedTier(undefined)} planMeta={planMeta} subscriptionPreviewQueryResult={subscriptionPreviewData} projects={orgProjects} currentPlanMeta={currentPlanMeta} onAddressChange={handleAddressChange} onTaxIdChange={handleTaxIdChange} useAsDefaultBillingAddress={useAsDefaultBillingAddress} onUseAsDefaultBillingAddressChange={handleUseAsDefaultBillingAddressChange} /> setShowDowngradeError(false)} /> { setShowExitSurvey(false) if (success) onClose() }} /> { setShowUpgradeSurvey(false) if (success) onClose() }} /> ) }