diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png index 8f769d24dc1a0..b68d610395e31 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 c5a5cc8a7d4c5..8fe71071cbcc3 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/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--dark.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--dark.png index 8f769d24dc1a0..2214ff0ec0e37 100644 Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--dark.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--light.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--light.png index c5a5cc8a7d4c5..06ff726857eb4 100644 Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--light.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-sd-ks--light.png differ diff --git a/frontend/__snapshots__/scenes-other-products--products--dark.png b/frontend/__snapshots__/scenes-other-products--products--dark.png index 6cdcd36531653..30670624e71af 100644 Binary files a/frontend/__snapshots__/scenes-other-products--products--dark.png and b/frontend/__snapshots__/scenes-other-products--products--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-products--products--light.png b/frontend/__snapshots__/scenes-other-products--products--light.png index 0fda7efecf001..5716d63c6b0a4 100644 Binary files a/frontend/__snapshots__/scenes-other-products--products--light.png and b/frontend/__snapshots__/scenes-other-products--products--light.png differ diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 67b2f4ae1920b..f0a82d060b19c 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -6,6 +6,7 @@ import { IconGraph, IconHome, IconLive, + IconLogomark, IconNotebook, IconPeople, IconPieChart, @@ -58,7 +59,14 @@ export const navigation3000Logic = kea([ path(['layout', 'navigation-3000', 'navigationLogic']), props({} as { inputElement?: HTMLInputElement | null }), connect(() => ({ - values: [sceneLogic, ['sceneConfig'], navigationLogic, ['mobileLayout'], teamLogic, ['currentTeam']], + values: [ + sceneLogic, + ['sceneConfig'], + navigationLogic, + ['mobileLayout'], + teamLogic, + ['currentTeam', 'hasOnboardedAnyProduct'], + ], actions: [navigationLogic, ['closeAccountPopover']], })), actions({ @@ -324,76 +332,95 @@ export const navigation3000Logic = kea([ dashboardsModel.selectors.dashboardsLoading, dashboardsModel.selectors.pinnedDashboards, s.currentTeam, + s.hasOnboardedAnyProduct, ], - (featureFlags, dashboardsLoading, pinnedDashboards, currentTeam): NavbarItem[][] => { + ( + featureFlags, + dashboardsLoading, + pinnedDashboards, + currentTeam, + hasOnboardedAnyProduct + ): NavbarItem[][] => { const isUsingSidebar = featureFlags[FEATURE_FLAGS.POSTHOG_3000_NAV] const hasOnboardedFeatureFlags = currentTeam?.has_completed_onboarding_for?.[ProductKey.FEATURE_FLAGS] - return [ - [ - { - identifier: Scene.ProjectHomepage, - label: 'Home', - icon: , - to: urls.projectHomepage(), - }, - { - identifier: Scene.Dashboards, - label: 'Dashboards', - icon: , - logic: isUsingSidebar ? dashboardsSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.dashboards(), - sideAction: { - identifier: 'pinned-dashboards-dropdown', - dropdown: { - overlay: ( - ({ - label: dashboard.name, - to: urls.dashboard(dashboard.id), - })), - footer: dashboardsLoading && ( -
- Loading… -
- ), - }, - ]} - /> - ), - placement: 'bottom-end', - }, + const sectionOne: NavbarItem[] = [ + { + identifier: Scene.Dashboards, + label: 'Dashboards', + icon: , + logic: isUsingSidebar ? dashboardsSidebarLogic : undefined, + to: isUsingSidebar ? undefined : urls.dashboards(), + sideAction: { + identifier: 'pinned-dashboards-dropdown', + dropdown: { + overlay: ( + ({ + label: dashboard.name, + to: urls.dashboard(dashboard.id), + })), + footer: dashboardsLoading && ( +
+ Loading… +
+ ), + }, + ]} + /> + ), + placement: 'bottom-end', }, }, - { - identifier: Scene.Notebooks, - label: 'Notebooks', - icon: , - to: urls.notebooks(), - }, - { - identifier: Scene.DataManagement, - label: 'Data management', - icon: , - logic: isUsingSidebar ? dataManagementSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.eventDefinitions(), - }, - { - identifier: Scene.PersonsManagement, - label: 'People', - icon: , - logic: isUsingSidebar ? personsAndGroupsSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.persons(), - }, - { - identifier: Scene.Events, - label: 'Activity', - icon: , - to: urls.events(), - }, - ], + }, + { + identifier: Scene.Notebooks, + label: 'Notebooks', + icon: , + to: urls.notebooks(), + }, + { + identifier: Scene.DataManagement, + label: 'Data management', + icon: , + logic: isUsingSidebar ? dataManagementSidebarLogic : undefined, + to: isUsingSidebar ? undefined : urls.eventDefinitions(), + }, + { + identifier: Scene.PersonsManagement, + label: 'People', + icon: , + logic: isUsingSidebar ? personsAndGroupsSidebarLogic : undefined, + to: isUsingSidebar ? undefined : urls.persons(), + }, + { + identifier: Scene.Events, + label: 'Activity', + icon: , + to: urls.events(), + }, + ] + + sectionOne.unshift( + hasOnboardedAnyProduct + ? { + identifier: Scene.ProjectHomepage, + label: 'Home', + icon: , + to: urls.projectHomepage(), + } + : { + identifier: Scene.Products, + label: 'Welcome to PostHog', + icon: , + to: urls.products(), + } + ) + + return [ + sectionOne, [ { identifier: Scene.SavedInsights, diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index f124ae1959bd3..6c6db272e7ee5 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -70,7 +70,6 @@ export const appScenes: Record any> = { [Scene.Canvas]: () => import('./notebooks/NotebookCanvasScene'), [Scene.Products]: () => import('./products/Products'), [Scene.Onboarding]: () => import('./onboarding/Onboarding'), - [Scene.OnboardingProductIntroduction]: () => import('./onboarding/OnboardingProductIntroduction'), [Scene.Settings]: () => import('./settings/SettingsScene'), [Scene.MoveToPostHogCloud]: () => import('./moveToPostHogCloud/MoveToPostHogCloud'), } diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 496f3d0c6a332..9a7bc99d277d5 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -109,9 +109,9 @@ export const PlanComparison = ({ return null } const fullyFeaturedPlan = plans[plans.length - 1] - const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) - const { redirectPath, billing } = useValues(billingLogic) + const { billing, redirectPath } = useValues(billingLogic) const { width, ref: planComparisonRef } = useResizeObserver() + const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const upgradeButtons = plans?.map((plan) => { return ( @@ -132,7 +132,7 @@ export const PlanComparison = ({ > {plan.current_plan ? 'Current plan' : 'Subscribe'} - {!plan.current_plan && includeAddons && product.addons?.length > 0 && ( + {!plan.current_plan && !plan.free_allocation && includeAddons && product.addons?.length > 0 && (

- {/* Pricing section */} - - - Pricing - - Monthly base price {plans?.map((plan) => ( @@ -177,7 +168,6 @@ export const PlanComparison = ({ ))} - {includeAddons && product.addons?.length > 0 && ( @@ -191,7 +181,6 @@ export const PlanComparison = ({ {getProductTiers(plan, product)} ))} - {includeAddons && product.addons?.map((addon) => { return addon.tiered ? ( @@ -220,22 +209,15 @@ export const PlanComparison = ({ ) : null })} - {upgradeButtons} - -

- {getProductIcon(product.icon_key, 'text-2xl')} - - {product.name} - -
+ +

Product Features:

- {fullyFeaturedPlan?.features?.map((feature, i) => ( ))} - {!billing?.has_active_subscription && ( <> - -

+ +

Included platform features: -

+

{billing?.products diff --git a/frontend/src/scenes/onboarding/Onboarding.stories.tsx b/frontend/src/scenes/onboarding/Onboarding.stories.tsx index ec8cd66fd5d58..4fd3fc2f9e617 100644 --- a/frontend/src/scenes/onboarding/Onboarding.stories.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.stories.tsx @@ -47,7 +47,7 @@ export const _OnboardingSDKs = (): JSX.Element => { useEffect(() => { const product: BillingProductV2Type = billingJson.products[1] as unknown as BillingProductV2Type setProduct(product) - router.actions.push(urls.onboarding(ProductKey.SESSION_REPLAY) + '?step=sdks') + router.actions.push(urls.onboarding(ProductKey.SESSION_REPLAY) + '?step=install') }, []) return } @@ -64,45 +64,8 @@ export const _OnboardingBilling = (): JSX.Element => { const { setProduct } = useActions(onboardingLogic) useEffect(() => { - setProduct(billingJson.products[1] as unknown as BillingProductV2Type) - router.actions.push(urls.onboarding(ProductKey.SESSION_REPLAY, OnboardingStepKey.BILLING)) - }, []) - return -} - -export const _OnboardingOtherProducts = (): JSX.Element => { - useStorybookMocks({ - get: { - '/api/billing-v2/': { - ...billingJson, - }, - }, - }) - - const { setProduct } = useActions(onboardingLogic) - - useEffect(() => { - setProduct(billingJson.products[1] as unknown as BillingProductV2Type) - router.actions.push(urls.onboarding(ProductKey.SESSION_REPLAY, OnboardingStepKey.OTHER_PRODUCTS)) - }, []) - return -} -_OnboardingOtherProducts.tags = ['test-skip'] // FIXME: For some reason this is captured correctly the first time, but then is written over a second time with SDKs view - -export const _OnboardingProductIntroduction = (): JSX.Element => { - useStorybookMocks({ - get: { - '/api/billing-v2/': { - ...billingJson, - }, - }, - }) - - const { setProduct } = useActions(onboardingLogic) - - useEffect(() => { - setProduct(billingJson.products[0]) - router.actions.push(urls.onboardingProductIntroduction(ProductKey.PRODUCT_ANALYTICS)) + setProduct(billingUnsubscribedJson.products[1] as unknown as BillingProductV2Type) + router.actions.push(urls.onboarding(ProductKey.SESSION_REPLAY, OnboardingStepKey.PLANS)) }, []) return } diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 0dc78e6eaaf47..ff20eb02a9513 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -12,7 +12,7 @@ import { OnboardingInviteTeammates } from './OnboardingInviteTeammates' import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' import { OnboardingProductConfiguration } from './OnboardingProductConfiguration' import { ProductConfigOption } from './onboardingProductConfigurationLogic' -import { OnboardingVerificationStep } from './OnboardingVerificationStep' +import { OnboardingProductIntroduction } from './OnboardingProductIntroduction' import { FeatureFlagsSDKInstructions } from './sdks/feature-flags/FeatureFlagsSDKInstructions' import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions' import { SDKs } from './sdks/SDKs' @@ -28,9 +28,8 @@ export const scene: SceneExport = { * Wrapper for custom onboarding content. This automatically includes billing, other products, and invite steps. */ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => { - const { currentOnboardingStep, shouldShowBillingStep } = useValues(onboardingLogic) + const { currentOnboardingStep, shouldShowBillingStep, product, includeIntro } = useValues(onboardingLogic) const { setAllOnboardingSteps } = useActions(onboardingLogic) - const { product } = useValues(onboardingLogic) const [allSteps, setAllSteps] = useState([]) useEffect(() => { @@ -55,8 +54,12 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele } else { steps = [children as JSX.Element] } + if (includeIntro) { + const IntroStep = + steps = [IntroStep, ...steps] + } if (shouldShowBillingStep) { - const BillingStep = + const BillingStep = steps = [...steps, BillingStep] } const inviteTeammatesStep = @@ -75,12 +78,7 @@ const ProductAnalyticsOnboarding = (): JSX.Element => { - { { title: 'Autocapture frontend interactions', description: `If you use our JavaScript or React Native libraries, we'll automagically - capture frontend interactions like pageviews, clicks, and more. Fine-tune what you - capture directly in your code snippet.`, + capture frontend interactions like pageviews, clicks, and more. Fine-tune what you + capture directly in your code snippet.`, teamProperty: 'autocapture_opt_out', value: !currentTeam?.autocapture_opt_out, type: 'toggle', @@ -139,7 +137,7 @@ const SessionReplayOnboarding = (): JSX.Element => { usersAction="recording sessions" sdkInstructionMap={SessionReplaySDKInstructions} subtitle="Choose the framework your frontend is built on, or use our all-purpose JavaScript library. If you already have the snippet installed, you can skip this step!" - stepKey={OnboardingStepKey.SDKS} + stepKey={OnboardingStepKey.INSTALL} /> @@ -152,7 +150,7 @@ const FeatureFlagsOnboarding = (): JSX.Element => { usersAction="loading flags & experiments" sdkInstructionMap={FeatureFlagsSDKInstructions} subtitle="Choose the framework where you want to use feature flags and/or run experiments, or use our all-purpose JavaScript library. If you already have the snippet installed, you can skip this step!" - stepKey={OnboardingStepKey.SDKS} + stepKey={OnboardingStepKey.INSTALL} /> ) @@ -165,7 +163,7 @@ const SurveysOnboarding = (): JSX.Element => { usersAction="taking surveys" sdkInstructionMap={SurveysSDKInstructions} subtitle="Choose the framework your frontend is built on, or use our all-purpose JavaScript library. If you already have the snippet installed, you can skip this step!" - stepKey={OnboardingStepKey.SDKS} + stepKey={OnboardingStepKey.INSTALL} /> ) diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index 18b3c77554ae2..5967b4843b632 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -4,6 +4,7 @@ import { StarHog } from 'lib/components/hedgehogs' import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { useState } from 'react' import { getUpgradeProductLink } from 'scenes/billing/billing-utils' import { BillingHero } from 'scenes/billing/BillingHero' import { billingLogic } from 'scenes/billing/billingLogic' @@ -17,7 +18,7 @@ import { OnboardingStep } from './OnboardingStep' export const OnboardingBillingStep = ({ product, - stepKey = OnboardingStepKey.BILLING, + stepKey = OnboardingStepKey.PLANS, }: { product: BillingProductV2Type stepKey?: OnboardingStepKey @@ -29,9 +30,11 @@ export const OnboardingBillingStep = ({ const plan = currentAndUpgradePlans?.upgradePlan const currentPlan = currentAndUpgradePlans?.currentPlan + const [showPlanComp, setShowPlanComp] = useState(false) + return ( { reportBillingUpgradeClicked(product.type) }} > - Subscribe + Subscribe to Paid Plan ) } > {billing?.products && productKey && product ? (
- {product.subscribed ? ( + {product.subscribed && (
@@ -67,6 +71,9 @@ export const OnboardingBillingStep = ({
+ setShowPlanComp(!showPlanComp)}> + {showPlanComp ? 'Hide' : 'Show'} plans + {currentPlan?.initial_billing_limit && (
@@ -77,7 +84,9 @@ export const OnboardingBillingStep = ({
)}
- ) : ( + )} + + {(!product.subscribed || showPlanComp) && ( <> diff --git a/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx b/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx index f97f7a19d0c5f..3704ed1564b26 100644 --- a/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx +++ b/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx @@ -1,5 +1,4 @@ import { useActions, useValues } from 'kea' -import { PhonePairHogs } from 'lib/components/hedgehogs' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { InviteTeamMatesComponent } from 'scenes/settings/organization/InviteModal' @@ -49,7 +48,6 @@ export const OnboardingInviteTeammates = ({ stepKey }: { stepKey: OnboardingStep } continueAction={() => preflight?.email_service_available && invitesToSend[0]?.target_email && diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx index 59f541bf89574..ba8c394f22296 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx @@ -2,22 +2,19 @@ import { IconCheck, IconMap, IconMessage, IconStack } from '@posthog/icons' import { LemonButton, Link, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { WavingHog } from 'lib/components/hedgehogs' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React from 'react' import { convertLargeNumberToWords } from 'scenes/billing/billing-utils' import { billingProductLogic } from 'scenes/billing/billingProductLogic' import { ProductPricingModal } from 'scenes/billing/ProductPricingModal' import { getProductIcon } from 'scenes/products/Products' -import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' import { BillingProductV2Type, BillingV2FeatureType, ProductKey } from '~/types' -import { onboardingLogic } from './onboardingLogic' - -export const scene: SceneExport = { - component: OnboardingProductIntroduction, - logic: onboardingLogic, -} +import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' +import { OnboardingStep } from './OnboardingStep' export const Feature = ({ name, description, images }: BillingV2FeatureType): JSX.Element => { return images ? ( @@ -44,6 +41,8 @@ export const Subfeature = ({ name, description, icon_key }: BillingV2FeatureType } const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.Element => { + const { user } = useValues(userLogic) + const { reportOnboardingProductSelected } = useActions(eventUsageLogic) const cta: Partial> = { [ProductKey.SESSION_REPLAY]: 'Start recording my website', [ProductKey.FEATURE_FLAGS]: 'Create a feature flag or experiment', @@ -53,12 +52,18 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E return (
{ + const includeFirstOnboardingProductOnUserProperties = user?.date_joined + ? new Date(user?.date_joined) > new Date('2024-01-10T00:00:00Z') + : false + reportOnboardingProductSelected(product.type, includeFirstOnboardingProductOnUserProperties) + }} > {cta[product.type] || 'Get started'} @@ -130,7 +135,7 @@ const PricingSection = ({ product }: { product: BillingProductV2Type }): JSX.Ele ) } -export function OnboardingProductIntroduction(): JSX.Element | null { +export function OnboardingProductIntroduction({ stepKey }: { stepKey: OnboardingStepKey }): JSX.Element | null { const { product } = useValues(onboardingLogic) const websiteSlug: Partial> = { [ProductKey.SESSION_REPLAY]: 'session-replay', @@ -140,111 +145,112 @@ export function OnboardingProductIntroduction(): JSX.Element | null { [ProductKey.PRODUCT_ANALYTICS]: 'product-analytics', } - return product ? ( - <> -
-
-
-
-

{product.name}

-

{product.headline}

-

{product.description}

- + return ( + } hideHeader> + {product ? ( +
+
+
+
+

{product.headline}

+

{product.description}

+ +
+ {product.image_url && ( + + )}
- {product.image_url && ( - - )} -
-
- {product.screenshot_url && ( -
-
- + + {product.screenshot_url && ( +
+
+ +
-
- )} + )} -
-
-

Features

-
    - {product.features - .filter((feature) => feature.type == 'primary') - .map((feature, i) => { - return ( - - - - ) - })} -
+
+
+

Features

+
    + {product.features + .filter((feature) => feature.type == 'primary') + .map((feature, i) => { + return ( + + + + ) + })} +
-
    - {product.features - .filter((feature) => feature.type == 'secondary') - .map((subfeature, i) => { - return ( - - - - ) - })} -
-
-

Get the most out of {product.name}

-
    -
  • - - - Product docs - -
  • -
  • - - - Tutorials - -
  • -
  • - - - Community - -
  • +
      + {product.features + .filter((feature) => feature.type == 'secondary') + .map((subfeature, i) => { + return ( + + + + ) + })}
    +
    +

    Get the most out of {product.name}

    +
      +
    • + + + Product docs + +
    • +
    • + + + Tutorials + +
    • +
    • + + + Community + +
    • +
    +
-
-
-
- -
-
-
-
-
-

Get started with {product.name}

-

{product.description}

- +
+
+
-
- +
+
+
+
+

Get started with {product.name}

+

{product.description}

+ +
+
+ +
-
- - ) : ( -
- -
+ ) : ( +
+ +
+ )} + ) } diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index 7c7474850edcd..c1e2c925c6ef3 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -1,16 +1,10 @@ -import { LemonButton } from '@posthog/lemon-ui' +import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { router } from 'kea-router' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { PhonePairHogs } from 'lib/components/hedgehogs' import { supportLogic } from 'lib/components/Support/supportLogic' -import { IconArrowLeft, IconArrowRight } from 'lib/lemon-ui/icons' +import { IconArrowRight, IconChevronRight } from 'lib/lemon-ui/icons' import React from 'react' -import { urls } from 'scenes/urls' -import { ProductKey } from '~/types' - -import { getProductUri, onboardingLogic, OnboardingStepKey } from './onboardingLogic' +import { onboardingLogic, OnboardingStepKey, stepKeyToTitle } from './onboardingLogic' export const OnboardingStep = ({ stepKey, @@ -22,8 +16,7 @@ export const OnboardingStep = ({ onSkip, continueAction, continueOverride, - backActionOverride, - hedgehog, + hideHeader, }: { stepKey: OnboardingStepKey title: string @@ -34,51 +27,58 @@ export const OnboardingStep = ({ onSkip?: () => void continueAction?: () => void continueOverride?: JSX.Element - backActionOverride?: () => void - hedgehog?: JSX.Element + hideHeader?: boolean }): JSX.Element => { - const { hasNextStep, hasPreviousStep, productKey, isFirstProductOnboarding } = useValues(onboardingLogic) - const { completeOnboarding, goToNextStep, goToPreviousStep } = useActions(onboardingLogic) + const { hasNextStep, onboardingStepKeys, currentOnboardingStep } = useValues(onboardingLogic) + const { completeOnboarding, goToNextStep, setStepKey } = useActions(onboardingLogic) const { openSupportForm } = useActions(supportLogic) - const hedgehogToRender = React.cloneElement(hedgehog || , { - className: 'h-full w-full', - }) - if (!stepKey) { throw new Error('stepKey is required in any OnboardingStep') } return ( - - } - onClick={() => - backActionOverride - ? backActionOverride() - : hasPreviousStep - ? goToPreviousStep() - : !isFirstProductOnboarding - ? router.actions.push(getProductUri(productKey as ProductKey)) - : router.actions.push(urls.products()) - } - > - Back - + <> +
+
+

+ {title || stepKeyToTitle(currentOnboardingStep?.props.stepKey)} +

+
+ {onboardingStepKeys.map((stepName, idx) => { + return ( + + setStepKey(stepName)} + > + + {stepKeyToTitle(stepName)} + + + {onboardingStepKeys.length > 1 && idx !== onboardingStepKeys.length - 1 && ( + + )} + + ) + })} +
- } - > -
- {hedgehog &&
{hedgehogToRender}
} - -

{title}

-

{subtitle}

+
+
+ {subtitle &&

{subtitle}

} {children}
{showHelpButton && ( @@ -91,6 +91,7 @@ export const OnboardingStep = ({ )} {showSkip && ( { onSkip && onSkip() !hasNextStep ? completeOnboarding() : goToNextStep() @@ -104,17 +105,18 @@ export const OnboardingStep = ({ ) : ( { continueAction && continueAction() !hasNextStep ? completeOnboarding() : goToNextStep() }} sideIcon={hasNextStep ? : null} > - {!hasNextStep ? 'Finish' : 'Continue'} + {!hasNextStep ? 'Finish' : 'Next'} )}
- + ) } diff --git a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx deleted file mode 100644 index 32688cb85e11c..0000000000000 --- a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Spinner } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { BlushingHog } from 'lib/components/hedgehogs' -import { useInterval } from 'lib/hooks/useInterval' -import { capitalizeFirstLetter } from 'lib/utils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from 'scenes/teamLogic' - -import { OnboardingStepKey } from './onboardingLogic' -import { OnboardingStep } from './OnboardingStep' - -export const OnboardingVerificationStep = ({ - listeningForName, - teamPropertyToVerify, - stepKey = OnboardingStepKey.VERIFY, -}: { - listeningForName: string - teamPropertyToVerify: string - stepKey?: OnboardingStepKey -}): JSX.Element => { - const { loadCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) - - useInterval(() => { - if (!currentTeam?.[teamPropertyToVerify]) { - loadCurrentTeam() - } - }, 2000) - - return !currentTeam?.[teamPropertyToVerify] ? ( - { - reportIngestionContinueWithoutVerifying() - }} - continueOverride={<>} - showHelpButton - > -
- -
-
- ) : ( - -
- -
-
- ) -} diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index 942fe3ced24c4..0f7799cfbd200 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -4,11 +4,12 @@ import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { billingLogic } from 'scenes/billing/billingLogic' +import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { BillingProductV2Type, ProductKey } from '~/types' +import { BillingProductV2Type, Breadcrumb, ProductKey } from '~/types' import type { onboardingLogicType } from './onboardingLogicType' @@ -18,14 +19,44 @@ export interface OnboardingLogicProps { export enum OnboardingStepKey { PRODUCT_INTRO = 'product_intro', - SDKS = 'sdks', - BILLING = 'billing', - OTHER_PRODUCTS = 'other_products', + INSTALL = 'install', + PLANS = 'plans', VERIFY = 'verify', PRODUCT_CONFIGURATION = 'configure', INVITE_TEAMMATES = 'invite_teammates', } +const productKeyToProductName = { + [ProductKey.PRODUCT_ANALYTICS]: 'Product Analytics', + [ProductKey.SESSION_REPLAY]: 'Session Replay', + [ProductKey.FEATURE_FLAGS]: 'Feature Flags', + [ProductKey.SURVEYS]: 'Surveys', +} + +const productKeyToURL = { + [ProductKey.PRODUCT_ANALYTICS]: urls.insights(), + [ProductKey.SESSION_REPLAY]: urls.replay(), + [ProductKey.FEATURE_FLAGS]: urls.featureFlags(), + [ProductKey.SURVEYS]: urls.surveys(), +} + +const productKeyToScene = { + [ProductKey.PRODUCT_ANALYTICS]: Scene.SavedInsights, + [ProductKey.SESSION_REPLAY]: Scene.Replay, + [ProductKey.FEATURE_FLAGS]: Scene.FeatureFlags, + [ProductKey.SURVEYS]: Scene.Surveys, +} + +export const stepKeyToTitle = (stepKey?: OnboardingStepKey): undefined | string => { + return ( + stepKey && + stepKey + .split('_') + .map((part, i) => (i == 0 ? part[0].toUpperCase() + part.substring(1) : part)) + .join(' ') + ) +} + // These types have to be set like this, so that kea typegen is happy export type AllOnboardingSteps = OnboardingStep[] export type OnboardingStep = JSX.Element @@ -70,6 +101,7 @@ export const onboardingLogic = kea([ setAllOnboardingSteps: (allOnboardingSteps: AllOnboardingSteps) => ({ allOnboardingSteps }), setStepKey: (stepKey: OnboardingStepKey) => ({ stepKey }), setSubscribedDuringOnboarding: (subscribedDuringOnboarding: boolean) => ({ subscribedDuringOnboarding }), + setIncludeIntro: (includeIntro: boolean) => ({ includeIntro }), goToNextStep: true, goToPreviousStep: true, resetStepKey: true, @@ -105,8 +137,31 @@ export const onboardingLogic = kea([ setSubscribedDuringOnboarding: (_, { subscribedDuringOnboarding }) => subscribedDuringOnboarding, }, ], + includeIntro: [ + true, + { + setIncludeIntro: (_, { includeIntro }) => includeIntro, + }, + ], })), selectors({ + breadcrumbs: [ + (s) => [s.productKey, s.stepKey], + (productKey, stepKey): Breadcrumb[] => { + return [ + { + key: Scene.Onboarding, + name: productKeyToProductName[productKey ?? ''], + path: productKeyToURL[productKey ?? ''], + }, + { + key: productKeyToScene[productKey ?? ''], + name: stepKeyToTitle(stepKey), + path: urls.onboarding(productKey ?? '', stepKey), + }, + ] + }, + ], onCompleteOnboardingRedirectUrl: [ (s) => [s.featureFlags, s.productKey], (featureFlags: FeatureFlagsSet, productKey: string | null) => { @@ -117,6 +172,12 @@ export const onboardingLogic = kea([ (s) => [s.allOnboardingSteps], (allOnboardingSteps: AllOnboardingSteps) => allOnboardingSteps.length, ], + onboardingStepKeys: [ + (s) => [s.allOnboardingSteps], + (allOnboardingSteps: AllOnboardingSteps) => { + return allOnboardingSteps.map((step) => step.props.stepKey) + }, + ], currentOnboardingStep: [ (s) => [s.allOnboardingSteps, s.stepKey], (allOnboardingSteps: AllOnboardingSteps, stepKey: OnboardingStepKey): OnboardingStep | null => @@ -169,13 +230,6 @@ export const onboardingLogic = kea([ window.location.href = urls.default() } else { actions.resetStepKey() - const includeFirstOnboardingProductOnUserProperties = values.user?.date_joined - ? new Date(values.user?.date_joined) > new Date('2024-01-10T00:00:00Z') - : false - eventUsageLogic.actions.reportOnboardingProductSelected( - product.type, - includeFirstOnboardingProductOnUserProperties - ) switch (product.type) { case ProductKey.PRODUCT_ANALYTICS: return @@ -210,6 +264,7 @@ export const onboardingLogic = kea([ eventUsageLogic.actions.reportSubscribedDuringOnboarding(productKey) } }, + completeOnboarding: ({ nextProductKey }) => { if (values.productKey) { const product = values.productKey @@ -277,15 +332,12 @@ export const onboardingLogic = kea([ }, })), urlToAction(({ actions, values }) => ({ - '/onboarding/:productKey(/:intro)': ({ productKey, intro }, { success, upgraded, step }) => { + '/onboarding/:productKey': ({ productKey }, { success, upgraded, step }) => { if (!productKey) { window.location.href = urls.default() return } - if (intro === 'intro') { - // this prevents us from jumping straight back into onboarding if they are trying to see the intro again - actions.setAllOnboardingSteps([]) - } + if (success || upgraded) { actions.setSubscribedDuringOnboarding(true) } diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 08502e7af1f5f..89b424fd9bae7 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -1,50 +1,55 @@ -import { IconArrowLeft, IconEye } from '@posthog/icons' -import { LemonButton, LemonCard, LemonDivider, LemonSelect, Link, Spinner } from '@posthog/lemon-ui' +import { IconArrowLeft, IconArrowRight, IconCheck } from '@posthog/icons' +import { LemonButton, LemonCard, LemonDivider, LemonSelect, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LaptopHog1 } from 'lib/components/hedgehogs' +import { useInterval } from 'lib/hooks/useInterval' import { useWindowSize } from 'lib/hooks/useWindowSize' import { useEffect } from 'react' import React from 'react' +import { teamLogic } from 'scenes/teamLogic' import { InviteMembersButton } from '~/layout/navigation/TopBar/AccountPopover' -import { ProductKey, SDKInstructionsMap } from '~/types' +import { SDKInstructionsMap } from '~/types' import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' import { OnboardingStep } from '../OnboardingStep' -import { multiInstallProducts, sdksLogic } from './sdksLogic' +import { sdksLogic } from './sdksLogic' import { SDKSnippet } from './SDKSnippet' export function SDKs({ - usersAction, sdkInstructionMap, - subtitle, - stepKey = OnboardingStepKey.SDKS, + stepKey = OnboardingStepKey.INSTALL, + listeningForName = 'event', + teamPropertyToVerify = 'ingested_event', }: { usersAction?: string sdkInstructionMap: SDKInstructionsMap subtitle?: string stepKey?: OnboardingStepKey + listeningForName?: string + teamPropertyToVerify?: string }): JSX.Element { - const { - setSourceFilter, - setSelectedSDK, - setAvailableSDKInstructionsMap, - setShowSideBySide, - setPanel, - setHasSnippetEvents, - } = useActions(sdksLogic) - const { - sourceFilter, - sdks, - selectedSDK, - sourceOptions, - showSourceOptionsSelect, - showSideBySide, - panel, - hasSnippetEvents, - } = useValues(sdksLogic) - const { productKey, product, isFirstProductOnboarding } = useValues(onboardingLogic) + const { loadCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap, setShowSideBySide, setPanel } = + useActions(sdksLogic) + const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect, showSideBySide, panel } = + useValues(sdksLogic) + const { productKey, hasNextStep } = useValues(onboardingLogic) + const { goToNextStep, completeOnboarding } = useActions(onboardingLogic) + const [showListeningFor, setShowListeningFor] = React.useState(false) + const [hasCheckedInstallation, setHasCheckedInstallation] = React.useState(false) + const { width } = useWindowSize() + + useEffect(() => { + if (showListeningFor && !currentTeam?.[teamPropertyToVerify]) { + setHasCheckedInstallation(true) + setTimeout(() => { + setShowListeningFor(false) + }, 5000) + } + }, [showListeningFor]) + const minimumSideBySideSize = 768 useEffect(() => { @@ -55,38 +60,44 @@ export function SDKs({ width && setShowSideBySide(width > minimumSideBySideSize) }, [width]) - return !isFirstProductOnboarding && hasSnippetEvents === null ? ( - }> -
- -
-
- ) : !isFirstProductOnboarding && hasSnippetEvents ? ( - } - > -

{product?.name} works with PostHog.js with no extra installation required. Easy peasy, huh?

- {multiInstallProducts.includes(productKey as ProductKey) && ( -

- Need to install somewhere else?{' '} - setHasSnippetEvents(false)}> - Show SDK instructions - -

- )} -
- ) : ( + useInterval(() => { + if (!currentTeam?.[teamPropertyToVerify]) { + loadCurrentTeam() + } + }, 2000) + + return ( : undefined} - backActionOverride={!showSideBySide && panel === 'instructions' ? () => setPanel('options') : undefined} - hedgehog={} + continueOverride={ +
+ {!showSideBySide && panel === 'options' ? ( + <> + ) : !currentTeam?.[teamPropertyToVerify] ? ( + <> + (!hasNextStep ? completeOnboarding() : goToNextStep())} + > + Skip setup + + + ) : ( + <> + : null} + type="primary" + status="alt" + onClick={() => (!hasNextStep ? completeOnboarding() : goToNextStep())} + > + Continue + + + )} +
+ } > -

Need help with this step?

Invite a team member to help you get set up.

- +
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && ( @@ -145,6 +156,32 @@ export function SDKs({ )} + +

Verify installation

+ {!showListeningFor && !currentTeam?.[teamPropertyToVerify] ? ( + <> + setShowListeningFor(true)}> + Check installation + + {hasCheckedInstallation && !showListeningFor && ( +

+ No {listeningForName}s received. Please check your implementation and try again. +

+ )} + + ) : ( +

+ {!currentTeam?.[teamPropertyToVerify] ? ( + <> + Verifying installation... + + ) : ( + <> + Installation complete + + )} +

+ )}
)}
diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx index ee9c98475dc05..cdaa38dd84697 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx @@ -1,5 +1,3 @@ -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' - import { SDKHtmlSnippetInstructions } from '../sdk-install-instructions/html-snippet' import { SessionReplayFinalSteps } from '../shared-snippets' @@ -7,8 +5,6 @@ export function HTMLSnippetInstructions(): JSX.Element { return ( <> - -

Final steps

) diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx index d465faf1de9cb..855b7834feaed 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx @@ -1,5 +1,3 @@ -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' - import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' import { SessionReplayFinalSteps } from '../shared-snippets' @@ -7,8 +5,6 @@ export function JSWebInstructions(): JSX.Element { return ( <> - -

Final steps

) diff --git a/frontend/src/scenes/onboarding/sdks/shared-snippets.tsx b/frontend/src/scenes/onboarding/sdks/shared-snippets.tsx index 215f9a07693d2..d638c97867922 100644 --- a/frontend/src/scenes/onboarding/sdks/shared-snippets.tsx +++ b/frontend/src/scenes/onboarding/sdks/shared-snippets.tsx @@ -1,16 +1,6 @@ -import { Link } from 'lib/lemon-ui/Link' - export function SessionReplayFinalSteps(): JSX.Element { return ( <> -

Optional: Configure

-

- Advanced users can add{' '} - - configuration options - {' '} - to customize text masking, customize or disable event capturing, and more. -

Create a recording

Visit your site and click around to generate an initial recording.

diff --git a/frontend/src/scenes/onboarding/sdks/surveys/SurveysFinalSteps.tsx b/frontend/src/scenes/onboarding/sdks/surveys/SurveysFinalSteps.tsx deleted file mode 100644 index f0caa15d85c03..0000000000000 --- a/frontend/src/scenes/onboarding/sdks/surveys/SurveysFinalSteps.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const SurveysFinalSteps = (): JSX.Element => { - return ( - <> -

Next up: Create a survey

-

Complete this onboarding flow, then create a survey from one of our templates or from scratch.

- - ) -} diff --git a/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx index 616224d0b2fdc..1b4ba1bf51c6d 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx @@ -1,14 +1,9 @@ -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' - import { SDKHtmlSnippetInstructions } from '../sdk-install-instructions/html-snippet' -import { SurveysFinalSteps } from './SurveysFinalSteps' export function HTMLSnippetInstructions(): JSX.Element { return ( <> - - ) } diff --git a/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx b/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx index d478de9b3dd0b..cc782ae9280a3 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx @@ -1,14 +1,9 @@ -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' - import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' -import { SurveysFinalSteps } from './SurveysFinalSteps' export function JSWebInstructions(): JSX.Element { return ( <> - - ) } diff --git a/frontend/src/scenes/onboarding/sdks/surveys/next-js.tsx b/frontend/src/scenes/onboarding/sdks/surveys/next-js.tsx index 2e7f76fb610ff..a09a8d748411b 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/next-js.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/next-js.tsx @@ -1,11 +1,9 @@ import { SDKInstallNextJSInstructions } from '../sdk-install-instructions/next-js' -import { SurveysFinalSteps } from './SurveysFinalSteps' export function NextJSInstructions(): JSX.Element { return ( <> - ) } diff --git a/frontend/src/scenes/onboarding/sdks/surveys/react.tsx b/frontend/src/scenes/onboarding/sdks/surveys/react.tsx index 88713cbbafbf1..a31d10dfa22e8 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/react.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/react.tsx @@ -1,11 +1,9 @@ import { SDKInstallReactInstructions } from '../sdk-install-instructions/react' -import { SurveysFinalSteps } from './SurveysFinalSteps' export function ReactInstructions(): JSX.Element { return ( <> - ) } diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index d28d43fc80eee..34f2e120a535d 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -1,16 +1,18 @@ import * as Icons from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard' import { Spinner } from 'lib/lemon-ui/Spinner' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { billingLogic } from 'scenes/billing/billingLogic' import { getProductUri, onboardingLogic } from 'scenes/onboarding/onboardingLogic' import { SceneExport } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' import { BillingProductV2Type, ProductKey } from '~/types' @@ -21,10 +23,12 @@ export const scene: SceneExport = { function OnboardingCompletedButton({ productUrl, onboardingUrl, + getStartedActionOverride, }: { productUrl: string onboardingUrl: string productKey: ProductKey + getStartedActionOverride?: () => void }): JSX.Element { return ( <> @@ -34,6 +38,9 @@ function OnboardingCompletedButton({ { + if (getStartedActionOverride) { + getStartedActionOverride() + } router.actions.push(onboardingUrl) }} > @@ -57,9 +64,8 @@ function OnboardingNotCompletedButton({ onClick={() => { if (getStartedActionOverride) { getStartedActionOverride() - } else { - router.actions.push(url) } + router.actions.push(url) }} > Get started @@ -84,6 +90,9 @@ export function ProductCard({ }): JSX.Element { const { currentTeam } = useValues(teamLogic) const { featureFlags } = useValues(featureFlagLogic) + const { setIncludeIntro } = useActions(onboardingLogic) + const { user } = useValues(userLogic) + const { reportOnboardingProductSelected } = useActions(eventUsageLogic) const onboardingCompleted = currentTeam?.has_completed_onboarding_for?.[product.type] const vertical = orientation === 'vertical' @@ -107,13 +116,26 @@ export function ProductCard({ productUrl={getProductUri(product.type as ProductKey, featureFlags)} onboardingUrl={urls.onboarding(product.type)} productKey={product.type as ProductKey} + getStartedActionOverride={() => { + setIncludeIntro(false) + }} /> ) : (
{ + setIncludeIntro(false) + const includeFirstOnboardingProductOnUserProperties = user?.date_joined + ? new Date(user?.date_joined) > new Date('2024-01-10T00:00:00Z') + : false + reportOnboardingProductSelected( + product.type, + includeFirstOnboardingProductOnUserProperties + ) + getStartedActionOverride && getStartedActionOverride() + }} />
)} diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index 73375a8432cc5..a7b6fb6ea0957 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -15,9 +15,11 @@ import { AvailableFeature, ProductKey } from '~/types' import { appContextLogic } from './appContextLogic' import { handleLoginRedirect } from './authentication/loginLogic' +import { OnboardingStepKey } from './onboarding/onboardingLogic' import { organizationLogic } from './organizationLogic' import { preflightLogic } from './PreflightCheck/preflightLogic' import type { sceneLogicType } from './sceneLogicType' +import { inviteLogic } from './settings/organization/inviteLogic' import { teamLogic } from './teamLogic' import { userLogic } from './userLogic' @@ -37,7 +39,7 @@ export const sceneLogic = kea([ path(['scenes', 'sceneLogic']), connect(() => ({ logic: [router, userLogic, preflightLogic, appContextLogic], - actions: [router, ['locationChanged'], commandBarLogic, ['setCommandBar']], + actions: [router, ['locationChanged'], commandBarLogic, ['setCommandBar'], inviteLogic, ['hideInviteModal']], values: [featureFlagLogic, ['featureFlags']], })), actions({ @@ -249,10 +251,7 @@ export const sceneLogic = kea([ !removeProjectIdIfPresent(location.pathname).startsWith(urls.products()) && !removeProjectIdIfPresent(location.pathname).startsWith(urls.settings()) ) { - if ( - !teamLogic.values.currentTeam.completed_snippet_onboarding && - !Object.keys(teamLogic.values.currentTeam.has_completed_onboarding_for || {}).length - ) { + if (!teamLogic.values.hasOnboardedAnyProduct) { console.warn('No onboarding completed, redirecting to /products') router.actions.replace(urls.products()) return @@ -283,7 +282,9 @@ export const sceneLogic = kea([ console.warn( `Onboarding not completed for ${productKeyFromUrl}, redirecting to onboarding intro` ) - router.actions.replace(urls.onboardingProductIntroduction(productKeyFromUrl)) + router.actions.replace( + urls.onboarding(productKeyFromUrl, OnboardingStepKey.PRODUCT_INTRO) + ) return } } diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 2d69a0c17649d..a9e4864a9ca77 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -75,7 +75,6 @@ export enum Scene { Canvas = 'Canvas', Products = 'Products', Onboarding = 'Onboarding', - OnboardingProductIntroduction = 'OnboardingProductIntroduction', Settings = 'Settings', MoveToPostHogCloud = 'MoveToPostHogCloud', } diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index f49e8435d5af6..4ddee395f1729 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -265,15 +265,10 @@ export const sceneConfigurations: Record = { }, [Scene.Products]: { projectBased: true, - layout: 'plain', + hideProjectNotice: true, }, [Scene.Onboarding]: { projectBased: true, - layout: 'plain', - }, - [Scene.OnboardingProductIntroduction]: { - projectBased: true, - name: 'Product introduction', hideProjectNotice: true, }, [Scene.ToolbarLaunch]: { @@ -547,7 +542,6 @@ export const routes: Record = { [urls.passwordResetComplete(':uuid', ':token')]: Scene.PasswordResetComplete, [urls.products()]: Scene.Products, [urls.onboarding(':productKey')]: Scene.Onboarding, - [urls.onboardingProductIntroduction(':productKey')]: Scene.OnboardingProductIntroduction, [urls.verifyEmail()]: Scene.VerifyEmail, [urls.verifyEmail(':uuid')]: Scene.VerifyEmail, [urls.verifyEmail(':uuid', ':token')]: Scene.VerifyEmail, diff --git a/frontend/src/scenes/teamLogic.tsx b/frontend/src/scenes/teamLogic.tsx index 2cbec72f42360..3c0b7268044c3 100644 --- a/frontend/src/scenes/teamLogic.tsx +++ b/frontend/src/scenes/teamLogic.tsx @@ -119,6 +119,19 @@ export const teamLogic = kea([ ], })), selectors(() => ({ + hasOnboardedAnyProduct: [ + (selectors) => [selectors.currentTeam], + (currentTeam): boolean => { + if ( + currentTeam && + !currentTeam.completed_snippet_onboarding && + !Object.keys(currentTeam.has_completed_onboarding_for || {}).length + ) { + return false + } + return true + }, + ], currentTeamId: [ (selectors) => [selectors.currentTeam], (currentTeam): number | null => (currentTeam ? currentTeam.id : null), diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index c884773f30da9..3d7b54a846065 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -173,7 +173,6 @@ export const urls = { products: (): string => '/products', onboarding: (productKey: string, stepKey?: OnboardingStepKey): string => `/onboarding/${productKey}${stepKey ? '?step=' + stepKey : ''}`, - onboardingProductIntroduction: (productKey: string): string => '/onboarding/' + productKey + '/intro', // Cloud only organizationBilling: (): string => '/organization/billing', // Self-hosted only