Skip to content

Commit

Permalink
feat(billing): subscribe to all products (client side) (#22768)
Browse files Browse the repository at this point in the history
Co-authored-by: Raquel Smith <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored and thmsobrmlr committed Jun 24, 2024
1 parent 63f6486 commit 2f243ee
Show file tree
Hide file tree
Showing 35 changed files with 1,010 additions and 119 deletions.
5 changes: 5 additions & 0 deletions ee/api/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 40 additions & 5 deletions frontend/src/lib/components/PayGateMini/PayGateButton.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -8,6 +12,7 @@ interface PayGateButtonProps {
featureInfo: BillingV2FeatureType
onCtaClick: () => void
billing: BillingV2Type | null
isAddonProduct?: boolean
scrollToProduct: boolean
}

Expand All @@ -17,16 +22,27 @@ export const PayGateButton = ({
featureInfo,
onCtaClick,
billing,
isAddonProduct,
scrollToProduct = true,
}: PayGateButtonProps): JSX.Element => {
const { featureFlags } = useValues(featureFlagLogic)
return (
<LemonButton
to={getCtaLink(gateVariant, productWithFeature, featureInfo, scrollToProduct)}
to={getCtaLink(
gateVariant,
productWithFeature,
featureInfo,
featureFlags,
billing?.subscription_level,
isAddonProduct,
scrollToProduct
)}
disableClientSideRouting={gateVariant === 'add-card' && !isAddonProduct}
type="primary"
center
onClick={onCtaClick}
>
{getCtaLabel(gateVariant, billing)}
{getCtaLabel(gateVariant, billing, featureFlags)}
</LemonButton>
)
}
Expand All @@ -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:[email protected]?subject=Inquiring about ${featureInfo.name}`
Expand All @@ -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'
Expand Down
35 changes: 32 additions & 3 deletions frontend/src/lib/components/PayGateMini/PayGateMini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -95,6 +103,7 @@ export function PayGateMini({
productWithFeature={productWithFeature}
isGrandfathered={isGrandfathered}
isAddonProduct={isAddonProduct}
billing={billing}
featureInfoOnNextPlan={featureInfoOnNextPlan}
handleCtaClick={handleCtaClick}
>
Expand All @@ -106,6 +115,7 @@ export function PayGateMini({
onCtaClick={handleCtaClick}
billing={billing}
scrollToProduct={scrollToProduct}
isAddonProduct={isAddonProduct}
/>
{docsLink && isCloudOrDev && (
<LemonButton
Expand Down Expand Up @@ -135,6 +145,7 @@ interface PayGateContentProps {
productWithFeature: BillingProductV2AddonType | BillingProductV2Type
isGrandfathered?: boolean
isAddonProduct?: boolean
billing: BillingV2Type | null
featureInfoOnNextPlan?: BillingV2FeatureType
children: React.ReactNode
handleCtaClick: () => void
Expand All @@ -149,10 +160,12 @@ function PayGateContent({
productWithFeature,
isGrandfathered,
isAddonProduct,
billing,
featureInfoOnNextPlan,
children,
handleCtaClick,
}: PayGateContentProps): JSX.Element {
const { featureFlags } = useValues(featureFlagLogic)
return (
<div
className={clsx(
Expand All @@ -171,6 +184,8 @@ function PayGateContent({
gateVariant,
featureInfo,
productWithFeature,
billing,
featureFlags,
isAddonProduct,
handleCtaClick
)}
Expand All @@ -187,6 +202,8 @@ const renderUsageLimitMessage = (
gateVariant: 'add-card' | 'contact-sales' | 'move-to-cloud' | null,
featureInfo: BillingV2FeatureType,
productWithFeature: BillingProductV2AddonType | BillingProductV2Type,
billing: BillingV2Type | null,
featureFlags: FeatureFlagsSet,
isAddonProduct?: boolean,
handleCtaClick?: () => void
): JSX.Element => {
Expand Down Expand Up @@ -223,9 +240,13 @@ const renderUsageLimitMessage = (
.
</p>
</>
) : featureFlags[FEATURE_FLAGS.SUBSCRIBE_TO_ALL_PRODUCTS] === 'test' &&
billing?.subscription_level === 'free' &&
!isAddonProduct ? (
<p>Upgrade to create more {featureInfo.name}</p>
) : (
<p>
Please upgrade your <b>{productWithFeature.name}</b> plan to create more {featureInfo.name}
Upgrade your <b>{productWithFeature.name}</b> plan to create more {featureInfo.name}
</p>
)}
</div>
Expand All @@ -234,14 +255,16 @@ const renderUsageLimitMessage = (
return (
<>
<p className="max-w-140">{featureInfo.description}</p>
<p>{renderGateVariantMessage(gateVariant, productWithFeature, isAddonProduct)}</p>
<p>{renderGateVariantMessage(gateVariant, productWithFeature, billing, featureFlags, isAddonProduct)}</p>
</>
)
}

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') {
Expand All @@ -252,7 +275,13 @@ const renderGateVariantMessage = (
Subscribe to the <b>{productWithFeature?.name}</b> 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 <b>{productWithFeature?.name}</b> plan to use this feature.
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -29,6 +30,7 @@ export function LemonBanner({
action,
className,
dismissKey = '',
hideIcon = false,
}: LemonBannerProps): JSX.Element | null {
const logic = lemonBannerLogic({ dismissKey })
const { isDismissed } = useValues(logic)
Expand All @@ -49,11 +51,12 @@ export function LemonBanner({
return (
<div className={clsx('LemonBanner @container', `LemonBanner--${type}`, className)}>
<div className="flex items-center gap-2 grow @md:px-1">
{type === 'warning' || type === 'error' ? (
<IconWarning className="LemonBanner__icon hidden @md:block" />
) : (
<IconInfo className="LemonBanner__icon hidden @md:block" />
)}
{!hideIcon &&
(type === 'warning' || type === 'error' ? (
<IconWarning className="LemonBanner__icon hidden @md:block" />
) : (
<IconInfo className="LemonBanner__icon hidden @md:block" />
))}
<div className="grow overflow-hidden">{children}</div>
{action && <LemonButton className="hidden @md:flex" type="secondary" {...action} />}
{showCloseButton && <LemonButton size="small" icon={<IconX />} onClick={_onClose} aria-label="close" />}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/mocks/fixtures/_billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Loading

0 comments on commit 2f243ee

Please sign in to comment.