feat: new platform addons (#32624)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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!')
|
||||
} 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()
|
||||
} catch (e) {
|
||||
lemonToast.error('There was an error cancelling your trial. Please try again or contact support.')
|
||||
actions.setTrialLoading(false)
|
||||
actions.loadBilling()
|
||||
}
|
||||
},
|
||||
triggerMoreHedgehogs: async (_, breakpoint) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||