diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index 109ed36f40b41..0f74dc168071a 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -9,8 +9,8 @@ export const BillingHero = (): JSX.Element => {

Get the whole hog.

Only pay for what you use.

- Upgrade to any paid product plan to get access to features like A/B testing, multivariate feature - flags, and more. + Add your credit card details 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 d9b3b3bd7596d..a32edcd909f37 100644 --- a/frontend/src/scenes/billing/BillingProduct.tsx +++ b/frontend/src/scenes/billing/BillingProduct.tsx @@ -15,7 +15,7 @@ import { import { More } from 'lib/lemon-ui/LemonButton/More' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { BillingProductV2AddonType, BillingProductV2Type, BillingV2TierType } from '~/types' -import { convertLargeNumberToWords, getUpgradeAllProductsLink, summarizeUsage } from './billing-utils' +import { convertLargeNumberToWords, getUpgradeProductLink, summarizeUsage } from './billing-utils' import { BillingGauge } from './BillingGauge' import { billingLogic } from './billingLogic' import { BillingLimitInput } from './BillingLimitInput' @@ -23,7 +23,7 @@ import { billingProductLogic } from './billingProductLogic' import { capitalizeFirstLetter, compactNumber } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { ProductPricingModal } from './ProductPricingModal' -import { PlanComparisonModal } from './PlanComparisonModal' +import { PlanComparisonModal } from './PlanComparison' export const getTierDescription = ( tiers: BillingV2TierType[], @@ -621,21 +621,12 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): Compare plans } disableClientSideRouting diff --git a/frontend/src/scenes/billing/PlanComparison.scss b/frontend/src/scenes/billing/PlanComparison.scss new file mode 100644 index 0000000000000..fbda341b032a6 --- /dev/null +++ b/frontend/src/scenes/billing/PlanComparison.scss @@ -0,0 +1,42 @@ +.PlanComparisonModal { + max-width: 900px; +} +table.PlanComparison { + table-layout: fixed; + td { + vertical-align: top; + padding: 0.75rem 1.25rem; + + &.PlanTable__td__upgradeButton { + padding-top: 1rem; + padding-bottom: 1rem; + } + } + th { + vertical-align: top; + padding: 0.75rem 1.25rem 0.75rem; + font-weight: 600; + text-align: left; + + &.PlanTable__th__section { + padding: 0.25rem 1.25rem 0.25rem; + font-weight: 500; + } + + &.PlanTable__th__feature { + padding: 0.75rem 1.25rem 0.75rem 3.25rem; + font-weight: 600; + } + + &.PlanTable__th__last-feature { + padding-bottom: 2rem; + } + + p { + font-weight: 400; + } + } +} +.PlanTable__tr__border { + border-bottom: 3px rgba(0, 0, 0, 0.07) dotted; +} diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx new file mode 100644 index 0000000000000..1d2c68bb9aa0b --- /dev/null +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -0,0 +1,361 @@ +import React from 'react' +import { LemonButton, LemonModal, LemonTag, Link } from '@posthog/lemon-ui' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { IconCheckmark, IconClose, IconWarning } from 'lib/lemon-ui/icons' +import { BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType, BillingV2PlanType } from '~/types' +import './PlanComparison.scss' +import { useActions, useValues } from 'kea' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { convertLargeNumberToWords, getUpgradeProductLink } from './billing-utils' +import { billingLogic } from './billingLogic' + +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 getProductTiers = ( + plan: BillingV2PlanType, + product: BillingProductV2Type | BillingProductV2AddonType +): JSX.Element => { + const tiers = plan?.tiers + + 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(6)}`} + +
+ )) + ) : product?.free_allocation ? ( +
+ + Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo + + Free +
+ ) : null} + + ) +} + +export const PlanComparison = ({ + product, + includeAddons = false, +}: { + product: BillingProductV2Type + includeAddons?: boolean +}): JSX.Element | null => { + const plans = product.plans + if (plans?.length === 0) { + return null + } + const fullyFeaturedPlan = plans[plans.length - 1] + const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) + const { redirectPath, billing } = useValues(billingLogic) + + const upgradeButtons = plans?.map((plan) => { + return ( + + { + if (!plan.current_plan) { + reportBillingUpgradeClicked(product.type) + } + }} + > + {plan.current_plan ? 'Current plan' : 'Upgrade'} + + {!plan.current_plan && includeAddons && product.addons?.length > 0 && ( +

+ + or upgrade without addons + +

+ )} + + ) + }) + + return ( + + + + + ))} + + + + {/* Pricing section */} + + + + + + {plans?.map((plan) => ( + + ))} + + + + + {plans?.map((plan) => ( + + ))} + + + {includeAddons && + product.addons?.map((addon) => { + return addon.tiered ? ( + + + {plans?.map((plan) => + // If the plan is free, the addon isn't available + plan.free_allocation && !plan.tiers ? ( + + ) : ( + + ) + )} + + ) : null + })} + + + + + + + + {fullyFeaturedPlan?.features?.map((feature, i) => ( + + + {plans?.map((plan) => ( + + ))} + + ))} + + {!billing?.has_active_subscription && ( + <> + + + + {billing?.products + .filter((product) => product.inclusion_only) + .map((includedProduct) => ( + + + + + {includedProduct.plans + .find((plan: BillingV2PlanType) => plan.included_if == 'has_subscription') + ?.features?.map((feature, i) => ( + + + {includedProduct.plans?.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. + */} + {includedProduct.plans?.length === 1 && ( + + )} + + + ))} + + ))} + + ))} + + )} + +
+ {plans?.map((plan) => ( + +

{plan.free_allocation && !plan.tiers ? 'Free' : 'Paid'}

+
+ Pricing +
Monthly base price + {plan.free_allocation && !plan.tiers ? 'Free forever' : '$0 per month'} +
+ {includeAddons && product.addons?.length > 0 && ( +

+ {product.name} +

+ )} +

Priced per {product.unit}

+
{getProductTiers(plan, product)}
+

+ {addon.name} + + addon + +

+

Priced per {addon.unit}

+
+

Not available on this plan.

+
+ {getProductTiers(addon.plans?.[0], addon)} +
+ {upgradeButtons} +
+
+ {product.image_url && ( + {`Logo + )} + + {product.name} + +
+
+ {feature.name} + + feature.key === thisPlanFeature.key + )} + className={'text-base'} + /> +
+

