Repository: PostHog/posthog
Pull request #32624: feat: new platform addons
Author: login: zlwaterfield
Created: 2025-05-23T23:05:53Z | Merged: 2025-05-26T18:08:39Z

> [!IMPORTANT]
> 👉 _Stay up-to-date with [PostHog coding conventions](https://posthog.com/docs/contribute/coding-conventions) for a smoother review._

## Changes

Adding new platform addons. Main changes
- legacy product support
- more consistent plan nameing for free/paid
- new addon support
- some misc clean up

This PR should be shipped first. Then billing then posthog.com

See related PRs
- https://github.com/PostHog/billing/pull/1280
- https://github.com/PostHog/posthog.com/pull/11585

## Did you write or update any docs for this change?

<!-- Engineers are responsible for doing the first pass at documenting their features and/or code.  -->

- [ ] I've added or updated the docs
- [ ] I've reached out for help from the docs team
- [ ] No docs needed for this change

## How did you test this code?

<!-- Briefly describe the steps you took. -->
<!-- Include automated tests if possible, otherwise describe the manual testing routine. -->



Diff:
diff --git a/cypress/fixtures/api/billing/billing-unsubscribed.json b/cypress/fixtures/api/billing/billing-unsubscribed.json
index 9d94cb26ed23..1de97c3c1654 100644
--- a/cypress/fixtures/api/billing/billing-unsubscribed.json
+++ b/cypress/fixtures/api/billing/billing-unsubscribed.json
@@ -2813,7 +2813,7 @@
                 {
                     "plan_key": "free-20230117",
                     "product_key": "platform_and_support",
-                    "name": "Totally free",
+                    "name": "Free",
                     "description": "SSO, permission management, and support.",
                     "image_url": "https://posthog.com/images/product/product-icons/platform.svg",
                     "docs_url": "https://posthog.com/docs",
diff --git a/cypress/fixtures/api/billing/billing.json b/cypress/fixtures/api/billing/billing.json
index efaa32a6bb7b..f699ae81b8c0 100644
--- a/cypress/fixtures/api/billing/billing.json
+++ b/cypress/fixtures/api/billing/billing.json
@@ -2934,7 +2934,7 @@
                 {
                     "plan_key": "free-20230117",
                     "product_key": "platform_and_support",
-                    "name": "Totally free",
+                    "name": "Free",
                     "description": "SSO, permission management, and support.",
                     "image_url": "https://posthog.com/images/product/product-icons/platform.svg",
                     "docs_url": "https://posthog.com/docs",
diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png
index 1e26cf6370ea..0af7a7590398 100644
Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png
index f1126b1d9a8f..aac9b9f0a850 100644
Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png
index 58dd4473d4e0..61651b218428 100644
Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png
index 3fb22e502d8c..44f2168da62e 100644
Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png differ
diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx
index d43d755c00c6..93987ba6502c 100644
--- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx
+++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx
@@ -87,14 +87,14 @@ const SupportResponseTimesTable = ({
         link?: string
     }[] = [
         {
-            name: 'Totally free',
+            name: 'Free',
             current_plan: billing?.subscription_level === 'free' && !hasActiveTrial && !hasEnterprisePlan,
             features: [{ note: 'Community support only' }],
             plan_key: 'free',
             link: 'https://posthog.com/questions',
         },
         {
-            name: 'Ridiculously cheap',
+            name: 'Pay-as-you-go',
             current_plan:
                 billing?.subscription_level === 'paid' && !teamsAddonActive && !hasEnterprisePlan && !hasActiveTrial,
             features: [{ note: '2 business days' }],
@@ -359,8 +359,8 @@ export function SidePanelSupport(): JSX.Element {
                                 <Section title="">
                                     <h3>Can't find what you need in the docs?</h3>
                                     <p>
-                                        With the totally free plan you can ask the community via the link below, or
-                                        explore your upgrade choices for the ability to email a support engineer.
+                                        With the free plan you can ask the community via the link below, or explore your
+                                        upgrade choices for the ability to email a support engineer.
                                     </p>
                                 </Section>
                             )}
diff --git a/frontend/src/mocks/fixtures/_billing.tsx b/frontend/src/mocks/fixtures/_billing.tsx
index b962c1ab2172..4ac7ae80a6ab 100644
--- a/frontend/src/mocks/fixtures/_billing.tsx
+++ b/frontend/src/mocks/fixtures/_billing.tsx
@@ -2635,7 +2635,7 @@ export const billingJson: BillingType = {
                 {
                     plan_key: 'free-20230117',
                     product_key: 'platform_and_support',
-                    name: 'Totally free',
+                    name: 'Free',
                     description: 'SSO, permission management, and support.',
                     image_url: 'https://posthog.com/images/product/product-icons/platform.svg',
                     docs_url: 'https://posthog.com/docs',
diff --git a/frontend/src/mocks/fixtures/_billing_with_billing_limits.json b/frontend/src/mocks/fixtures/_billing_with_billing_limits.json
index 44092d79aee5..b75f6f9a3c9b 100644
--- a/frontend/src/mocks/fixtures/_billing_with_billing_limits.json
+++ b/frontend/src/mocks/fixtures/_billing_with_billing_limits.json
@@ -3338,7 +3338,7 @@
                 {
                     "plan_key": "free-20230117",
                     "product_key": "platform_and_support",
-                    "name": "Totally free",
+                    "name": "Free",
                     "description": "SSO, permission management, and support.",
                     "image_url": "https://posthog.com/images/product/product-icons/platform.svg",
                     "docs_url": "https://posthog.com/docs",
diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx
index b5f1d8bdbc37..1a1615a2926e 100644
--- a/frontend/src/scenes/billing/BillingHero.tsx
+++ b/frontend/src/scenes/billing/BillingHero.tsx
@@ -22,7 +22,9 @@ import { PlanComparisonModal } from './PlanComparison'
 const PLAN_BADGES: Record<BillingPlan, string> = {
     [BillingPlan.Free]: planFree,
     [BillingPlan.Paid]: planPaid,
-    [BillingPlan.Teams]: planTeams,
+    [BillingPlan.Teams]: planTeams, // Legacy
+    [BillingPlan.Boost]: planTeams, // TODO: Add Boost badge
+    [BillingPlan.Scale]: planTeams, // TODO: Add Scale badge
     [BillingPlan.Enterprise]: planEnterprise,
 }
 
@@ -53,27 +55,49 @@ const BADGE_CONFIG: Record<BillingPlan | StartupProgramLabel, CopyVariation> = {
     },
     [BillingPlan.Paid]: {
         title: 'Good call!',
-        subtitle: "You're on the Ridiculously Cheap™ plan.",
+        subtitle: "You're on the Pay-as-you-go plan.",
         backgroundColor: 'bg-warning-highlight',
         getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
             <p>
-                If you're growing like crazy, you might want to check out the{' '}
+                If you're growing like crazy, you might want to check out our{' '}
                 {scrollToProduct ? (
                     <>
-                        <Link onClick={() => scrollToProduct('teams')}>Teams</Link>
-                        {' or '}
-                        <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link>
+                        <Link onClick={() => scrollToProduct('platform_and_support')}>Platform add-ons</Link>
                     </>
                 ) : (
-                    'Teams or Enterprise'
-                )}{' '}
-                plan.
+                    'Platform add-ons'
+                )}
+                .
             </p>
         ),
     },
     [BillingPlan.Teams]: {
         title: 'Good call!',
-        subtitle: "You're on the Teams plan.",
+        subtitle: "You're on the Pay-as-you-go plan (with Teams add-on).",
+        backgroundColor: 'bg-warning-highlight',
+        getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
+            <p>
+                If you're growing like crazy, you might want to check out the{' '}
+                {scrollToProduct ? <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link> : 'Enterprise'}{' '}
+                plan.
+            </p>
+        ),
+    },
+    [BillingPlan.Boost]: {
+        title: 'Good call!',
+        subtitle: "You're on the Pay-as-you-go plan (with Boost add-on).",
+        backgroundColor: 'bg-warning-highlight',
+        getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
+            <p>
+                If you're growing like crazy, you might want to check out the{' '}
+                {scrollToProduct ? <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link> : 'Enterprise'}{' '}
+                plan.
+            </p>
+        ),
+    },
+    [BillingPlan.Scale]: {
+        title: 'Good call!',
+        subtitle: "You're on the Pay-as-you-go plan (with Scale add-on).",
         backgroundColor: 'bg-warning-highlight',
         getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
             <p>
@@ -93,23 +117,15 @@ const BADGE_CONFIG: Record<BillingPlan | StartupProgramLabel, CopyVariation> = {
         title: 'Good for you!',
         subtitle: "You're on the startup plan.",
         backgroundColor: 'bg-warning-highlight',
-        getDescription: (billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
+        getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
             <p>
-                If you're growing like crazy, you might want to check out the{' '}
-                {billingPlan !== BillingPlan.Teams ? (
-                    <>
-                        {scrollToProduct ? (
-                            <>
-                                <Link onClick={() => scrollToProduct('teams')}>Teams</Link>
-                                {' or '}
-                            </>
-                        ) : (
-                            'Teams or '
-                        )}
-                    </>
-                ) : null}
-                {scrollToProduct ? <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link> : 'Enterprise'}{' '}
-                plan.
+                If you're growing like crazy, you might want to check out our{' '}
+                {scrollToProduct ? (
+                    <Link onClick={() => scrollToProduct('platform_and_support')}>Platform add-ons</Link>
+                ) : (
+                    'Platform add-ons'
+                )}
+                .
             </p>
         ),
     },
@@ -117,32 +133,20 @@ const BADGE_CONFIG: Record<BillingPlan | StartupProgramLabel, CopyVariation> = {
         title: 'Lucky you!',
         subtitle: "You're on the YC plan.",
         backgroundColor: 'bg-warning-highlight',
-        getDescription: (billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
+        getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
             <>
                 <p>
                     Enjoy your founder merch, and don't forget to say hello in the{' '}
                     <Link to="https://posthog.slack.com/archives/C04J1TJ11UZ">Founders Club!</Link>
                 </p>
                 <p>
-                    If you're growing like crazy, you might want to check out the{' '}
-                    {billingPlan !== BillingPlan.Teams ? (
-                        <>
-                            {scrollToProduct ? (
-                                <>
-                                    <Link onClick={() => scrollToProduct('teams')}>Teams</Link>
-                                    {' or '}
-                                </>
-                            ) : (
-                                'Teams or '
-                            )}
-                        </>
-                    ) : null}
+                    If you're growing like crazy, you might want to check out our{' '}
                     {scrollToProduct ? (
-                        <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link>
+                        <Link onClick={() => scrollToProduct('platform_and_support')}>Platform add-ons</Link>
                     ) : (
-                        'Enterprise'
-                    )}{' '}
-                    plan.
+                        'Platform add-ons'
+                    )}
+                    .
                 </p>
             </>
         ),
diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx
index 633c883ffca6..c0ce9f710628 100644
--- a/frontend/src/scenes/billing/BillingProduct.tsx
+++ b/frontend/src/scenes/billing/BillingProduct.tsx
@@ -54,6 +54,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
         surveyID,
         billingProductLoading,
         isSessionReplayWithAddons,
+        visibleAddons,
     } = useValues(billingProductLogic({ product }))
     const {
         setShowTierBreakdown,
@@ -100,7 +101,10 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
             <div className="border border-primary rounded w-full bg-surface-primary" ref={productRef}>
                 <div className="border-b border-primary rounded-t p-4">
                     <div className="flex gap-4 items-center justify-between">
+                        {/* Product icon */}
                         {getProductIcon(product.name, product.icon_key, 'text-2xl')}
+
+                        {/* Product name and description */}
                         <div>
                             <h3 className="font-bold mb-0 flex items-center gap-x-2">
                                 {product.name}{' '}
@@ -110,6 +114,8 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                             </h3>
                             <div>{product.description}</div>
                         </div>
+
+                        {/* Product actions */}
                         <div className="flex grow justify-end gap-x-2 items-center">
                             {product.docs_url && (
                                 <LemonButton
@@ -170,12 +176,15 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                     </div>
                 </div>
                 <div className="px-8 pb-8 sm:pb-0">
+                    {/* Exceeded limit notice */}
                     {product.percentage_usage > 1 && (
                         <LemonBanner className="mt-6" type="error">
                             You have exceeded the {hasCustomLimitSet ? 'billing limit' : 'free tier limit'} for this
                             product.
                         </LemonBanner>
                     )}
+
+                    {/* Usage and projected usage */}
                     <div className="sm:flex w-full items-center gap-x-8">
                         {product.contact_support && (!product.subscribed || isUnlicensedDebug) ? (
                             <div className="py-8">
@@ -329,15 +338,36 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                             )
                         )}
                     </div>
+
                     {product.price_description ? (
                         <LemonBanner type="info">
                             <span dangerouslySetInnerHTML={{ __html: product.price_description }} />
                         </LemonBanner>
                     ) : null}
+
                     {/* Table with tiers */}
                     {showTierBreakdown && <BillingProductPricingTable product={product} />}
+
+                    {/* Add-ons */}
                     {product.addons?.length > 0 && (
                         <div className="pb-8">
+                            {/* Legacy teams addon */}
+                            {product.type === 'platform_and_support' &&
+                                product.addons.find((addon) => addon.legacy_product && addon.subscribed) && (
+                                    <LemonBanner type="warning" className="my-4" hideIcon>
+                                        <p>
+                                            You're currently subscribed to our legacy{' '}
+                                            {
+                                                product.addons.find((addon) => addon.legacy_product && addon.subscribed)
+                                                    ?.name
+                                            }{' '}
+                                            add-on. If you'd like to move to one of our new add-ons please subscribe
+                                            below.
+                                        </p>
+                                    </LemonBanner>
+                                )}
+
+                            {/* Add-ons title */}
                             <h4 className="my-4">Add-ons</h4>
                             {billing?.subscription_level == 'free' && (
                                 <LemonBanner type="warning" className="text-sm mb-4" hideIcon>
@@ -376,28 +406,18 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                                 </LemonBanner>
                             )}
                             <div className="gap-y-4 flex flex-col">
-                                {product.addons
-                                    // TODO: enhanced_persons: remove this filter
-                                    .filter((addon) => {
-                                        if (addon.inclusion_only) {
-                                            if (featureFlags[FEATURE_FLAGS.PERSONLESS_EVENTS_NOT_SUPPORTED]) {
-                                                return false
-                                            }
-                                        }
-                                        return true
-                                    })
-                                    .filter((addon) => {
-                                        const hideAddonFlag = `billing_hide_addon_${addon.type}`
-                                        return featureFlags[hideAddonFlag] !== true
-                                    })
-                                    .map((addon, i) => {
-                                        return <BillingProductAddon key={i} addon={addon} />
-                                    })}
+                                {visibleAddons.map((addon: BillingProductV2AddonType, i: number) => {
+                                    return <BillingProductAddon key={i} addon={addon} />
+                                })}
                             </div>
                         </div>
                     )}
                 </div>
+
+                {/* Billing limit */}
                 {!isTemporaryFreeProduct && <BillingLimit product={product} />}
+
+                {/* Feature flag usage notice */}
                 <FeatureFlagUsageNotice product={product} />
             </div>
             <ProductPricingModal
diff --git a/frontend/src/scenes/billing/BillingProductAddon.tsx b/frontend/src/scenes/billing/BillingProductAddon.tsx
index db19b5236823..de743763f1d7 100644
--- a/frontend/src/scenes/billing/BillingProductAddon.tsx
+++ b/frontend/src/scenes/billing/BillingProductAddon.tsx
@@ -1,8 +1,7 @@
-import { IconCheckCircle, IconChevronDown, IconChevronRight } from '@posthog/icons'
-import { LemonButton, LemonModal, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posthog/lemon-ui'
+import { IconCheckCircle, IconChevronDown, IconChevronRight, IconInfo } from '@posthog/icons'
+import { LemonButton, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posthog/lemon-ui'
 import clsx from 'clsx'
 import { useActions, useValues } from 'kea'
-import { supportLogic } from 'lib/components/Support/supportLogic'
 import { capitalizeFirstLetter, humanFriendlyCurrency } from 'lib/utils'
 import { ReactNode, useRef } from 'react'
 import { getProductIcon } from 'scenes/products/Products'
@@ -34,17 +33,13 @@ export const formatFlatRate = (flatRate: number, unit: string | null): string |
 export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonType }): JSX.Element => {
     const productRef = useRef<HTMLDivElement | null>(null)
     const { billing } = useValues(billingLogic)
-    const { isPricingModalOpen, currentAndUpgradePlans, surveyID, trialModalOpen, trialLoading, showTierBreakdown } =
-        useValues(billingProductLogic({ product: addon, productRef }))
-    const { toggleIsPricingModalOpen, setTrialModalOpen, activateTrial, setShowTierBreakdown } = useActions(
-        billingProductLogic({ product: addon })
+    const { isPricingModalOpen, currentAndUpgradePlans, surveyID, showTierBreakdown } = useValues(
+        billingProductLogic({ product: addon, productRef })
     )
-    const { openSupportForm } = useActions(supportLogic)
+    const { toggleIsPricingModalOpen, setShowTierBreakdown } = useActions(billingProductLogic({ product: addon }))
     const logic = billingProductAddonLogic({ addon })
     const { gaugeItems } = useValues(logic)
 
-    const upgradePlan = currentAndUpgradePlans?.upgradePlan
-
     const productType = { plural: `${addon.unit}s`, singular: addon.unit }
     const tierDisplayOptions: LemonSelectOptions<string> = [
         { label: `Per ${productType.singular}`, value: 'individual' },
@@ -98,6 +93,13 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
                                     </div>
                                 )
                             )}
+                            {addon.legacy_product && (
+                                <div>
+                                    <LemonTag type="highlight" icon={<IconInfo />}>
+                                        Legacy add-on
+                                    </LemonTag>
+                                </div>
+                            )}
                         </div>
                         <p className="ml-0 mb-0">
                             {addon.description}{' '}
@@ -204,60 +206,6 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
 
             {/* Unsubscribe survey modal */}
             {surveyID && <UnsubscribeSurveyModal product={addon} />}
-
-            {/* Trial modal */}
-            {/* Not currently used but keeping around incase we need it again */}
-            <LemonModal
-                isOpen={trialModalOpen}
-                onClose={() => setTrialModalOpen(false)}
-                title={`Start your ${addon.name} trial`}
-                description={`You'll have ${addon.trial?.length} days to try it out before being charged.`}
-                footer={
-                    <>
-                        <LemonButton type="secondary" onClick={() => setTrialModalOpen(false)}>
-                            Cancel
-                        </LemonButton>
-                        <LemonButton type="primary" onClick={activateTrial} loading={trialLoading}>
-                            Start trial
-                        </LemonButton>
-                    </>
-                }
-            >
-                <p className="mb-1.5">Here's some stuff about the trial:</p>
-                <ul className="flex flex-col gap-0.5">
-                    <li className="ml-2">
-                        🎉 It's <b>free!</b>
-                    </li>
-                    <li className="ml-2">
-                        📅 The trial is for <b>{addon.trial?.length} days</b>
-                    </li>
-                    <li className="ml-2">
-                        🚀 You'll get access to <b>all the features</b> of the plan immediately
-                    </li>
-                    <li className="ml-2">
-                        📧 3 days before the trial ends, you'll be emailed a reminder that you'll be charged
-                    </li>
-                    <li className="ml-2">
-                        🚫 If you don't want to be charged, you can cancel anytime before the trial ends
-                    </li>
-                    <li className="ml-2">
-                        💵 At the end of the trial, you'll be be subscribed and charged{' '}
-                        {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)}
-                    </li>
-                    <li className="ml-2">
-                        ☎️ If you have any questions, you can{' '}
-                        <Link
-                            onClick={() => {
-                                setTrialModalOpen(false)
-                                openSupportForm({ kind: 'support', target_area: 'billing' })
-                            }}
-                            className="cursor-pointer"
-                        >
-                            contact us
-                        </Link>
-                    </li>
-                </ul>
-            </LemonModal>
         </div>
     )
 }
diff --git a/frontend/src/scenes/billing/BillingProductAddonActions.tsx b/frontend/src/scenes/billing/BillingProductAddonActions.tsx
index f6e8f1119eae..0e6f01b37973 100644
--- a/frontend/src/scenes/billing/BillingProductAddonActions.tsx
+++ b/frontend/src/scenes/billing/BillingProductAddonActions.tsx
@@ -21,7 +21,7 @@ interface BillingProductAddonActionsProps {
 
 export const BillingProductAddonActions = ({ addon, productRef }: BillingProductAddonActionsProps): JSX.Element => {
     const { billing, billingError, timeTotalInSeconds, timeRemainingInSeconds } = useValues(billingLogic)
-    const { currentAndUpgradePlans, billingProductLoading, trialLoading } = useValues(
+    const { currentAndUpgradePlans, billingProductLoading, trialLoading, isSubscribedToAnotherAddon } = useValues(
         billingProductLogic({ product: addon, productRef })
     )
 
@@ -121,7 +121,7 @@ export const BillingProductAddonActions = ({ addon, productRef }: BillingProduct
                             (billingError && billingError.message) ||
                             (billing?.subscription_level === 'free' && 'Upgrade to add add-ons')
                         }
-                        loading={billingProductLoading === addon.type}
+                        loading={billingProductLoading === addon.type || trialLoading}
                         onClick={
                             isTrialEligible
                                 ? () => activateTrial()
@@ -152,7 +152,7 @@ export const BillingProductAddonActions = ({ addon, productRef }: BillingProduct
             return null
         }
 
-        if (isTrialEligible) {
+        if (isTrialEligible && !isSubscribedToAnotherAddon) {
             return (
                 <p className="mt-2 text-xs text-secondary text-right">
                     You'll have {addon.trial?.length} days to try it out. Then you'll be charged{' '}
@@ -161,7 +161,7 @@ export const BillingProductAddonActions = ({ addon, productRef }: BillingProduct
             )
         }
 
-        if (isProrated) {
+        if (isProrated && !isSubscribedToAnotherAddon) {
             return (
                 <p className="mt-2 text-xs text-secondary text-right">
                     Pay ~${prorationAmount} today (prorated) and
@@ -192,8 +192,9 @@ export const BillingProductAddonActions = ({ addon, productRef }: BillingProduct
                 Contact support
             </LemonButton>
         )
-    } else if (!billing?.trial) {
+    } else if (!billing?.trial && !isSubscribedToAnotherAddon) {
         // Customer is not subscribed to any trial
+        // We don't allow multiple add-ons to be subscribed to at the same time so this checks if the customer is subscribed to another add-on
         // TODO: add support for when a customer has a Paid Plan trial
         content = renderPurchaseActions()
     }
diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts
index 0f480990423f..d0e565c042ae 100644
--- a/frontend/src/scenes/billing/billingProductLogic.ts
+++ b/frontend/src/scenes/billing/billingProductLogic.ts
@@ -2,6 +2,7 @@ import { LemonDialog, lemonToast } from '@posthog/lemon-ui'
 import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea'
 import { forms } from 'kea-forms'
 import api from 'lib/api'
+import { FEATURE_FLAGS } from 'lib/constants'
 import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
 import posthog from 'posthog-js'
 import React from 'react'
@@ -11,6 +12,7 @@ import {
     BillingProductV2AddonType,
     BillingProductV2Type,
     BillingTierType,
+    BillingType,
     SurveyEventName,
 } from '~/types'
 
@@ -109,7 +111,6 @@ export const billingProductLogic = kea<billingProductLogicType>([
         }),
         activateTrial: true,
         cancelTrial: true,
-        setTrialModalOpen: (isOpen: boolean) => ({ isOpen }),
         setTrialLoading: (loading: boolean) => ({ loading }),
         setUnsubscribeModalStep: (step: number) => ({ step }),
         resetUnsubscribeModalStep: true,
@@ -193,12 +194,6 @@ export const billingProductLogic = kea<billingProductLogicType>([
                 toggleIsPlanComparisonModalOpen: (_, { highlightedFeatureKey }) => highlightedFeatureKey || null,
             },
         ],
-        trialModalOpen: [
-            false,
-            {
-                setTrialModalOpen: (_, { isOpen }) => isOpen,
-            },
-        ],
         trialLoading: [
             false,
             {
@@ -220,6 +215,32 @@ export const billingProductLogic = kea<billingProductLogicType>([
         ],
     }),
     selectors(({ values }) => ({
+        isSubscribedToAnotherAddon: [
+            (s, p) => [s.billing, p.product],
+            (billing: BillingType, addon: BillingProductV2AddonType) => {
+                const subscribed = addon.subscribed
+                if (subscribed) {
+                    // They are subscribed to this addon so can't be subscribed to another one
+                    return false
+                }
+
+                const parentProduct = billing?.products.find((product: any) =>
+                    product.addons.find((a: BillingProductV2AddonType) => a.type === addon.type)
+                )
+                if (!parentProduct) {
+                    return false
+                }
+
+                if (parentProduct?.type !== 'platform_and_support') {
+                    // Only platform and support can have multiple add-ons
+                    return false
+                }
+
+                // Check if they are subscribed to another add-on that is not a legacy add-on
+                // This is because if they are on a legacy add-on, we want them to be able to move to a new add-on.
+                return parentProduct.addons.some((a: BillingProductV2AddonType) => a.subscribed && !a.legacy_product)
+            },
+        ],
         customLimitUsd: [
             (s, p) => [s.billing, p.product],
             (billing, product) => {
@@ -230,6 +251,34 @@ export const billingProductLogic = kea<billingProductLogicType>([
                 return product.usage_key ? billing?.custom_limits_usd?.[product.usage_key] ?? null : null
             },
         ],
+        visibleAddons: [
+            (s, p) => [s.featureFlags, p.product],
+            (featureFlags: Record<string, any>, product: BillingProductV2Type) => {
+                if (!product.addons?.length) {
+                    return []
+                }
+
+                return product.addons.filter((addon: BillingProductV2AddonType) => {
+                    // Filter out inclusion-only addons if personless events are not supported
+                    if (addon.inclusion_only && featureFlags[FEATURE_FLAGS.PERSONLESS_EVENTS_NOT_SUPPORTED]) {
+                        return false
+                    }
+
+                    // Filter out legacy addons for platform_and_support if not subscribed
+                    if (product.type === 'platform_and_support' && addon.legacy_product && !addon.subscribed) {
+                        return false
+                    }
+
+                    // Filter out addons that are hidden by feature flag
+                    const hideAddonFlag = `billing_hide_addon_${addon.type}`
+                    if (featureFlags[hideAddonFlag]) {
+                        return false
+                    }
+
+                    return true
+                })
+            },
+        ],
         hasCustomLimitSet: [
             (s) => [s.customLimitUsd],
             (customLimitUsd) => (!!customLimitUsd || customLimitUsd === 0) && customLimitUsd >= 0,
@@ -407,13 +456,12 @@ export const billingProductLogic = kea<billingProductLogicType>([
                     target: props.product.type,
                 })
                 lemonToast.success('Your trial has been activated!')
-            } catch (e) {
-                lemonToast.error('There was an error activating your trial. Please try again or contact support.')
-            } finally {
                 await breakpoint(400)
                 window.location.reload()
+            } catch (e) {
+                lemonToast.error('There was an error activating your trial. Please try again or contact support.')
                 actions.setTrialLoading(false)
-                actions.setTrialModalOpen(false)
+                actions.loadBilling()
             }
         },
         cancelTrial: async () => {
@@ -421,13 +469,11 @@ export const billingProductLogic = kea<billingProductLogicType>([
             try {
                 await api.create(`api/billing/trials/cancel`)
                 lemonToast.success('Your trial has been cancelled!')
+                window.location.reload()
             } catch (e) {
-                console.error(e)
                 lemonToast.error('There was an error cancelling your trial. Please try again or contact support.')
-            } finally {
-                actions.loadBilling()
-                window.location.reload()
                 actions.setTrialLoading(false)
+                actions.loadBilling()
             }
         },
         triggerMoreHedgehogs: async (_, breakpoint) => {
diff --git a/frontend/src/scenes/onboarding/billing/PlanCards.tsx b/frontend/src/scenes/onboarding/billing/PlanCards.tsx
index 0117ee13eb7e..59bd8a8dc70e 100644
--- a/frontend/src/scenes/onboarding/billing/PlanCards.tsx
+++ b/frontend/src/scenes/onboarding/billing/PlanCards.tsx
@@ -197,7 +197,7 @@ export const PlanCard: React.FC<PlanCardProps> = ({ planData, product, highlight
 
 const PLANS_DATA: PlanData[] = [
     {
-        title: 'Totally free',
+        title: 'Free',
         plan: Plan.TOTALLY_FREE,
         billingPlanKeyPrefix: 'free',
         subtitle: 'No credit card required',
@@ -212,7 +212,7 @@ const PLANS_DATA: PlanData[] = [
         ctaAction: 'next',
     },
     {
-        title: 'Ridiculously cheap',
+        title: 'Pay-as-you-go',
         plan: Plan.RIDICULOUSLY_CHEAP,
         billingPlanKeyPrefix: 'paid',
         subtitle: 'Usage-based pricing after free tier',
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 99da64dc76da..c683dd305d96 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -226,7 +226,9 @@ export enum LicensePlan {
 export enum BillingPlan {
     Free = 'free',
     Paid = 'paid',
-    Teams = 'teams',
+    Teams = 'teams', // Legacy
+    Boost = 'boost',
+    Scale = 'scale',
     Enterprise = 'enterprise',
 }
 
@@ -1806,6 +1808,7 @@ export interface BillingProductV2Type {
     // addons-only: if this addon is included with the base product and not subscribed individually. for backwards compatibility.
     included_with_main_product?: boolean
     trial?: BillingTrialType
+    legacy_product?: boolean | null
 }
 
 export interface BillingProductV2AddonType {
@@ -1837,6 +1840,7 @@ export interface BillingProductV2AddonType {
     included_if?: 'no_active_subscription' | 'has_subscription' | null
     usage_limit?: number | null
     trial?: BillingTrialType
+    legacy_product?: boolean | null
 }
 export interface BillingType {
     customer_id: string
diff --git a/playwright/mocks/billing/billing-unsubscribed.json b/playwright/mocks/billing/billing-unsubscribed.json
index ed693c7ce694..68f2d83dbd31 100644
--- a/playwright/mocks/billing/billing-unsubscribed.json
+++ b/playwright/mocks/billing/billing-unsubscribed.json
@@ -2813,7 +2813,7 @@
                 {
                     "plan_key": "free-20230117",
                     "product_key": "platform_and_support",
-                    "name": "Totally free",
+                    "name": "Free",
                     "description": "SSO, permission management, and support.",
                     "image_url": "https://posthog.com/images/product/product-icons/platform.svg",
                     "docs_url": "https://posthog.com/docs",
diff --git a/playwright/mocks/billing/billing.json b/playwright/mocks/billing/billing.json
index 23132128c92b..c2d0ea9f2eb2 100644
--- a/playwright/mocks/billing/billing.json
+++ b/playwright/mocks/billing/billing.json
@@ -2934,7 +2934,7 @@
                 {
                     "plan_key": "free-20230117",
                     "product_key": "platform_and_support",
-                    "name": "Totally free",
+                    "name": "Free",
                     "description": "SSO, permission management, and support.",
                     "image_url": "https://posthog.com/images/product/product-icons/platform.svg",
                     "docs_url": "https://posthog.com/docs",


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T23:08:57Z
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR introduces platform addons and billing plan updates across PostHog's frontend, focusing on standardizing plan naming and supporting legacy products.

- Renamed plans from 'Totally free' to 'Free' and 'Ridiculously cheap' to 'Pay-as-you-go' across all billing components and fixtures
- Added new platform addons including Teams with features like priority support, unlimited projects, and SSO enforcement
- Added `legacy_product` flag in types and special handling for legacy team add-ons with warning banners
- Added `isSubscribedToAnotherAddon` check to prevent multiple addon subscriptions
- Introduced new Boost and Scale billing plans while maintaining legacy Teams plan support

<sub>14 file(s) reviewed, 3 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_32624)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T23:10:40Z
user: login: pawel-cebula
body: Looks great! Very happy to see "Pay-as-you-go" instead of "Ridiculously cheap".

Left a couple of minor suggestions and have one overall feedback on the billing page UI/UX. Neither is a blocker for merging though.

Currently, it's not clear from the UI that the platform add-ons are incremental and all the features from the previous ones are included. We could e.g.
- State this more clearly on each card ("Everything included in Scale plus: ...")
- If you are subscribed to a platform add-on, make the other add-on cards a bit less opaque, lighter grey, kind of like a disabled button
- Indicate progression between different add-on tiers visually
  - Right now they have the same icon - maybe use a different one and show it in different quantity depending on the add-on plan? Maybe like army pins - 1/2/3/4 star pin/badge
  - Instead of full width boxes, use a 3-4 columns view (probably tight, esp. with 4), like on pricing pages
  - Probably too weird (and probably fits better for posthog.com pricing pages, if at all) but maybe we could get a hedgehog version of the [wrestling guy reaction meme](https://imgflip.com/memegenerator/127634487/Mr-McMahon-reaction)

Might be a good follow up task for @yasen-posthog to experiment with!
 
![image](https://github.com/user-attachments/assets/5d542c9e-6ad2-4c25-bc8f-3076e72150d0)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T14:41:42Z
user: login: pawel-cebula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T14:43:10Z

Review comments:
diff_hunk: @@ -329,15 +337,36 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                             )
                         )}
                     </div>
+
                     {product.price_description ? (
                         <LemonBanner type="info">
                             <span dangerouslySetInnerHTML={{ __html: product.price_description }} />
                         </LemonBanner>
                     ) : null}
+
                     {/* Table with tiers */}
                     {showTierBreakdown && <BillingProductPricingTable product={product} />}
+
+                    {/* Add-ons */}
                     {product.addons?.length > 0 && (
                         <div className="pb-8">
+                            {/* Legacy teams addon */}
+                            {product.type === 'platform_and_support' &&
+                                product.addons.find((addon) => addon.legacy_product && addon.subscribed) && (
+                                    <LemonBanner type="warning" className="my-4" hideIcon>
+                                        <p>
+                                            You're currently subscribed to our legacy{' '}
+                                            {
+                                                product.addons.find((addon) => addon.legacy_product && addon.subscribed)
+                                                    ?.name
+                                            }{' '}
path: frontend/src/scenes/billing/BillingProduct.tsx
user: login: greptile-apps[bot]
body: style: Multiple find() calls on the same condition. Consider storing the result in a variable
created_at: 2025-05-23T23:08:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,14 +415,28 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                                         }
                                         return true
                                     })
+                                    .filter((addon) => {
+                                        if (
+                                            product.type === 'platform_and_support' &&
+                                            addon.legacy_product &&
+                                            !addon.subscribed
+                                        ) {
+                                            return false
+                                        }
+                                        return true
+                                    })
                                     .map((addon, i) => {
path: frontend/src/scenes/billing/BillingProduct.tsx
user: login: greptile-apps[bot]
body: style: Using array index as React key may cause issues with list rendering if items are reordered
created_at: 2025-05-23T23:08:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -407,27 +427,24 @@ export const billingProductLogic = kea<billingProductLogicType>([
                     target: props.product.type,
                 })
                 lemonToast.success('Your trial has been activated!')
-            } catch (e) {
-                lemonToast.error('There was an error activating your trial. Please try again or contact support.')
-            } finally {
                 await breakpoint(400)
                 window.location.reload()
+            } catch (e) {
+                lemonToast.error('There was an error activating your trial. Please try again or contact support.')
                 actions.setTrialLoading(false)
-                actions.setTrialModalOpen(false)
+                actions.loadBilling()
path: frontend/src/scenes/billing/billingProductLogic.ts
user: login: graphite-app[bot]
body: The `window.location.reload()` call is placed before the error handling block, which means if an error occurs during trial activation, the page will reload before the error handling code executes. This prevents the error toast from displaying and proper cleanup from occurring.

Consider restructuring this to ensure error handling works correctly:

```javascript
try {
    await api.create('api/billing/trials/activate', {
        target: props.product.type,
    })
    lemonToast.success('Your trial has been activated!')
    await breakpoint(400)
    window.location.reload() // Only reload on success
} catch (e) {
    lemonToast.error('There was an error activating your trial. Please try again or contact support.')
    actions.setTrialLoading(false)
    actions.loadBilling()
}
```

This ensures the page only reloads on successful trial activation, while error cases are properly handled.
```suggestion
            try {
                await api.create('api/billing/trials/activate', {
                    target: props.product.type,
                })
                lemonToast.success('Your trial has been activated!')
                await breakpoint(400)
                window.location.reload()
            } catch (e) {
                lemonToast.error('There was an error activating your trial. Please try again or contact support.')
                actions.setTrialLoading(false)
                actions.loadBilling()
            }
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=PostHog&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-23T23:08:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,6 +214,32 @@ export const billingProductLogic = kea<billingProductLogicType>([
         ],
     }),
     selectors(({ values }) => ({
+        isSubscribedToAnotherAddon: [
+            (s, p) => [s.billing, p.product],
+            (billing: BillingType, addon: BillingProductV2AddonType) => {
+                const subscribed = addon.subscribed
+                if (subscribed) {
+                    // They are subscribed to this addon so can't be subscribed to another one
+                    return false
+                }
+
+                const parentProduct = billing.products.find((product: any) =>
+                    product.addons.find((a: BillingProductV2AddonType) => a.type === addon.type)
+                )
path: frontend/src/scenes/billing/billingProductLogic.ts
user: login: greptile-apps[bot]
body: style: Type `any` used for product. Should explicitly type this as `BillingProductV2Type` to ensure type safety

```suggestion
                const parentProduct = billing.products.find((product: BillingProductV2Type) =>
                    product.addons?.find((a: BillingProductV2AddonType) => a.type === addon.type)
                )
```
created_at: 2025-05-23T23:09:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,56 +117,36 @@ const BADGE_CONFIG: Record<BillingPlan | StartupProgramLabel, CopyVariation> = {
         title: 'Good for you!',
         subtitle: "You're on the startup plan.",
         backgroundColor: 'bg-warning-highlight',
-        getDescription: (billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
+        getDescription: (_billingPlan: BillingPlan, scrollToProduct: (productType: string) => void) => (
             <p>
-                If you're growing like crazy, you might want to check out the{' '}
-                {billingPlan !== BillingPlan.Teams ? (
-                    <>
-                        {scrollToProduct ? (
-                            <>
-                                <Link onClick={() => scrollToProduct('teams')}>Teams</Link>
-                                {' or '}
-                            </>
-                        ) : (
-                            'Teams or '
-                        )}
-                    </>
-                ) : null}
-                {scrollToProduct ? <Link onClick={() => scrollToProduct('enterprise')}>Enterprise</Link> : 'Enterprise'}{' '}
-                plan.
+                If you're growing like crazy, you might want to check out our{' '}
+                {scrollToProduct ? (
+                    <Link onClick={() => scrollToProduct('platform_and_support')}>Platform add-ons</Link>
+                ) : (
+                    'Platform add-ons'
+                )}
+                .
path: frontend/src/scenes/billing/BillingHero.tsx
user: login: pawel-cebula
body: Should we still check if they already have the platform add-on to avoid showing it,at least if they're already on Enterprise? Small edge case though.
created_at: 2025-05-26T13:57:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,14 +415,28 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
                                         }
                                         return true
                                     })
+                                    .filter((addon) => {
+                                        if (
+                                            product.type === 'platform_and_support' &&
+                                            addon.legacy_product &&
+                                            !addon.subscribed
+                                        ) {
+                                            return false
+                                        }
+                                        return true
+                                    })
path: frontend/src/scenes/billing/BillingProduct.tsx
user: login: pawel-cebula
body: nit: move the `if` to the existing `filter` to avoid looping twice

I'm also a fan of having things like this in the logic, via something like e.g. `visibleAddons` selector - makes reading these components easier
created_at: 2025-05-26T14:01:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,7 +22,9 @@ import { PlanComparisonModal } from './PlanComparison'
 const PLAN_BADGES: Record<BillingPlan, string> = {
     [BillingPlan.Free]: planFree,
     [BillingPlan.Paid]: planPaid,
-    [BillingPlan.Teams]: planTeams,
+    [BillingPlan.Teams]: planTeams, // Legacy
+    [BillingPlan.Boost]: planTeams, // TODO: Add Boost badge
+    [BillingPlan.Scale]: planTeams, // TODO: Add Scale badge
path: frontend/src/scenes/billing/BillingHero.tsx
user: login: pawel-cebula
body: Good to request this art work asap as it might take a while
created_at: 2025-05-26T14:43:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: posthog-bot
created_at: 2025-05-23T23:17:37Z
body: ## 📸 UI snapshots have been updated

**4** snapshot changes in total. **0** added, **4** modified, **0** deleted:

- **`chromium`**: **0** added, **4** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/32624/commits/74f04bd9d5cd9a4a9ac323c441e2912daa709878))
- `webkit`: **0** added, **0** modified, **0** deleted

_Triggered by [this commit](https://github.com/PostHog/posthog/pull/32624/commits/0108260858973ec34ae54f6eba5810645779547e)._

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/32624/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-23T23:29:57Z
body: **Size Change:** 0 B 

**Total Size:** 3.72 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `frontend/dist/toolbar.js` | 3.72 MB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR