diff --git a/ee/api/billing.py b/ee/api/billing.py index f8d17abc1237f..4fc575f3f20fd 100644 --- a/ee/api/billing.py +++ b/ee/api/billing.py @@ -93,6 +93,7 @@ class ActivateSerializer(serializers.Serializer): required=False ) # This is required but in order to support an error for the legacy 'plan' param we need to set required=False redirect_path = serializers.CharField(required=False) + intent_product = serializers.CharField(required=False) def validate(self, data): plan = data.get("plan") @@ -137,6 +138,10 @@ def handle_activate(self, request: Request, *args: Any, **kwargs: Any) -> HttpRe products = serializer.validated_data.get("products") url = f"{url}&products={products}" + intent_product = serializer.validated_data.get("intent_product") + if intent_product: + url = f"{url}&intent_product={intent_product}" + if license: billing_service_token = build_billing_token(license, organization) url = f"{url}&token={billing_service_token}" diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png index 8d03b460eaa59..334fe2456043a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png index 39a80754dc23b..c6748bed94cfe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png index bb6e192a5657f..d431e3917d1e0 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--light.png index 8f4d1ee744de6..7f0e7183c9c42 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png index 7051bac980dcd..740ad3e2472c9 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png index 8b8412564ad3f..edec49654e3fb 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png index dd8385c01c855..93c95b9ab39fc 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png index 5a047eae33621..0543328b68b13 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png index 53c517a1d76bd..8379eea3966ed 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png index 8739317556a85..84c7358818a49 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--dark.png index 41cf076d4b61a..a0c43fd960e26 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--light.png index c21fbc19dae0b..ceda7e31b5207 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-limit-and-100-percent-discount--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 5f7d18dd2a783..73463aef127dd 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 a9e3a5f2b7551..61c378927dc4c 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/lib/components/PayGateMini/PayGateButton.tsx b/frontend/src/lib/components/PayGateMini/PayGateButton.tsx index 31a18e5eee854..26790cf7d138e 100644 --- a/frontend/src/lib/components/PayGateMini/PayGateButton.tsx +++ b/frontend/src/lib/components/PayGateMini/PayGateButton.tsx @@ -1,4 +1,8 @@ import { LemonButton } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' +import { urls } from 'scenes/urls' import { BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType, BillingV2Type } from '~/types' @@ -8,6 +12,7 @@ interface PayGateButtonProps { featureInfo: BillingV2FeatureType onCtaClick: () => void billing: BillingV2Type | null + isAddonProduct?: boolean scrollToProduct: boolean } @@ -17,16 +22,27 @@ export const PayGateButton = ({ featureInfo, onCtaClick, billing, + isAddonProduct, scrollToProduct = true, }: PayGateButtonProps): JSX.Element => { + const { featureFlags } = useValues(featureFlagLogic) return ( - {getCtaLabel(gateVariant, billing)} + {getCtaLabel(gateVariant, billing, featureFlags)} ) } @@ -35,9 +51,21 @@ const getCtaLink = ( gateVariant: 'add-card' | 'contact-sales' | 'move-to-cloud' | null, productWithFeature: BillingProductV2AddonType | BillingProductV2Type, featureInfo: BillingV2FeatureType, + featureFlags: FeatureFlagsSet, + subscriptionLevel?: BillingV2Type['subscription_level'], + isAddonProduct?: boolean, scrollToProduct: boolean = true ): string | undefined => { - if (gateVariant === 'add-card') { + if ( + gateVariant === 'add-card' && + !isAddonProduct && + featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + subscriptionLevel === 'free' + ) { + return `/api/billing/activate?products=all_products:&redirect_path=${urls.organizationBilling()}&intent_product=${ + productWithFeature.type + }` + } else if (gateVariant === 'add-card') { return `/organization/billing${scrollToProduct ? `?products=${productWithFeature.type}` : ''}` } else if (gateVariant === 'contact-sales') { return `mailto:sales@posthog.com?subject=Inquiring about ${featureInfo.name}` @@ -49,9 +77,16 @@ const getCtaLink = ( const getCtaLabel = ( gateVariant: 'add-card' | 'contact-sales' | 'move-to-cloud' | null, - billing: BillingV2Type | null + billing: BillingV2Type | null, + featureFlags: FeatureFlagsSet ): string => { - if (gateVariant === 'add-card') { + if ( + gateVariant === 'add-card' && + featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level === 'free' + ) { + return 'Upgrade now' + } else if (gateVariant === 'add-card') { return billing?.has_active_subscription ? 'Upgrade now' : 'Subscribe now' } else if (gateVariant === 'contact-sales') { return 'Contact sales' diff --git a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx index de33f34fbcc25..c76e4a7c030d9 100644 --- a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx +++ b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx @@ -2,13 +2,21 @@ import { IconInfo, IconOpenSidebar } from '@posthog/icons' import { LemonButton, Link, Tooltip } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' import posthog from 'posthog-js' import { useEffect } from 'react' import { billingLogic } from 'scenes/billing/billingLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { getProductIcon } from 'scenes/products/Products' -import { AvailableFeature, BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType } from '~/types' +import { + AvailableFeature, + BillingProductV2AddonType, + BillingProductV2Type, + BillingV2FeatureType, + BillingV2Type, +} from '~/types' import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic' import { PayGateButton } from './PayGateButton' @@ -95,6 +103,7 @@ export function PayGateMini({ productWithFeature={productWithFeature} isGrandfathered={isGrandfathered} isAddonProduct={isAddonProduct} + billing={billing} featureInfoOnNextPlan={featureInfoOnNextPlan} handleCtaClick={handleCtaClick} > @@ -106,6 +115,7 @@ export function PayGateMini({ onCtaClick={handleCtaClick} billing={billing} scrollToProduct={scrollToProduct} + isAddonProduct={isAddonProduct} /> {docsLink && isCloudOrDev && ( void @@ -149,10 +160,12 @@ function PayGateContent({ productWithFeature, isGrandfathered, isAddonProduct, + billing, featureInfoOnNextPlan, children, handleCtaClick, }: PayGateContentProps): JSX.Element { + const { featureFlags } = useValues(featureFlagLogic) return (
void ): JSX.Element => { @@ -223,9 +240,13 @@ const renderUsageLimitMessage = ( .

+ ) : featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level === 'free' && + !isAddonProduct ? ( +

Upgrade to create more {featureInfo.name}

) : (

- Please upgrade your {productWithFeature.name} plan to create more {featureInfo.name} + Upgrade your {productWithFeature.name} plan to create more {featureInfo.name}

)}
@@ -234,7 +255,7 @@ const renderUsageLimitMessage = ( return ( <>

{featureInfo.description}

-

{renderGateVariantMessage(gateVariant, productWithFeature, isAddonProduct)}

+

{renderGateVariantMessage(gateVariant, productWithFeature, billing, featureFlags, isAddonProduct)}

) } @@ -242,6 +263,8 @@ const renderUsageLimitMessage = ( const renderGateVariantMessage = ( gateVariant: 'add-card' | 'contact-sales' | 'move-to-cloud' | null, productWithFeature: BillingProductV2AddonType | BillingProductV2Type, + billing: BillingV2Type | null, + featureFlags: FeatureFlagsSet, isAddonProduct?: boolean ): JSX.Element => { if (gateVariant === 'move-to-cloud') { @@ -252,7 +275,13 @@ const renderGateVariantMessage = ( Subscribe to the {productWithFeature?.name} addon to use this feature. ) + } else if ( + featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level === 'free' + ) { + return <>Upgrade to use this feature. } + return ( <> Upgrade your {productWithFeature?.name} plan to use this feature. diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index c4009ae7d77d6..92327684c1fc2 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -206,6 +206,7 @@ export const FEATURE_FLAGS = { PERSONLESS_EVENTS_NOT_SUPPORTED: 'personless-events-not-supported', // owner: @raquelmsmith SESSION_REPLAY_UNIVERSAL_FILTERS: 'session-replay-universal-filters', // owner: #team-replay ALERTS: 'alerts', // owner: github.com/nikitaevg + SUBSCRIBE_TO_ALL_PRODUCTS: 'subscribe-to-all-products', // owner: #team-growth ERROR_TRACKING: 'error-tracking', // owner: #team-replay SETTINGS_BOUNCE_RATE_PAGE_VIEW_MODE: 'settings-bounce-rate-page-view-mode', // owner: @robbie-c SURVEYS_BRANCHING_LOGIC: 'surveys-branching-logic', // owner: @jurajmajerik #team-feature-success diff --git a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx index 9ce2a5eb28fa2..c49bf90d79933 100644 --- a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx +++ b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx @@ -19,6 +19,7 @@ export interface LemonBannerProps { className?: string /** If provided, the banner will be dismissed and hidden when the key is set in localStorage. */ dismissKey?: string + hideIcon?: boolean } /** Generic alert message. */ @@ -29,6 +30,7 @@ export function LemonBanner({ action, className, dismissKey = '', + hideIcon = false, }: LemonBannerProps): JSX.Element | null { const logic = lemonBannerLogic({ dismissKey }) const { isDismissed } = useValues(logic) @@ -49,11 +51,12 @@ export function LemonBanner({ return (
- {type === 'warning' || type === 'error' ? ( - - ) : ( - - )} + {!hideIcon && + (type === 'warning' || type === 'error' ? ( + + ) : ( + + ))}
{children}
{action && } {showCloseButton && } onClick={_onClose} aria-label="close" />} diff --git a/frontend/src/mocks/fixtures/_billing.tsx b/frontend/src/mocks/fixtures/_billing.tsx index 0b24ab031e9f2..f8fa0010554ff 100644 --- a/frontend/src/mocks/fixtures/_billing.tsx +++ b/frontend/src/mocks/fixtures/_billing.tsx @@ -3835,4 +3835,5 @@ export const billingJson: BillingV2Type = { custom_limits_usd: {}, stripe_portal_url: 'https://billing.stripe.com/p/session/test_YWNjdF8xSElNRERFdUlhdFJYU2R6LF9QaEVJR3VyemlvMDZzRzdiQXZrc1AxSjNXZk1BellP0100ZsforDQG', + subscription_level: 'paid', } diff --git a/frontend/src/scenes/billing/AllProductsPlanComparison.tsx b/frontend/src/scenes/billing/AllProductsPlanComparison.tsx new file mode 100644 index 0000000000000..2817ee2129cce --- /dev/null +++ b/frontend/src/scenes/billing/AllProductsPlanComparison.tsx @@ -0,0 +1,557 @@ +import { IconCheckCircle, IconWarning, IconX } from '@posthog/icons' +import { LemonCollapse, LemonModal, LemonTag, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' +import { FEATURE_FLAGS, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import React, { useState } from 'react' +import { getProductIcon } from 'scenes/products/Products' +import useResizeObserver from 'use-resize-observer' + +import { BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType, BillingV2PlanType } from '~/types' + +import { convertLargeNumberToWords, getProration, getProrationMessage, getUpgradeProductLink } from './billing-utils' +import { billingLogic } from './billingLogic' +import { billingProductLogic } from './billingProductLogic' +import { UnsubscribeSurveyModal } from './UnsubscribeSurveyModal' + +export function PlanIcon({ + feature, + className, + timeDenominator, +}: { + feature?: BillingV2FeatureType + className?: string + timeDenominator?: string +}): JSX.Element { + return ( +
+ {!feature ? ( + <> + + + ) : feature.limit ? ( + <> + + {feature.limit && + `${convertLargeNumberToWords(feature.limit, null)} ${feature.unit && feature.unit}${ + timeDenominator ? `/${timeDenominator}` : '' + }`} + {feature.note} + + ) : ( + <> + + {feature.note} + + )} +
+ ) +} + +const PricingTiers = ({ + plan, + product, +}: { + plan: BillingV2PlanType + product: BillingProductV2Type | BillingProductV2AddonType +}): JSX.Element => { + const { width, ref: tiersRef } = useResizeObserver() + const tiers = plan?.tiers + + const allTierPrices = tiers?.map((tier) => parseFloat(tier.unit_amount_usd)) + const sigFigs = allTierPrices?.map((price) => price?.toString().split('.')[1]?.length).sort((a, b) => b - a)[0] + + return ( + <> + {tiers ? ( + tiers?.map((tier, i) => ( +
+ + {convertLargeNumberToWords(tier.up_to, tiers[i - 1]?.up_to, true, product.unit)} + + + {i === 0 && parseFloat(tier.unit_amount_usd) === 0 + ? 'Free' + : `$${parseFloat(tier.unit_amount_usd).toFixed(sigFigs)}`} + +
+ )) + ) : product?.free_allocation ? ( +
+ + Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo + + Free +
+ ) : null} + + ) +} + +/** + * Determines the pricing description for a given plan. + * + * @param {Object} plan + * @param {boolean} plan.free_allocation - Indicates if the plan has a free allocation. + * @param {boolean} plan.tiers - Indicates if the plan has tiers. + * @param {string} plan.unit_amount_usd - The unit amount in USD. + * @param {boolean} plan.contact_support - Indicates if the plan requires contacting support. + * @param {string} plan.included_if - Condition for plan inclusion. + * @returns {string} - The pricing description for the plan. + */ +function getPlanDescription(plan: BillingV2PlanType): string { + if (plan.free_allocation && !plan.tiers) { + return 'Free forever' + } else if (plan.unit_amount_usd) { + return `$${parseFloat(plan.unit_amount_usd).toFixed(0)} per month` + } else if (plan.contact_support) { + return 'Custom' + } else if (plan.included_if === 'has_subscription') { + return 'Usage-based - starting at $0 per month' + } + return '$0 per month' +} + +export const AllProductsPlanComparison = ({ + product, + includeAddons = false, +}: { + product: BillingProductV2Type + includeAddons?: boolean +}): JSX.Element | null => { + const plans = product.plans?.filter( + (plan) => !plan.included_if || plan.included_if == 'has_subscription' || plan.current_plan + ) + if (plans?.length === 0) { + return null + } + const { billing, redirectPath, timeRemainingInSeconds, timeTotalInSeconds } = useValues(billingLogic) + const { ref: planComparisonRef } = useResizeObserver() + const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) + const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) + const { surveyID, comparisonModalHighlightedFeatureKey } = useValues(billingProductLogic({ product })) + const { reportSurveyShown, setSurveyResponse } = useActions(billingProductLogic({ product })) + const { featureFlags } = useValues(featureFlagLogic) + + const nonInclusionProducts = billing?.products.filter((p) => !p.inclusion_only) || [] + const inclusionProducts = billing?.products.filter((p) => !!p.inclusion_only) || [] + const sortedProducts = nonInclusionProducts + ?.filter((p) => p.type === product.type) + .slice() + .concat(nonInclusionProducts.filter((p) => p.type !== product.type)) + const platformAndSupportProduct = inclusionProducts.find((p) => p.type === 'platform_and_support') + const platformAndSupportPlans = platformAndSupportProduct?.plans.filter((p) => !p.contact_support) || [] + + const upgradeButtons = plans?.map((plan, i) => { + return ( + + = currentPlanIndex) + ? 'default' + : 'alt' + } + fullWidth + center + disableClientSideRouting={!plan.contact_support} + disabledReason={ + plan.included_if == 'has_subscription' && i >= currentPlanIndex + ? billing?.has_active_subscription + ? 'Unsubscribe from all products to remove' + : null + : plan.current_plan + ? 'Current plan' + : undefined + } + onClick={() => { + if (!plan.current_plan) { + // TODO: add current plan key and new plan key + reportBillingUpgradeClicked(product.type) + } + if (plan.included_if == 'has_subscription' && !plan.current_plan && i < currentPlanIndex) { + setSurveyResponse(product.type, '$survey_response_1') + reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type) + } + }} + data-attr={`upgrade-${plan.name}`} + > + {plan.current_plan + ? 'Current plan' + : i < currentPlanIndex + ? 'Downgrade' + : plan.contact_support + ? 'Get in touch' + : plan.included_if == 'has_subscription' && + i >= currentPlanIndex && + !billing?.has_active_subscription + ? 'Upgrade' + : plan.free_allocation && !plan.tiers + ? 'Select' // Free plan + : 'Upgrade'} + + + ) + }) + + return ( +
+ {surveyID && } + + + {/* Plan name header row */} + + + ))} + + + + {/* Plan price row */} + + + {platformAndSupportPlans?.map((plan) => { + const { prorationAmount, isProrated } = getProration({ + timeRemainingInSeconds, + timeTotalInSeconds, + amountUsd: plan.unit_amount_usd, + hasActiveSubscription: billing?.has_active_subscription, + }) + return ( + + ) + })} + + {/* CTA Row */} + + + {/* Inclusion products */} + {inclusionProducts.reverse().map((includedProduct) => { + const includedPlans = includedProduct.plans.filter( + (plan) => plan.included_if == 'has_subscription' || plan.current_plan + ) + return ( + + + {/* Inclusion product title row */} + + + {includedPlans + .find((plan: BillingV2PlanType) => plan.included_if == 'has_subscription') + ?.features?.map((feature) => ( + // Inclusion product feature row + + + {includedPlans?.map((plan) => ( + + {/* Some products don't have a free plan, so we need to pretend there is one + so the features line up in the correct columns in the UI. This is kind of + hacky because it assumes we only have 2 plans total, but it works for now. + */} + {includedPlans?.length === 1 && ( + + )} + + + ))} + + ))} + + ) + })} + +
+ {platformAndSupportPlans?.map((plan) => ( + +

{plan.name}

+
Monthly {product.tiered && 'base '} price + {getPlanDescription(plan)} + {isProrated && ( +

+ {getProrationMessage(prorationAmount, plan.unit_amount_usd)} +

+ )} +
+ {upgradeButtons} +
+
+ {getProductIcon(includedProduct.name, includedProduct.icon_key, 'text-2xl')} + + {includedProduct.name} + +
+
+ + {feature.name} + + + + + feature.key === thisPlanFeature.key + )} + className="text-base" + /> +
+ +

Product features breakdown:

+ ({ + header: ( + + {getProductIcon(currentProduct.name, currentProduct.icon_key, 'text-2xl')} + + {currentProduct.name} {currentProduct.type === product.type ? '(this product)' : ''} + + + ), + className: 'bg-white', + key: currentProduct.type, + content: ( + + + {/* Pricing row */} + + + {currentProduct.plans?.map((plan) => ( + + ))} + + + +

Product Features:

+ + + {currentProduct.plans[currentProduct.plans.length - 1]?.features?.map( + (feature, i) => ( + + + {currentProduct.plans?.map((plan) => ( + + ))} + + ) + )} + {includeAddons && product.addons.length > 0 && ( + + + + )} + {includeAddons && + currentProduct.addons + ?.filter((addon) => { + if (addon.inclusion_only) { + if (featureFlags[FEATURE_FLAGS.PERSONLESS_EVENTS_NOT_SUPPORTED]) { + return false + } + } + return true + }) + .map((addon) => { + return addon.tiered ? ( + + + {plans?.map((plan, i) => { + // If the parent plan is free, the addon isn't available + return !addon.inclusion_only ? ( + plan.free_allocation && !plan.tiers ? ( + + ) : ( + + ) + ) : plan.free_allocation && !plan.tiers ? ( + + ) : ( + + ) + })} + + ) : null + })} + +
+ {includeAddons && currentProduct.addons?.length > 0 && ( +

+ {currentProduct.name} +

+ )} +

Priced per {currentProduct.unit}

+
+ +
+ +
+ {feature.name} +
+
+
+ feature.key === thisPlanFeature.key + )} + className="text-base" + /> +
+

Available add-ons:

+
+

+ + + {addon.name} + + + + + {addon.inclusion_only ? 'config' : 'add-on'} + + +

+

+ Priced per {addon.unit} +

+
+

+ Not available on this plan. +

+
+ + + + + +
+ ), + })) || [] + } + /> +
+ ) +} + +export const AllProductsPlanComparisonModal = ({ + product, + title, + includeAddons = false, + modalOpen, + onClose, +}: { + product: BillingProductV2Type + title?: string + includeAddons?: boolean + modalOpen: boolean + onClose?: () => void +}): JSX.Element | null => { + return ( + +
+
+ {title ?

{title}

:

{product.name} plans

} + +
+
+
+ ) +} + +const AddonPlanTiers = ({ + plan, + addon, +}: { + plan: BillingV2PlanType + addon: BillingProductV2AddonType +}): JSX.Element => { + const [showTiers, setShowTiers] = useState(false) + + return showTiers ? ( + <> + +

+ setShowTiers(false)} className="text-xs"> + Hide volume discounts + +

+ + ) : ( + <> +

+ + First {convertLargeNumberToWords(plan?.tiers?.[0].up_to || 0, null)} {addon.unit}s free + + , then just ${plan?.tiers?.[1].unit_amount_usd}. +

+

+ setShowTiers(true)} className="text-xs"> + Show volume discounts + +

+ + ) +} diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index c6cb267ffd255..7052186af73f6 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -8,20 +8,24 @@ import { Field, Form } from 'kea-forms' import { router } from 'kea-router' import { SurprisedHog } from 'lib/components/hedgehogs' import { supportLogic } from 'lib/components/Support/supportLogic' +import { FEATURE_FLAGS } from 'lib/constants' import { dayjs } from 'lib/dayjs' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { useEffect } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { BillingCTAHero } from './BillingCTAHero' import { BillingHero } from './BillingHero' import { billingLogic } from './billingLogic' import { BillingProduct } from './BillingProduct' +import { UnsubscribeCard } from './UnsubscribeCard' export const scene: SceneExport = { component: Billing, @@ -42,6 +46,7 @@ export function Billing(): JSX.Element { const { reportBillingV2Shown } = useActions(billingLogic) const { preflight, isCloudOrDev } = useValues(preflightLogic) const { openSupportForm } = useActions(supportLogic) + const { featureFlags } = useValues(featureFlagLogic) if (preflight && !isCloudOrDev) { router.actions.push(urls.default()) @@ -87,8 +92,9 @@ export function Billing(): JSX.Element { } const products = billing?.products + const platformAndSupportProduct = products?.find((product) => product.type === 'platform_and_support') return ( -
+
{showLicenseDirectInput && ( <>
@@ -119,11 +125,15 @@ export function Billing(): JSX.Element { ) : null} {!billing?.has_active_subscription && ( - <> -
+
+ {featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' ? ( + platformAndSupportProduct ? ( + + ) : null + ) : ( -
- + )} +
)}
{!isOnboarding && billing?.billing_period && ( -
+
{billing?.has_active_subscription && ( <> @@ -204,7 +214,7 @@ export function Billing(): JSX.Element {
{!isOnboarding && billing?.has_active_subscription && ( -
+
+ +

Products

- {products ?.filter((product) => !product.inclusion_only || product.plans.some((plan) => !plan.included_if)) @@ -278,6 +289,16 @@ export function Billing(): JSX.Element {
))} +
+ {featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level == 'paid' && + !!platformAndSupportProduct ? ( + <> + + + + ) : null} +
) } diff --git a/frontend/src/scenes/billing/BillingCTAHero.tsx b/frontend/src/scenes/billing/BillingCTAHero.tsx new file mode 100644 index 0000000000000..16b6b9338f2c8 --- /dev/null +++ b/frontend/src/scenes/billing/BillingCTAHero.tsx @@ -0,0 +1,67 @@ +import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { BlushingHog } from 'lib/components/hedgehogs' +import useResizeObserver from 'use-resize-observer' + +import { BillingProductV2Type } from '~/types' + +import { billingLogic } from './billingLogic' +import { billingProductLogic } from './billingProductLogic' +import { PlanComparisonModal } from './PlanComparison' + +export const BillingCTAHero = ({ product }: { product: BillingProductV2Type }): JSX.Element => { + const { width, ref: billingHeroRef } = useResizeObserver() + + const { redirectPath } = useValues(billingLogic) + const { isPlanComparisonModalOpen, billingProductLoading } = useValues(billingProductLogic({ product })) + const { toggleIsPlanComparisonModalOpen, setBillingProductLoading } = useActions(billingProductLogic({ product })) + + return ( +
+
+

Get the whole hog.

+

Only pay for what you use.

+
+

PostHog comes with all product features on every plan.

+

+ Add your credit card to remove usage limits and unlock all platform features. Set billing limits + as low as $0 to control your spend. +

+

P.S. You still keep the monthly free allotment for every product!

+
+
+ setBillingProductLoading(product.type)} + > + Upgrade now + + toggleIsPlanComparisonModalOpen()} + type="primary" + > + Compare plans + +
+
+ {width && width > 500 && ( +
+ +
+ )} + toggleIsPlanComparisonModalOpen()} + /> +
+ ) +} diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index 7045b3b048af9..af8e4639e7ef4 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -13,7 +13,7 @@ export const BillingHero = (): JSX.Element => {

Get the whole hog.

Only pay for what you use.

- Subscribe to get access to premium product and platform features. Set billing limits as low as $0 to + Upgrade to get access to premium product and platform features. Set billing limits as low as $0 to control spend.

diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx index 7d48c0c2e079d..c07f58b1f1d04 100644 --- a/frontend/src/scenes/billing/BillingProduct.tsx +++ b/frontend/src/scenes/billing/BillingProduct.tsx @@ -78,8 +78,12 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): const upgradeToPlanKey = upgradePlan?.plan_key const currentPlanKey = currentPlan?.plan_key + + // Note(@zach): The upgrade card will be removed when Subscribe to all products is fully rolled out const showUpgradeCard = - (upgradePlan?.product_key !== 'platform_and_support' || product?.addons?.length === 0) && upgradePlan + (upgradePlan?.product_key !== 'platform_and_support' || product?.addons?.length === 0) && + upgradePlan && + (featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] !== 'test' || billing?.subscription_level == 'custom') const { ref, size } = useResizeBreakpoints({ 0: 'small', @@ -88,7 +92,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): return (
Learn how to reduce your bill - {product.plans?.length > 0 ? ( - { - setSurveyResponse(product.type, '$survey_response_1') - reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type) - }} - > - Unsubscribe - - ) : ( - - Contact support to unsubscribe - - )} + {featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] !== 'test' && + (product.plans?.length > 0 ? ( + { + setSurveyResponse(product.type, '$survey_response_1') + reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type) + }} + > + Unsubscribe + + ) : ( + + Contact support to unsubscribe + + ))} } /> @@ -284,7 +289,29 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): {showTierBreakdown && } {product.addons?.length > 0 && (
-

Addons

+

Add-ons

+ {featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] == 'test' && + billing?.subscription_level == 'free' && ( + +
+
+ Add-ons are only available on paid plans. Upgrade to access these + features. +
+ setBillingProductLoading(product.type)} + > + Upgrade now + +
+
+ )}
{product.addons // TODO: enhanced_persons: remove this filter @@ -392,12 +419,14 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): !upgradePlan.unit_amount_usd && ( } disableClientSideRouting diff --git a/frontend/src/scenes/billing/BillingProductAddon.tsx b/frontend/src/scenes/billing/BillingProductAddon.tsx index 7456c4baedee1..ff1ea9842f1b3 100644 --- a/frontend/src/scenes/billing/BillingProductAddon.tsx +++ b/frontend/src/scenes/billing/BillingProductAddon.tsx @@ -1,8 +1,9 @@ -import { IconCheckCircle, IconDocument, IconPlus } from '@posthog/icons' +import { IconCheckCircle, IconPlus } from '@posthog/icons' import { LemonButton, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { FEATURE_FLAGS, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' import { More } from 'lib/lemon-ui/LemonButton/More' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { ReactNode, useMemo, useRef } from 'react' import { getProductIcon } from 'scenes/products/Products' @@ -29,6 +30,7 @@ const formatFlatRate = (flatRate: number, unit: string | null): string | ReactNo export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonType }): JSX.Element => { const productRef = useRef(null) + const { featureFlags } = useValues(featureFlagLogic) const { billing, redirectPath, billingError, timeTotalInSeconds, timeRemainingInSeconds } = useValues(billingLogic) const { isPricingModalOpen, currentAndUpgradePlans, surveyID, billingProductLoading } = useValues( billingProductLogic({ product: addon, productRef }) @@ -93,7 +95,14 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp ) )}
-

{addon.description}

+

+ {addon.description}{' '} + {addon.docs_url && ( + <> + Read the docs for more information. + + )} +

{is_enhanced_persons_og_customer && (

- {addon.docs_url && ( - } - size="small" - to={addon.docs_url} - tooltip="Read the docs" - /> - )} {addon.subscribed && !addon.inclusion_only ? ( <> - Remove addon + Remove add-on } @@ -164,7 +165,12 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp icon={} size="small" disableClientSideRouting - disabledReason={billingError && billingError.message} + disabledReason={ + (billingError && billingError.message) || + (featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level === 'free' && + 'Upgrade to add add-ons') + } loading={billingProductLoading === addon.type} onClick={() => initiateProductUpgrade( @@ -191,7 +197,7 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
- {addonFeatures?.length > 1 && ( + {addonFeatures?.length > 2 && (

Features included:

diff --git a/frontend/src/scenes/billing/PlanComparison.scss b/frontend/src/scenes/billing/PlanComparison.scss index 6f72a074f8b61..93ec0768904b3 100644 --- a/frontend/src/scenes/billing/PlanComparison.scss +++ b/frontend/src/scenes/billing/PlanComparison.scss @@ -6,7 +6,7 @@ table.PlanComparison { table-layout: fixed; td { - padding: 0.75rem 1.25rem; + padding: 0.75rem 1rem; vertical-align: top; &.PlanTable__td__upgradeButton { @@ -16,22 +16,22 @@ table.PlanComparison { } th { - padding: 0.75rem 1.25rem; + padding: 0.75rem 1rem; font-weight: 600; text-align: left; vertical-align: top; &.PlanTable__th__section { - padding: 0.25rem 1.25rem; + padding: 0.25rem 1rem; font-weight: 500; } &.PlanTable__th__feature { - padding: 0.75rem 1.25rem 0.75rem 3.25rem; + padding: 0.75rem 1rem 0.75rem 3.25rem; font-weight: 600; &.PlanTable__th__feature--reduced_padding { - padding: 0.75rem 1.25rem; + padding: 0.75rem 1rem; } } diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 42c627495c64e..741609879ffd2 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -11,7 +11,6 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React, { useState } from 'react' import { getProductIcon } from 'scenes/products/Products' -import { urls } from 'scenes/urls' import useResizeObserver from 'use-resize-observer' import { BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType, BillingV2PlanType } from '~/types' @@ -128,6 +127,7 @@ export const PlanComparison = ({ const { reportSurveyShown, setSurveyResponse } = useActions(billingProductLogic({ product })) const { featureFlags } = useValues(featureFlagLogic) + const ctaAction = featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' ? 'Upgrade' : 'Subscribe' const upgradeButtons = plans?.map((plan, i) => { return ( @@ -135,13 +135,14 @@ export const PlanComparison = ({ to={ plan.contact_support ? 'mailto:sales@posthog.com?subject=Enterprise%20plan%20request' - : !plan.included_if - ? getUpgradeProductLink(product, plan.plan_key || '', redirectPath, includeAddons) - : plan.included_if == 'has_subscription' && - i >= currentPlanIndex && - !billing?.has_active_subscription - ? urls.organizationBilling() - : undefined + : getUpgradeProductLink({ + product, + upgradeToPlanKey: plan.plan_key || '', + redirectPath, + includeAddons, + subscriptionLevel: billing?.subscription_level, + featureFlags, + }) } type={plan.current_plan || i < currentPlanIndex ? 'secondary' : 'primary'} status={ @@ -182,15 +183,20 @@ export const PlanComparison = ({ : plan.included_if == 'has_subscription' && i >= currentPlanIndex && !billing?.has_active_subscription - ? 'View products' + ? ctaAction : plan.free_allocation && !plan.tiers ? 'Select' // Free plan - : 'Subscribe'} + : ctaAction} {!plan.current_plan && !plan.free_allocation && includeAddons && product.addons?.length > 0 && (

@@ -233,7 +239,9 @@ export const PlanComparison = ({ : plan.contact_support ? 'Custom' : plan.included_if == 'has_subscription' - ? 'Free, included with any product subscription' + ? featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' + ? 'Usage-based - starting at $0' + : 'Free, included with any product subscription' : '$0 per month'} {isProrated && (

@@ -336,7 +344,9 @@ export const PlanComparison = ({ })} -

Product Features:

+

+ {product.type === 'platform_and_support' ? 'Platform' : 'Product'} features: +

{fullyFeaturedPlan?.features?.map((feature, i) => ( @@ -480,20 +490,22 @@ export const PlanComparison = ({ export const PlanComparisonModal = ({ product, + title, includeAddons = false, modalOpen, onClose, }: { product: BillingProductV2Type + title?: string includeAddons?: boolean modalOpen: boolean onClose?: () => void }): JSX.Element | null => { return ( -
+
-

{product.name} plans

+ {title ?

{title}

:

{product.name} plans

}
diff --git a/frontend/src/scenes/billing/UnsubscribeCard.tsx b/frontend/src/scenes/billing/UnsubscribeCard.tsx new file mode 100644 index 0000000000000..017378fb0077f --- /dev/null +++ b/frontend/src/scenes/billing/UnsubscribeCard.tsx @@ -0,0 +1,52 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' +import { useActions } from 'kea' +import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' + +import { BillingProductV2Type } from '~/types' + +import { billingProductLogic } from './billingProductLogic' + +export const UnsubscribeCard = ({ product }: { product: BillingProductV2Type }): JSX.Element => { + const { reportSurveyShown, setSurveyResponse } = useActions(billingProductLogic({ product })) + + return ( +
+
+

Need to take a break?

+

+ Downgrade to the free plan at any time. You'll lose access to platform features and usage limits + will apply immediately. +

+

+ Need to control your costs? Learn about ways to{' '} + + reduce your bill + {' '} + or{' '} + + chat with support. + {' '} + Check out more about our pricing on our{' '} + + pricing page + + . +

+ { + setSurveyResponse(product.type, '$survey_response_1') + reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type) + }} + > + Downgrade to free plan + +
+
+ ) +} diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx index 042c1b2f95c47..78020c5de6112 100644 --- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx +++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx @@ -2,6 +2,8 @@ import './UnsubscribeSurveyModal.scss' import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { BillingProductV2AddonType, BillingProductV2Type } from '~/types' @@ -14,7 +16,8 @@ export const UnsubscribeSurveyModal = ({ }: { product: BillingProductV2Type | BillingProductV2AddonType }): JSX.Element | null => { - const { surveyID, surveyResponse } = useValues(billingProductLogic({ product })) + const { featureFlags } = useValues(featureFlagLogic) + const { surveyID, surveyResponse, isAddonProduct } = useValues(billingProductLogic({ product })) const { setSurveyResponse, reportSurveyDismissed } = useActions(billingProductLogic({ product })) const { deactivateProduct, resetUnsubscribeError } = useActions(billingLogic) const { unsubscribeError, billingLoading, billing } = useValues(billingLogic) @@ -25,7 +28,17 @@ export const UnsubscribeSurveyModal = ({ product.type == 'data_pipelines' || (product.type == 'product_analytics' && (product as BillingProductV2Type)?.addons?.filter((addon) => addon.type === 'data_pipelines')[0] - ?.subscribed) + ?.subscribed) || + billing?.subscription_level === 'paid' + + const subscribeToAllProductsAndPaid = + featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && billing?.subscription_level === 'paid' + let action = 'Unsubscribe' + let actionVerb = 'unsubscribing' + if (subscribeToAllProductsAndPaid) { + action = isAddonProduct ? 'Remove addon' : 'Downgrade' + actionVerb = isAddonProduct ? 'removing this addon' : 'downgrading' + } return ( { - deactivateProduct(product.type) + deactivateProduct( + featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + billing?.subscription_level === 'paid' && + !isAddonProduct + ? 'all_products' + : product.type + ) }} loading={billingLoading} > - Unsubscribe + {action} } @@ -68,7 +91,7 @@ export const UnsubscribeSurveyModal = ({ ) : (

- Your invoice will be billed immediately.{' '} + Any outstanding invoices will be billed immediately.{' '} View invoices @@ -77,7 +100,7 @@ export const UnsubscribeSurveyModal = ({ )} { setSurveyResponse(value, '$survey_response') diff --git a/frontend/src/scenes/billing/billing-utils.ts b/frontend/src/scenes/billing/billing-utils.ts index 07d738e9efbd3..49457eb9a6a16 100644 --- a/frontend/src/scenes/billing/billing-utils.ts +++ b/frontend/src/scenes/billing/billing-utils.ts @@ -1,4 +1,6 @@ +import { FEATURE_FLAGS } from 'lib/constants' import { dayjs } from 'lib/dayjs' +import { FeatureFlagsSet } from 'lib/logic/featureFlagLogic' import { BillingProductV2Type, BillingV2TierType, BillingV2Type } from '~/types' @@ -158,14 +160,32 @@ export const convertAmountToUsage = ( return Math.round(usage) } -export const getUpgradeProductLink = ( - product: BillingProductV2Type, - upgradeToPlanKey: string, - redirectPath?: string, - includeAddons: boolean = true -): string => { - let url = '/api/billing/activate?products=' - url += `${product.type}:${upgradeToPlanKey},` +export const getUpgradeProductLink = ({ + product, + upgradeToPlanKey, + redirectPath, + includeAddons = true, + subscriptionLevel, + featureFlags, +}: { + product: BillingProductV2Type + upgradeToPlanKey: string + redirectPath?: string + includeAddons: boolean + subscriptionLevel?: BillingV2Type['subscription_level'] + featureFlags: FeatureFlagsSet +}): string => { + let url = '/api/billing/activate?' + if (redirectPath) { + url += `redirect_path=${redirectPath}&` + } + + if (featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && subscriptionLevel == 'free') { + url += `products=all_products:&intent_product=${product.type}` + return url + } + url += `products=${product.type}:${upgradeToPlanKey},` + if (includeAddons && product.addons?.length) { for (const addon of product.addons) { if ( @@ -179,9 +199,6 @@ export const getUpgradeProductLink = ( } // remove the trailing comma that will be at the end of the url url = url.slice(0, -1) - if (redirectPath) { - url += `&redirect_path=${redirectPath}` - } return url } @@ -251,3 +268,7 @@ export const getProration = ({ prorationAmount: prorationAmount.toFixed(2), } } + +export const getProrationMessage = (prorationAmount: string, unitAmountUsd: string | null): string => { + return `Pay ~$${prorationAmount} today (prorated) and $${parseInt(unitAmountUsd || '0')} every month thereafter.` +} diff --git a/frontend/src/scenes/billing/billingLogic.tsx b/frontend/src/scenes/billing/billingLogic.tsx index dcfce29f300af..30604688729a5 100644 --- a/frontend/src/scenes/billing/billingLogic.tsx +++ b/frontend/src/scenes/billing/billingLogic.tsx @@ -202,7 +202,7 @@ export const billingLogic = kea([ try { const response = await api.getResponse('api/billing/deactivate?products=' + key) const jsonRes = await getJSONOrNull(response) - lemonToast.success('Product unsubscribed') + lemonToast.success('You have been unsubscribed') actions.reportProductUnsubscribed(key) return parseBillingResponse(jsonRes) } catch (error: any) { diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index c66abb53c5f43..e365ca8fa2e72 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -1,6 +1,8 @@ import { LemonDialog } from '@posthog/lemon-ui' import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { forms } from 'kea-forms' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import posthog from 'posthog-js' import React from 'react' @@ -24,7 +26,12 @@ export const billingProductLogic = kea([ key((props) => props.product.type), path(['scenes', 'billing', 'billingProductLogic']), connect({ - values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey', 'unsubscribeError']], + values: [ + billingLogic, + ['billing', 'isUnlicensedDebug', 'scrollToProductKey', 'unsubscribeError'], + featureFlagLogic, + ['featureFlags'], + ], actions: [ billingLogic, [ @@ -63,13 +70,8 @@ export const billingProductLogic = kea([ product, redirectPath, }), - handleProductUpgrade: ( - product: BillingProductV2Type | BillingProductV2AddonType, - plan: BillingV2PlanType, - redirectPath?: string - ) => ({ - plan, - product, + handleProductUpgrade: (products: string, redirectPath?: string) => ({ + products, redirectPath, }), }), @@ -237,6 +239,11 @@ export const billingProductLogic = kea([ ].filter(Boolean) }, ], + isAddonProduct: [ + (s, p) => [s.billing, p.product], + (billing, product): boolean => + !!billing?.products?.some((p) => p.addons?.some((addon) => addon.type === product?.type)), + ], })), listeners(({ actions, values, props }) => ({ updateBillingLimitsSuccess: () => { @@ -313,8 +320,6 @@ export const billingProductLogic = kea([ behavior: 'smooth', block: 'center', }) - props.productRef?.current.classList.add('border') - props.productRef?.current.classList.add('border-primary-3000') } }, 0) } @@ -322,10 +327,17 @@ export const billingProductLogic = kea([ }, initiateProductUpgrade: ({ plan, product, redirectPath }) => { actions.setBillingProductLoading(product.type) - actions.handleProductUpgrade(product, plan, redirectPath) + let products = `${product.type}:${plan?.plan_key}` + if ( + values.featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' && + values.billing?.subscription_level == 'free' + ) { + products += ',all_products:' + } + actions.handleProductUpgrade(products, redirectPath) }, - handleProductUpgrade: ({ plan, product, redirectPath }) => { - window.location.href = `/api/billing/activate?products=${product.type}:${plan?.plan_key}${ + handleProductUpgrade: ({ products, redirectPath }) => { + window.location.href = `/api/billing/activate?products=${products}${ redirectPath && `&redirect_path=${redirectPath}` }` }, diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index 463b3a80918f6..c66febd36d5d6 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -3,9 +3,12 @@ import { LemonBanner, LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' import { StarHog } from 'lib/components/hedgehogs' +import { FEATURE_FLAGS } from 'lib/constants' import { Spinner } from 'lib/lemon-ui/Spinner' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { useState } from 'react' +import { AllProductsPlanComparison } from 'scenes/billing/AllProductsPlanComparison' import { getUpgradeProductLink } from 'scenes/billing/billing-utils' import { BillingHero } from 'scenes/billing/BillingHero' import { billingLogic } from 'scenes/billing/billingLogic' @@ -24,6 +27,7 @@ export const OnboardingBillingStep = ({ product: BillingProductV2Type stepKey?: OnboardingStepKey }): JSX.Element => { + const { featureFlags } = useValues(featureFlagLogic) const { billing, redirectPath } = useValues(billingLogic) const { productKey } = useValues(onboardingLogic) const { currentAndUpgradePlans } = useValues(billingProductLogic({ product })) @@ -33,6 +37,7 @@ export const OnboardingBillingStep = ({ const [showPlanComp, setShowPlanComp] = useState(false) + const action = featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' ? 'Upgrade' : 'Subscribe' return ( - Subscribe to paid plan + {action} ) } @@ -65,7 +77,7 @@ export const OnboardingBillingStep = ({

-

Subscribe successful

+

{action} successful

You're all ready to use {product.name}.

@@ -95,7 +107,11 @@ export const OnboardingBillingStep = ({ {(!product.subscribed || showPlanComp) && ( <> - + {featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' ? ( + + ) : ( + + )} )}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 7e3996c6ffc93..3e5b2d6500099 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1578,6 +1578,7 @@ export interface BillingProductV2AddonType { export interface BillingV2Type { customer_id: string has_active_subscription: boolean + subscription_level: 'free' | 'paid' | 'custom' free_trial_until?: Dayjs stripe_portal_url?: string deactivated?: boolean