+ + Included platform features: + +

+
+
+ {includedProduct.image_url && ( + {`Logo + )} + + {includedProduct.name} + +
+
plan.included_if == 'has_subscription' + )?.features?.length || 0) - + 1 + ? 'PlanTable__th__last-feature' + : '' + }`} + > + {feature.name} + + + + + feature.key === thisPlanFeature.key + )} + className={'text-base'} + /> +
+ ) +} + +export const PlanComparisonModal = ({ + product, + includeAddons = false, + modalOpen, + onClose, +}: { + product: BillingProductV2Type + includeAddons?: boolean + modalOpen: boolean + onClose?: () => void +}): JSX.Element | null => { + return ( + +
+
+

{product.name} plans

+ +
+
+
+ ) +} diff --git a/frontend/src/scenes/billing/PlanComparisonModal.scss b/frontend/src/scenes/billing/PlanComparisonModal.scss deleted file mode 100644 index 68d1fdffb61de..0000000000000 --- a/frontend/src/scenes/billing/PlanComparisonModal.scss +++ /dev/null @@ -1,42 +0,0 @@ -.PlanComparisonModal { - max-width: 900px; - table { - table-layout: fixed; - td { - vertical-align: top; - padding: 0.75rem 1.25rem; - - &.PlanTable__td__upgradeButton { - padding-top: 1rem; - padding-bottom: 1rem; - } - } - th { - vertical-align: top; - padding: 0.75rem 1.25rem 0.75rem; - font-weight: 600; - text-align: left; - - &.PlanTable__th__section { - padding: 0.25rem 1.25rem 0.25rem; - font-weight: 500; - } - - &.PlanTable__th__feature { - padding: 0.75rem 1.25rem 0.75rem 3.25rem; - font-weight: 600; - } - - &.PlanTable__th__last-feature { - padding-bottom: 2rem; - } - - p { - font-weight: 400; - } - } - } - .PlanTable__tr__border { - border-bottom: 3px rgba(0, 0, 0, 0.07) dotted; - } -} diff --git a/frontend/src/scenes/billing/PlanComparisonModal.tsx b/frontend/src/scenes/billing/PlanComparisonModal.tsx deleted file mode 100644 index e5114042c9bec..0000000000000 --- a/frontend/src/scenes/billing/PlanComparisonModal.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import React from 'react' -import { LemonButton, LemonModal, LemonTag, Link } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconCheckmark, IconClose, IconWarning } from 'lib/lemon-ui/icons' -import { BillingProductV2AddonType, BillingProductV2Type, BillingV2FeatureType, BillingV2PlanType } from '~/types' -import './PlanComparisonModal.scss' -import { useActions, useValues } from 'kea' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { convertLargeNumberToWords, getUpgradeAllProductsLink } from './billing-utils' -import { billingLogic } from './billingLogic' - -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 getProductTiers = ( - plan: BillingV2PlanType, - product: BillingProductV2Type | BillingProductV2AddonType -): JSX.Element => { - const tiers = plan?.tiers - - 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(6)}`} - -
- )) - ) : product?.free_allocation ? ( -
- - Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo - - Free -
- ) : null} - - ) -} - -export const PlanComparisonModal = ({ - product, - includeAddons = false, - modalOpen, - onClose, -}: { - product: BillingProductV2Type - includeAddons?: boolean - modalOpen: boolean - onClose?: () => void -}): JSX.Element | null => { - const plans = product.plans - if (plans?.length === 0) { - return null - } - const fullyFeaturedPlan = plans[plans.length - 1] - const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) - const { redirectPath, billing } = useValues(billingLogic) - - const upgradeButtons = plans?.map((plan) => { - return ( - - { - if (!plan.current_plan) { - reportBillingUpgradeClicked(product.type) - } - }} - > - {plan.current_plan ? 'Current plan' : 'Upgrade'} - - {!plan.current_plan && includeAddons && product.addons?.length > 0 && ( -

- - or upgrade without addons - -

- )} - - ) - }) - - return ( - -
-
-

{product.name} plans

- - - - - ))} - - - - {/* Pricing section */} - - - - - - {plans?.map((plan) => ( - - ))} - - - - - {plans?.map((plan) => ( - - ))} - - - {includeAddons && - product.addons?.map((addon) => { - return addon.tiered ? ( - - - {plans?.map((plan) => - // If the plan is free, the addon isn't available - plan.free_allocation && !plan.tiers ? ( - - ) : ( - - ) - )} - - ) : null - })} - - - - - - - - {fullyFeaturedPlan?.features?.map((feature, i) => ( - - - {plans?.map((plan) => ( - - ))} - - ))} - - {!billing?.has_active_subscription && ( - <> - - - - {billing?.products - .filter((product) => product.inclusion_only) - .map((includedProduct) => ( - - - - - {includedProduct.plans - .find( - (plan: BillingV2PlanType) => - plan.included_if == 'has_subscription' - ) - ?.features?.map((feature, i) => ( - - - {includedProduct.plans?.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. - */} - {includedProduct.plans?.length === 1 && ( - - )} - - - ))} - - ))} - - ))} - - )} - -
- {plans?.map((plan) => ( - -

- {plan.free_allocation && !plan.tiers ? 'Free' : 'Paid'} -

-
- Pricing -
Monthly base price - {plan.free_allocation && !plan.tiers ? 'Free forever' : '$0 per month'} -
- {includeAddons && product.addons?.length > 0 && ( -

- {product.name} -

- )} -

Priced per {product.unit}

-
{getProductTiers(plan, product)}
-

- {addon.name} - - addon - -

-

Priced per {addon.unit}

-
-

- Not available on this plan. -

-
- {getProductTiers(addon.plans?.[0], addon)} -
- {upgradeButtons} -
-
- {product.image_url && ( - {`Logo - )} - - {product.name} - -
-
- {feature.name} - - feature.key === thisPlanFeature.key - )} - className={'text-base'} - /> -
-

