Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onboarding invite members, other products, verification steps #17615

Merged
merged 14 commits into from
Sep 25, 2023
30 changes: 25 additions & 5 deletions frontend/src/scenes/onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { ProductKey } from '~/types'
import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions'
import { SessionReplaySDKInstructions } from './sdks/session-replay/SessionReplaySDKInstructions'
import { OnboardingBillingStep } from './OnboardingBillingStep'
import { OnboardingOtherProductsStep } from './OnboardingOtherProductsStep'
import { teamLogic } from 'scenes/teamLogic'
import { OnboardingVerificationStep } from './OnboardingVerificationStep'
import { FeatureFlagsSDKInstructions } from './sdks/feature-flags/FeatureFlagsSDKInstructions'

export const scene: SceneExport = {
component: Onboarding,
Expand All @@ -20,7 +24,7 @@ export const scene: SceneExport = {
/**
* Wrapper for custom onboarding content. This automatically includes the product intro and billing step.
*/
const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => {
const OnboardingWrapper = ({ children, onStart }: { children: React.ReactNode; onStart?: () => void }): JSX.Element => {
const { currentOnboardingStepNumber, shouldShowBillingStep } = useValues(onboardingLogic)
const { setAllOnboardingSteps } = useActions(onboardingLogic)
const { product } = useValues(onboardingLogic)
Expand All @@ -42,7 +46,8 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele
}

const createAllSteps = (): void => {
const ProductIntro = <OnboardingProductIntro product={product} />
const ProductIntro = <OnboardingProductIntro product={product} onStart={onStart} />
const OtherProductsStep = <OnboardingOtherProductsStep />
let steps = []
if (Array.isArray(children)) {
steps = [ProductIntro, ...children]
Expand All @@ -53,6 +58,7 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele
const BillingStep = <OnboardingBillingStep product={product} />
steps = [...steps, BillingStep]
}
steps = [...steps, OtherProductsStep]
setAllSteps(steps)
}

Expand All @@ -63,22 +69,36 @@ const ProductAnalyticsOnboarding = (): JSX.Element => {
return (
<OnboardingWrapper>
<SDKs usersAction="collecting events" sdkInstructionMap={ProductAnalyticsSDKInstructions} />
<OnboardingVerificationStep listeningForName="event" teamPropertyToVerify="ingested_event" />
</OnboardingWrapper>
)
}
const SessionReplayOnboarding = (): JSX.Element => {
const { updateCurrentTeam } = useActions(teamLogic)
return (
<OnboardingWrapper>
<OnboardingWrapper
onStart={() => {
updateCurrentTeam({
session_recording_opt_in: true,
capture_console_log_opt_in: true,
capture_performance_opt_in: true,
})
}}
>
<SDKs
usersAction="recording sessions"
sdkInstructionMap={SessionReplaySDKInstructions}
subtitle="Choose the framework your frontend is built on, or use our all-purpose JavaScript library."
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!"
/>
</OnboardingWrapper>
)
}
const FeatureFlagsOnboarding = (): JSX.Element => {
return <OnboardingWrapper>{/* <SDKs usersAction="loading flags" /> */}</OnboardingWrapper>
return (
<OnboardingWrapper>
<SDKs usersAction="loading flags" sdkInstructionMap={FeatureFlagsSDKInstructions} />
</OnboardingWrapper>
)
}

export function Onboarding(): JSX.Element | null {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/onboarding/OnboardingBillingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const OnboardingBillingStep = ({ product }: { product: BillingProductV2Ty
<IconCheckCircleOutline className="text-success text-3xl mb-6" />
<div>
<h3 className="text-lg font-bold mb-1 text-left">Subscribe successful</h3>
<p className="mx-0 mb-0">You're all ready to use PostHog.</p>
<p className="mx-0 mb-0">You're all ready to use {product.name}.</p>
</div>
</div>
<div className="h-20">
Expand Down
54 changes: 54 additions & 0 deletions frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { LemonButton, LemonCard } from '@posthog/lemon-ui'
import { OnboardingStep } from './OnboardingStep'
import { onboardingLogic } from './onboardingLogic'
import { useActions, useValues } from 'kea'
import { urls } from 'scenes/urls'

export const OnboardingOtherProductsStep = (): JSX.Element => {
const { product, suggestedProducts } = useValues(onboardingLogic)
const { completeOnboarding } = useActions(onboardingLogic)
if (suggestedProducts.length === 0) {
completeOnboarding()
}

return (
<OnboardingStep
title={`${product?.name} pairs with...`}
subtitle="The magic in PostHog is having everyting all in one place. Get started with our other products to unlock your product and data superpowers."
showSkip
continueOverride={<></>}
>
<div className="flex flex-col gap-y-6 my-6">
{suggestedProducts?.map((suggestedProduct) => (
<LemonCard
className="flex items-center justify-between"
hoverEffect={false}
key={suggestedProduct.type}
>
<div className="flex items-center">
<div className="mr-4">
<img
src={suggestedProduct.image_url || ''}
alt={suggestedProduct.name}
className="w-8 h-8"
/>
</div>
<div>
<h3 className="font-bold mb-0">{suggestedProduct.name}</h3>
<p className="m-0">{suggestedProduct.description}</p>
</div>
</div>
<div className="justify-self-end min-w-30 flex justify-end">
<LemonButton
type="primary"
onClick={() => completeOnboarding(urls.onboarding(suggestedProduct.type))}
>
Get started
</LemonButton>
</div>
</LemonCard>
))}
</div>
</OnboardingStep>
)
}
13 changes: 11 additions & 2 deletions frontend/src/scenes/onboarding/OnboardingProductIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import { ProductPricingModal } from 'scenes/billing/ProductPricingModal'
import { IconArrowLeft, IconCheckCircleOutline, IconOpenInNew } from 'lib/lemon-ui/icons'
import { urls } from 'scenes/urls'

export const OnboardingProductIntro = ({ product }: { product: BillingProductV2Type }): JSX.Element => {
export const OnboardingProductIntro = ({
product,
onStart,
}: {
product: BillingProductV2Type
onStart?: () => void
}): JSX.Element => {
const { currentAndUpgradePlans, isPricingModalOpen } = useValues(billingProductLogic({ product }))
const { toggleIsPricingModalOpen } = useActions(billingProductLogic({ product }))
const { setCurrentOnboardingStepNumber } = useActions(onboardingLogic)
Expand Down Expand Up @@ -52,7 +58,10 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T
<div className="flex gap-x-2">
<LemonButton
type="primary"
onClick={() => setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)}
onClick={() => {
onStart && onStart()
setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)
}}
>
Get started
</LemonButton>
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/scenes/onboarding/OnboardingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ export const OnboardingStep = ({
subtitle,
children,
showSkip = false,
onSkip,
continueOverride,
}: {
title: string
subtitle?: string
children: React.ReactNode
showSkip?: boolean
onSkip?: () => void
continueOverride?: JSX.Element
}): JSX.Element => {
const { currentOnboardingStepNumber, totalOnboardingSteps } = useValues(onboardingLogic)
const { setCurrentOnboardingStepNumber, completeOnboarding } = useActions(onboardingLogic)
const isLastStep = currentOnboardingStepNumber == totalOnboardingSteps
return (
<BridgePage
view="onboarding-step"
Expand Down Expand Up @@ -46,14 +49,15 @@ export const OnboardingStep = ({
{showSkip && (
<LemonButton
type="tertiary"
onClick={() =>
currentOnboardingStepNumber == totalOnboardingSteps
onClick={() => {
onSkip && onSkip()
isLastStep
? completeOnboarding()
: setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)
}
}}
status="muted"
>
Skip for now
Skip {isLastStep ? 'and finish' : 'for now'}
</LemonButton>
)}
{continueOverride ? (
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Spinner } from '@posthog/lemon-ui'
import { OnboardingStep } from './OnboardingStep'
import { useActions, useValues } from 'kea'
import { teamLogic } from 'scenes/teamLogic'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { useInterval } from 'lib/hooks/useInterval'
import { BlushingHog } from 'lib/components/hedgehogs'
import { capitalizeFirstLetter } from 'lib/utils'

export const OnboardingVerificationStep = ({
listeningForName,
teamPropertyToVerify,
}: {
listeningForName: string
teamPropertyToVerify: string
}): JSX.Element => {
const { loadCurrentTeam } = useActions(teamLogic)
const { currentTeam } = useValues(teamLogic)
const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic)

useInterval(() => {
if (!currentTeam?.[teamPropertyToVerify]) {
loadCurrentTeam()
}
}, 2000)

return !currentTeam?.[teamPropertyToVerify] ? (
<OnboardingStep
title={`Listening for ${listeningForName}s...`}
subtitle={`Once you have integrated the snippet, we will verify the ${listeningForName} was properly received. It can take up to 2 minutes to recieve the ${listeningForName}.`}
showSkip={true}
onSkip={() => {
reportIngestionContinueWithoutVerifying()
}}
continueOverride={<></>}
>
<div className="text-center mt-8">
<Spinner className="text-5xl" />
</div>
</OnboardingStep>
) : (
<OnboardingStep
title={`${capitalizeFirstLetter(listeningForName)}s successfully sent!`}
subtitle={`Your ${listeningForName.toLocaleLowerCase()}s will now be available in PostHog. Use them to unlock your product and data superpowers.`}
>
<div className="w-40 mx-auto">
<BlushingHog className="h-full w-full" />
</div>
</OnboardingStep>
)
}
39 changes: 31 additions & 8 deletions frontend/src/scenes/onboarding/onboardingLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { urls } from 'scenes/urls'

import type { onboardingLogicType } from './onboardingLogicType'
import { billingLogic } from 'scenes/billing/billingLogic'
import { teamLogic } from 'scenes/teamLogic'

export interface OnboardingLogicProps {
productKey: ProductKey | null
Expand All @@ -13,7 +14,8 @@ export enum OnboardingStepKey {
PRODUCT_INTRO = 'product_intro',
SDKS = 'sdks',
BILLING = 'billing',
PAIRS_WITH = 'pairs_with',
OTHER_PRODUCTS = 'other_products',
VERIFY = 'verify',
}

export type OnboardingStepMap = Record<OnboardingStepKey, string>
Expand All @@ -22,7 +24,8 @@ const onboardingStepMap: OnboardingStepMap = {
[OnboardingStepKey.PRODUCT_INTRO]: 'OnboardingProductIntro',
[OnboardingStepKey.SDKS]: 'SDKs',
[OnboardingStepKey.BILLING]: 'OnboardingBillingStep',
[OnboardingStepKey.PAIRS_WITH]: 'OnboardingPairsWithStep',
[OnboardingStepKey.OTHER_PRODUCTS]: 'OnboardingOtherProductsStep',
[OnboardingStepKey.VERIFY]: 'OnboardingVerificationStep',
}

export type AllOnboardingSteps = JSX.Element[]
Expand All @@ -31,17 +34,17 @@ export const onboardingLogic = kea<onboardingLogicType>({
props: {} as OnboardingLogicProps,
path: ['scenes', 'onboarding', 'onboardingLogic'],
connect: {
values: [billingLogic, ['billing']],
actions: [billingLogic, ['loadBillingSuccess']],
values: [billingLogic, ['billing'], teamLogic, ['currentTeam']],
actions: [billingLogic, ['loadBillingSuccess'], teamLogic, ['updateCurrentTeam']],
},
actions: {
setProduct: (product: BillingProductV2Type | null) => ({ product }),
setProductKey: (productKey: string | null) => ({ productKey }),
setCurrentOnboardingStepNumber: (currentOnboardingStepNumber: number) => ({ currentOnboardingStepNumber }),
completeOnboarding: true,
completeOnboarding: (redirectUri?: string) => ({ redirectUri }),
setAllOnboardingSteps: (allOnboardingSteps: AllOnboardingSteps) => ({ allOnboardingSteps }),
setStepKey: (stepKey: string) => ({ stepKey }),
setSubscribedDuringOnboarding: (subscribedDuringOnboarding) => ({ subscribedDuringOnboarding }),
setSubscribedDuringOnboarding: (subscribedDuringOnboarding: boolean) => ({ subscribedDuringOnboarding }),
},
reducers: () => ({
productKey: [
Expand Down Expand Up @@ -110,6 +113,17 @@ export const onboardingLogic = kea<onboardingLogicType>({
return !product?.subscribed || !hasAllAddons || subscribedDuringOnboarding
},
],
suggestedProducts: [
(s) => [s.billing, s.product, s.currentTeam],
(billing, product, currentTeam) =>
billing?.products?.filter(
(p) =>
p.type !== product?.type &&
!p.contact_support &&
!p.inclusion_only &&
!currentTeam?.has_completed_onboarding_for?.[p.type]
) || [],
],
},
listeners: ({ actions, values }) => ({
loadBillingSuccess: () => {
Expand All @@ -130,8 +144,17 @@ export const onboardingLogic = kea<onboardingLogicType>({
actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null)
}
},
completeOnboarding: () => {
window.location.href = values.onCompleteOnbardingRedirectUrl
completeOnboarding: ({ redirectUri }) => {
if (values.productKey) {
// update the current team has_completed_onboarding_for field, only writing over the current product
actions.updateCurrentTeam({
has_completed_onboarding_for: {
...values.currentTeam?.has_completed_onboarding_for,
[values.productKey]: true,
},
})
}
window.location.href = redirectUri || values.onCompleteOnbardingRedirectUrl
},
setAllOnboardingSteps: ({ allOnboardingSteps }) => {
// once we have the onboarding steps we need to make sure the step key is valid,
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/scenes/onboarding/sdks/SDKs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemonButton, LemonDivider, LemonSelect } from '@posthog/lemon-ui'
import { LemonButton, LemonCard, LemonDivider, LemonSelect } from '@posthog/lemon-ui'
import { sdksLogic } from './sdksLogic'
import { useActions, useValues } from 'kea'
import { OnboardingStep } from '../OnboardingStep'
Expand All @@ -7,6 +7,7 @@ import { onboardingLogic } from '../onboardingLogic'
import { useEffect } from 'react'
import React from 'react'
import { SDKInstructionsMap } from '~/types'
import { InviteMembersButton } from '~/layout/navigation/TopBar/SitePopover'

export function SDKs({
usersAction,
Expand All @@ -32,7 +33,7 @@ export function SDKs({
>
<LemonDivider className="my-8" />
<div className="flex gap-x-8 mt-8">
<div className={`flex flex-col gap-y-2 flex-wrap gap-x-4 min-w-50`}>
<div className={`flex flex-col gap-y-2 flex-wrap gap-x-4 min-w-50 w-50`}>
{showSourceOptionsSelect && (
<LemonSelect
allowClear
Expand All @@ -58,6 +59,11 @@ export function SDKs({
</LemonButton>
</React.Fragment>
))}
<LemonCard className="mt-6" hoverEffect={false}>
<h3 className="font-bold">Need help with this step?</h3>
<p>Invite a team member to help you get set up.</p>
<InviteMembersButton type="primary" />
</LemonCard>
</div>
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && (
<div className="shrink min-w-8">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SDKInstructionsMap, SDKKey } from '~/types'
import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.'

export const FeatureFlagsSDKInstructions: SDKInstructionsMap = {
[SDKKey.JS_WEB]: JSWebInstructions,
[SDKKey.NEXT_JS]: NextJSInstructions,
[SDKKey.REACT]: ReactInstructions,
}
3 changes: 3 additions & 0 deletions frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './js-web'
export * from './next-js'
export * from './react'
Loading
Loading