- - Included platform features: - -

-
-
- {includedProduct.image_url && ( - {`Logo - )} - - - {includedProduct.name} - - -
-
plan.included_if == 'has_subscription' - )?.features?.length || 0) - - 1 - ? 'PlanTable__th__last-feature' - : '' - }`} - > - - {feature.name} - - - - - - feature.key === thisPlanFeature.key - )} - className={'text-base'} - /> -
-
-
-
- ) -} diff --git a/frontend/src/scenes/billing/billing-utils.ts b/frontend/src/scenes/billing/billing-utils.ts index 3995cf800487b..d0bfaca0a3cb3 100644 --- a/frontend/src/scenes/billing/billing-utils.ts +++ b/frontend/src/scenes/billing/billing-utils.ts @@ -144,14 +144,15 @@ export const convertAmountToUsage = ( return Math.round(usage) } -export const getUpgradeAllProductsLink = ( +export const getUpgradeProductLink = ( product: BillingProductV2Type, upgradeToPlanKey: string, - redirectPath?: string + redirectPath?: string, + includeAddons: boolean = true ): string => { let url = '/api/billing-v2/activation?products=' url += `${product.type}:${upgradeToPlanKey},` - if (product.addons?.length) { + if (includeAddons && product.addons?.length) { for (const addon of product.addons) { if (addon.plans?.[0]?.plan_key) { url += `${addon.type}:${addon.plans[0].plan_key},` diff --git a/frontend/src/scenes/billing/billingLogic.ts b/frontend/src/scenes/billing/billingLogic.ts index 16eb12665904f..2fdc2aa0f56ee 100644 --- a/frontend/src/scenes/billing/billingLogic.ts +++ b/frontend/src/scenes/billing/billingLogic.ts @@ -74,7 +74,11 @@ export const billingLogic = kea([ '' as string, { setRedirectPath: () => { - return window.location.pathname.includes('/ingestion') ? urls.ingestion() + '/billing' : '' + return window.location.pathname.includes('/ingestion') + ? urls.ingestion() + '/billing' + : window.location.pathname.includes('/onboarding') + ? window.location.pathname + : '' }, }, ], diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 5915d1f9a848e..41b299c417f36 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -7,19 +7,22 @@ import { urls } from 'scenes/urls' import { onboardingLogic } from './onboardingLogic' import { SDKs } from './sdks/SDKs' import { OnboardingProductIntro } from './OnboardingProductIntro' -import { OnboardingStep } from './OnboardingStep' import { ProductKey } from '~/types' import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions' import { SessionReplaySDKInstructions } from './sdks/session-replay/SessionReplaySDKInstructions' +import { OnboardingBillingStep } from './OnboardingBillingStep' export const scene: SceneExport = { component: Onboarding, logic: onboardingLogic, } +/** + * Wrapper for custom onboarding content. This automatically includes the product intro and billing step. + */ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => { - const { onboardingStep } = useValues(onboardingLogic) - const { setTotalOnboardingSteps } = useActions(onboardingLogic) + const { currentOnboardingStepNumber, shouldShowBillingStep } = useValues(onboardingLogic) + const { setAllOnboardingSteps } = useActions(onboardingLogic) const { product } = useValues(onboardingLogic) const [allSteps, setAllSteps] = useState([]) @@ -28,7 +31,10 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele }, [children]) useEffect(() => { - setTotalOnboardingSteps(allSteps.length) + if (!allSteps.length) { + return + } + setAllOnboardingSteps(allSteps) }, [allSteps]) if (!product || !children) { @@ -37,24 +43,26 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele const createAllSteps = (): void => { const ProductIntro = + let steps = [] if (Array.isArray(children)) { - setAllSteps([ProductIntro, ...children]) + steps = [ProductIntro, ...children] } else { - setAllSteps([ProductIntro, children as JSX.Element]) + steps = [ProductIntro, children as JSX.Element] + } + if (shouldShowBillingStep) { + const BillingStep = + steps = [...steps, BillingStep] } - setTotalOnboardingSteps(Array.isArray(children) ? children.length : 1) + setAllSteps(steps) } - return (allSteps[onboardingStep - 1] as JSX.Element) || <> + return (allSteps[currentOnboardingStepNumber - 1] as JSX.Element) || <> } const ProductAnalyticsOnboarding = (): JSX.Element => { return ( - -
my onboarding content
-
) } diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx new file mode 100644 index 0000000000000..510f91ebcdf8e --- /dev/null +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -0,0 +1,73 @@ +import { OnboardingStep } from './OnboardingStep' +import { PlanComparison } from 'scenes/billing/PlanComparison' +import { useActions, useValues } from 'kea' +import { billingLogic } from 'scenes/billing/billingLogic' +import { onboardingLogic } from './onboardingLogic' +import { BillingProductV2Type } from '~/types' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { BillingHero } from 'scenes/billing/BillingHero' +import { LemonButton } from '@posthog/lemon-ui' +import { getUpgradeProductLink } from 'scenes/billing/billing-utils' +import { billingProductLogic } from 'scenes/billing/billingProductLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' +import { StarHog } from 'lib/components/hedgehogs' + +export const OnboardingBillingStep = ({ product }: { product: BillingProductV2Type }): JSX.Element => { + const { billing, redirectPath } = useValues(billingLogic) + const { productKey } = useValues(onboardingLogic) + const { currentAndUpgradePlans } = useValues(billingProductLogic({ product })) + const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) + const plan = currentAndUpgradePlans?.upgradePlan + + return ( + { + reportBillingUpgradeClicked(product.type) + }} + > + Upgrade to paid +
+ ) + } + > + {billing?.products && productKey && product ? ( +
+ {product.subscribed ? ( +
+
+
+ +
+

Subscribe successful

+

You're all ready to use PostHog.

+
+
+
+ +
+
+
+ ) : ( + <> + + + + )} +
+ ) : ( + + )} + + ) +} diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx index 32f3295caaf7e..144f8d2d82128 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx @@ -12,8 +12,8 @@ import { urls } from 'scenes/urls' export const OnboardingProductIntro = ({ product }: { product: BillingProductV2Type }): JSX.Element => { const { currentAndUpgradePlans, isPricingModalOpen } = useValues(billingProductLogic({ product })) const { toggleIsPricingModalOpen } = useActions(billingProductLogic({ product })) - const { incrementOnboardingStep } = useActions(onboardingLogic) - const upgradePlan = currentAndUpgradePlans?.upgradePlan + const { setCurrentOnboardingStepNumber } = useActions(onboardingLogic) + const { currentOnboardingStepNumber } = useValues(onboardingLogic) const pricingBenefits = [ 'Only pay for what you use', @@ -27,6 +27,9 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T const productPageUrl = 'https://posthog.com/' + productWebsiteKey const productImageUrl = `https://posthog.com/images/product/${productWebsiteKey}-product.png` + const upgradePlan = currentAndUpgradePlans?.upgradePlan + const plan = upgradePlan ? upgradePlan : currentAndUpgradePlans?.currentPlan + return (
@@ -47,7 +50,10 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T

{product.name}

{product.description}

- + setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)} + > Get started {product.docs_url && ( @@ -66,7 +72,7 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T

Features

- {upgradePlan?.features?.map((feature, i) => ( + {plan?.features?.map((feature, i) => (
  • @@ -82,25 +88,23 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T

    Pricing

    - {upgradePlan?.tiers?.[0].unit_amount_usd && - parseInt(upgradePlan?.tiers?.[0].unit_amount_usd) === 0 && ( -

    - - First {convertLargeNumberToWords(upgradePlan?.tiers?.[0].up_to, null)}{' '} - {product.unit}s free - - , then ${upgradePlan?.tiers?.[1].unit_amount_usd} - /{product.unit}.{' '} - { - toggleIsPricingModalOpen() - }} - > - Volume discounts - {' '} - after {convertLargeNumberToWords(upgradePlan?.tiers?.[1].up_to, null)}/mo. -

    - )} + {plan?.tiers?.[0].unit_amount_usd && parseInt(plan?.tiers?.[0].unit_amount_usd) === 0 && ( +

    + + First {convertLargeNumberToWords(plan?.tiers?.[0].up_to, null)} {product.unit}s free + + , then ${plan?.tiers?.[1].unit_amount_usd} + /{product.unit}.{' '} + { + toggleIsPricingModalOpen() + }} + > + Volume discounts + {' '} + after {convertLargeNumberToWords(plan?.tiers?.[1].up_to, null)}/mo. +

    + )}
      {pricingBenefits.map((benefit, i) => (
    • @@ -136,7 +140,7 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T modalOpen={isPricingModalOpen} onClose={toggleIsPricingModalOpen} product={product} - planKey={upgradePlan?.plan_key} + planKey={plan?.plan_key} />
    ) diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index 291d712faba00..12d523eb78330 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -8,13 +8,17 @@ export const OnboardingStep = ({ title, subtitle, children, + showSkip = false, + continueOverride, }: { title: string subtitle?: string children: React.ReactNode + showSkip?: boolean + continueOverride?: JSX.Element }): JSX.Element => { - const { onboardingStep, totalOnboardingSteps } = useValues(onboardingLogic) - const { incrementOnboardingStep, decrementOnboardingStep, completeOnboarding } = useActions(onboardingLogic) + const { currentOnboardingStepNumber, totalOnboardingSteps } = useValues(onboardingLogic) + const { setCurrentOnboardingStepNumber, completeOnboarding } = useActions(onboardingLogic) return ( 1 && ( + currentOnboardingStepNumber > 1 && (
    - } onClick={decrementOnboardingStep}> + } + onClick={() => setCurrentOnboardingStepNumber(currentOnboardingStepNumber - 1)} + > Back
    ) } > -
    +

    {title}

    {subtitle}

    {children}
    - - onboardingStep == totalOnboardingSteps ? completeOnboarding() : incrementOnboardingStep() - } - sideIcon={onboardingStep !== totalOnboardingSteps ? : null} - > - {onboardingStep == totalOnboardingSteps ? 'Finish' : 'Continue'} - + {showSkip && ( + + currentOnboardingStepNumber == totalOnboardingSteps + ? completeOnboarding() + : setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) + } + status="muted" + > + Skip for now + + )} + {continueOverride ? ( + continueOverride + ) : ( + + currentOnboardingStepNumber == totalOnboardingSteps + ? completeOnboarding() + : setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) + } + sideIcon={currentOnboardingStepNumber !== totalOnboardingSteps ? : null} + > + {currentOnboardingStepNumber == totalOnboardingSteps ? 'Finish' : 'Continue'} + + )}
    diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index 584ebee9cf076..104779a1f4da4 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -8,6 +8,7 @@ import { billingLogic } from 'scenes/billing/billingLogic' export interface OnboardingLogicProps { productKey: ProductKey | null } +export type AllOnboardingSteps = JSX.Element[] export const onboardingLogic = kea({ props: {} as OnboardingLogicProps, @@ -19,11 +20,11 @@ export const onboardingLogic = kea({ actions: { setProduct: (product: BillingProductV2Type | null) => ({ product }), setProductKey: (productKey: string | null) => ({ productKey }), - setOnboardingStep: (onboardingStep: number) => ({ onboardingStep }), - incrementOnboardingStep: true, - decrementOnboardingStep: true, - setTotalOnboardingSteps: (totalOnboardingSteps: number) => ({ totalOnboardingSteps }), + setCurrentOnboardingStepNumber: (currentOnboardingStepNumber: number) => ({ currentOnboardingStepNumber }), completeOnboarding: true, + setAllOnboardingSteps: (allOnboardingSteps: AllOnboardingSteps) => ({ allOnboardingSteps }), + setStepKey: (stepKey: string) => ({ stepKey }), + setSubscribedDuringOnboarding: (subscribedDuringOnboarding) => ({ subscribedDuringOnboarding }), }, reducers: () => ({ productKey: [ @@ -38,18 +39,16 @@ export const onboardingLogic = kea({ setProduct: (_, { product }) => product, }, ], - onboardingStep: [ + currentOnboardingStepNumber: [ 1, { - setOnboardingStep: (_, { onboardingStep }) => onboardingStep, - incrementOnboardingStep: (state) => state + 1, - decrementOnboardingStep: (state) => state - 1, + setCurrentOnboardingStepNumber: (_, { currentOnboardingStepNumber }) => currentOnboardingStepNumber, }, ], - totalOnboardingSteps: [ - 1, + allOnboardingSteps: [ + [] as AllOnboardingSteps, { - setTotalOnboardingSteps: (_, { totalOnboardingSteps }) => totalOnboardingSteps, + setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps as AllOnboardingSteps, }, ], onCompleteOnbardingRedirectUrl: [ @@ -69,7 +68,26 @@ export const onboardingLogic = kea({ }, }, ], + subscribedDuringOnboarding: [ + false, + { + setSubscribedDuringOnboarding: (_, { subscribedDuringOnboarding }) => subscribedDuringOnboarding, + }, + ], }), + selectors: { + totalOnboardingSteps: [ + (s) => [s.allOnboardingSteps], + (allOnboardingSteps: AllOnboardingSteps) => allOnboardingSteps.length, + ], + shouldShowBillingStep: [ + (s) => [s.product, s.subscribedDuringOnboarding], + (product: BillingProductV2Type | null, subscribedDuringOnboarding: boolean) => { + const hasAllAddons = product?.addons?.every((addon) => addon.subscribed) + return !product?.subscribed || !hasAllAddons || subscribedDuringOnboarding + }, + ], + }, listeners: ({ actions, values }) => ({ loadBillingSuccess: () => { actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) @@ -94,13 +112,16 @@ export const onboardingLogic = kea({ }, }), urlToAction: ({ actions }) => ({ - '/onboarding/:productKey': ({ productKey }) => { + '/onboarding/:productKey': ({ productKey }, { success, upgraded }) => { if (!productKey) { window.location.href = urls.default() return } + if (success || upgraded) { + actions.setSubscribedDuringOnboarding(true) + } actions.setProductKey(productKey) - actions.setOnboardingStep(1) + actions.setCurrentOnboardingStepNumber(1) }, }), }) diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index e56ead940ff64..1b155f0974394 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -87,7 +87,7 @@ export function Products(): JSX.Element { return (
    -

    Let's get started.

    +

    Pick your first product.

    Pick your first product to get started with. You can set up any others you'd like later.

    @@ -95,7 +95,7 @@ export function Products(): JSX.Element { {products.length > 0 ? (
    {products - .filter((product) => !product.contact_support) + .filter((product) => !product.contact_support && !product.inclusion_only) .map((product) => ( ))}