Skip to content

Commit

Permalink
feat: onboarding steps, navigation, & sdk logic (#17253)
Browse files Browse the repository at this point in the history
  • Loading branch information
raquelmsmith authored Aug 31, 2023
1 parent a2d7c18 commit 62b43f9
Show file tree
Hide file tree
Showing 50 changed files with 2,775 additions and 127 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions frontend/src/lib/lemon-ui/LemonCard/LemonCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ export interface LemonCardProps {
hoverEffect?: boolean
className?: string
children?: React.ReactNode
onClick?: () => void
focused?: boolean
}

export function LemonCard({ hoverEffect = true, className, children }: LemonCardProps): JSX.Element {
export function LemonCard({ hoverEffect = true, className, children, onClick, focused }: LemonCardProps): JSX.Element {
return (
<div
className={`LemonCard ${
hoverEffect && 'LemonCard--hoverEffect'
} border border-border rounded-lg p-6 bg-white ${className}`}
className={`LemonCard ${hoverEffect && 'LemonCard--hoverEffect'} border ${
focused ? 'border-2 border-primary' : 'border-border'
} rounded-lg p-6 bg-white ${className}`}
onClick={onClick}
>
{children}
</div>
Expand Down
186 changes: 63 additions & 123 deletions frontend/src/scenes/onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,141 +4,81 @@ import { useEffect } from 'react'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { urls } from 'scenes/urls'
import { LemonButton, Link } from '@posthog/lemon-ui'
import { onboardingLogic } from './onboardingLogic'
import { billingProductLogic } from 'scenes/billing/billingProductLogic'
import { convertLargeNumberToWords } from 'scenes/billing/billing-utils'
import { BillingProductV2Type } from '~/types'
import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard'
import { ProductPricingModal } from 'scenes/billing/ProductPricingModal'
import { IconCheckCircleOutline, IconOpenInNew } from 'lib/lemon-ui/icons'
import { SDKs } from './sdks/SDKs'
import { OnboardingProductIntro } from './OnboardingProductIntro'
import { OnboardingStep } from './OnboardingStep'
import { ProductKey } from '~/types'

export const scene: SceneExport = {
component: Onboarding,
logic: onboardingLogic,
}

const OnboardingProductIntro = ({ product }: { product: BillingProductV2Type }): JSX.Element => {
const { currentAndUpgradePlans, isPricingModalOpen } = useValues(billingProductLogic({ product }))
const { toggleIsPricingModalOpen } = useActions(billingProductLogic({ product }))
const upgradePlan = currentAndUpgradePlans?.upgradePlan
const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => {
const { onboardingStep } = useValues(onboardingLogic)
const { setTotalOnboardingSteps } = useActions(onboardingLogic)

const pricingBenefits = [
'Only pay for what you use',
'Control spend with billing limits as low as $0/mo',
'Generous free volume every month, forever',
]
useEffect(() => {
setTotalOnboardingSteps(Array.isArray(children) ? children.length : 1)
}, [children])

if (!Array.isArray(children)) {
return children as JSX.Element
}
return children ? (children[onboardingStep - 1] as JSX.Element) : <></>
}

const ProductAnalyticsOnboarding = (): JSX.Element => {
const { product } = useValues(onboardingLogic)

return product ? (
<OnboardingWrapper>
<OnboardingProductIntro product={product} />
<SDKs usersAction="collecting events" />
<OnboardingStep title="my onboarding step" subtitle="my onboarding subtitle">
<div>my onboarding content</div>
</OnboardingStep>
</OnboardingWrapper>
) : (
<></>
)
}
const SessionReplayOnboarding = (): JSX.Element => {
const { product } = useValues(onboardingLogic)

const productWebsiteKey = product.type.replace('_', '-')
const communityUrl = 'https://posthog.com/questions/topic/' + productWebsiteKey
const tutorialsUrl = 'https://posthog.com/tutorials/categories/' + productWebsiteKey
const productPageUrl = 'https://posthog.com/' + productWebsiteKey
const productImageUrl = `https://posthog.com/images/product/${productWebsiteKey}-product.png`
return product ? (
<OnboardingWrapper>
<OnboardingProductIntro product={product} />
<SDKs usersAction="recording sessions" />
</OnboardingWrapper>
) : (
<></>
)
}
const FeatureFlagsOnboarding = (): JSX.Element => {
const { product } = useValues(onboardingLogic)

return (
<div className="w-full">
<div className="flex flex-col w-full p-6 bg-mid items-center justify-center">
<div className="max-w-lg flex flex-wrap my-8 items-center">
<div className="w-1/2 pr-6 min-w-80">
<h1 className="text-5xl font-bold">{product.name}</h1>
<h2 className="font-bold mb-6">{product.description}</h2>
<div className="flex gap-x-2">
<LemonButton type="primary">Get started</LemonButton>
{product.docs_url && (
<LemonButton type="secondary" to={productPageUrl}>
Learn more
</LemonButton>
)}
</div>
</div>
<div className="shrink w-1/2 min-w-80">
<img src={productImageUrl} className="w-full" />
</div>
</div>
</div>
<div className="my-12 flex justify-between mx-auto max-w-lg gap-x-8">
<div className="flex flex-col">
<h2 className="text-3xl">Features</h2>
<div className="flex flex-wrap gap-y-4 my-6 max-w-lg">
{upgradePlan.features.map((feature, i) => (
<li className="flex mb-2" key={`product-features-${i}`}>
<div>
<IconCheckCircleOutline className="text-success mr-2 mt-1 w-6" />
</div>
<div>
<h4 className="font-bold mb-0">{feature.name}</h4>
<p className="m-0">{feature.description}</p>
</div>
</li>
))}
</div>
</div>
<div>
<LemonCard hoverEffect={false}>
<h2 className="text-3xl">Pricing</h2>
<p>
{upgradePlan?.tiers?.[0].unit_amount_usd &&
parseInt(upgradePlan?.tiers?.[0].unit_amount_usd) === 0 && (
<p className="ml-0 mb-0 mt-4">
<span className="font-bold">
First {convertLargeNumberToWords(upgradePlan?.tiers?.[0].up_to, null)}{' '}
{product.unit}s free
</span>
, then{' '}
<span className="font-bold">${upgradePlan?.tiers?.[1].unit_amount_usd}</span>
<span className="text-muted">/{product.unit}</span>.{' '}
<Link
onClick={() => {
toggleIsPricingModalOpen()
}}
>
<span className="font-bold text-brand-red">Volume discounts</span>
</Link>{' '}
after {convertLargeNumberToWords(upgradePlan?.tiers?.[1].up_to, null)}/mo.
</p>
)}
</p>
<ul>
{pricingBenefits.map((benefit, i) => (
<li className="flex mb-2 ml-6" key={`pricing-benefits-${i}`}>
<IconCheckCircleOutline className="text-success mr-2 mt-1" />
{benefit}
</li>
))}
</ul>
</LemonCard>
<LemonCard className="mt-8" hoverEffect={false}>
<h2 className="text-3xl">Resources</h2>
{product.docs_url && (
<p>
<Link to={product.docs_url}>
Documentation <IconOpenInNew />
</Link>
</p>
)}
<p>
<Link to={communityUrl}>
Community forum <IconOpenInNew />
</Link>
</p>
<p>
<Link to={tutorialsUrl}>
Tutorials <IconOpenInNew />
</Link>
</p>
</LemonCard>
</div>
</div>
<ProductPricingModal
modalOpen={isPricingModalOpen}
onClose={toggleIsPricingModalOpen}
product={product}
planKey={upgradePlan.plan_key}
/>
</div>
return product ? (
<OnboardingWrapper>
<OnboardingProductIntro product={product} />
<SDKs usersAction="loading flags" />
</OnboardingWrapper>
) : (
<></>
)
}

const getOnboarding = (productKey: string): JSX.Element => {
const onboardingViews = {
[ProductKey.PRODUCT_ANALYTICS]: ProductAnalyticsOnboarding,
[ProductKey.SESSION_REPLAY]: SessionReplayOnboarding,
[ProductKey.FEATURE_FLAGS]: FeatureFlagsOnboarding,
}
const OnboardingView = onboardingViews[productKey]
return OnboardingView ? <OnboardingView /> : <></>
}

export function Onboarding(): JSX.Element | null {
const { featureFlags } = useValues(featureFlagLogic)
const { product } = useValues(onboardingLogic)
Expand All @@ -149,5 +89,5 @@ export function Onboarding(): JSX.Element | null {
}
}, [])

return product ? <OnboardingProductIntro product={product} /> : null
return product ? getOnboarding(product.type) : null
}
130 changes: 130 additions & 0 deletions frontend/src/scenes/onboarding/OnboardingProductIntro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useActions, useValues } from 'kea'
import { LemonButton, Link } from '@posthog/lemon-ui'
import { onboardingLogic } from './onboardingLogic'
import { billingProductLogic } from 'scenes/billing/billingProductLogic'
import { convertLargeNumberToWords } from 'scenes/billing/billing-utils'
import { BillingProductV2Type } from '~/types'
import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard'
import { ProductPricingModal } from 'scenes/billing/ProductPricingModal'
import { IconCheckCircleOutline, IconOpenInNew } from 'lib/lemon-ui/icons'

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 pricingBenefits = [
'Only pay for what you use',
'Control spend with billing limits as low as $0/mo',
'Generous free volume every month, forever',
]

const productWebsiteKey = product.type.replace('_', '-')
const communityUrl = 'https://posthog.com/questions/topic/' + productWebsiteKey
const tutorialsUrl = 'https://posthog.com/tutorials/categories/' + productWebsiteKey
const productPageUrl = 'https://posthog.com/' + productWebsiteKey
const productImageUrl = `https://posthog.com/images/product/${productWebsiteKey}-product.png`

return (
<div className="w-full">
<div className="flex flex-col w-full p-6 bg-mid items-center justify-center">
<div className="max-w-lg flex flex-wrap my-8 items-center">
<div className="w-1/2 pr-6 min-w-80">
<h1 className="text-5xl font-bold">{product.name}</h1>
<h2 className="font-bold mb-6">{product.description}</h2>
<div className="flex gap-x-2">
<LemonButton type="primary" onClick={incrementOnboardingStep}>
Get started
</LemonButton>
{product.docs_url && (
<LemonButton type="secondary" to={productPageUrl}>
Learn more
</LemonButton>
)}
</div>
</div>
<div className="shrink w-1/2 min-w-80">
<img src={productImageUrl} className="w-full" />
</div>
</div>
</div>
<div className="my-12 flex justify-between mx-auto max-w-lg gap-x-8">
<div className="flex flex-col">
<h2 className="text-3xl">Features</h2>
<div className="flex flex-wrap gap-y-4 my-6 max-w-lg">
{upgradePlan?.features?.map((feature, i) => (
<li className="flex mb-2" key={`product-features-${i}`}>
<div>
<IconCheckCircleOutline className="text-success mr-2 mt-1 w-6" />
</div>
<div>
<h4 className="font-bold mb-0">{feature.name}</h4>
<p className="m-0">{feature.description}</p>
</div>
</li>
))}
</div>
</div>
<div>
<LemonCard hoverEffect={false}>
<h2 className="text-3xl">Pricing</h2>
{upgradePlan?.tiers?.[0].unit_amount_usd &&
parseInt(upgradePlan?.tiers?.[0].unit_amount_usd) === 0 && (
<p className="ml-0 mb-0 mt-4">
<span className="font-bold">
First {convertLargeNumberToWords(upgradePlan?.tiers?.[0].up_to, null)}{' '}
{product.unit}s free
</span>
, then <span className="font-bold">${upgradePlan?.tiers?.[1].unit_amount_usd}</span>
<span className="text-muted">/{product.unit}</span>.{' '}
<Link
onClick={() => {
toggleIsPricingModalOpen()
}}
>
<span className="font-bold text-brand-red">Volume discounts</span>
</Link>{' '}
after {convertLargeNumberToWords(upgradePlan?.tiers?.[1].up_to, null)}/mo.
</p>
)}
<ul>
{pricingBenefits.map((benefit, i) => (
<li className="flex mb-2 ml-6" key={`pricing-benefits-${i}`}>
<IconCheckCircleOutline className="text-success mr-2 mt-1" />
{benefit}
</li>
))}
</ul>
</LemonCard>
<LemonCard className="mt-8" hoverEffect={false}>
<h2 className="text-3xl">Resources</h2>
{product.docs_url && (
<p>
<Link to={product.docs_url}>
Documentation <IconOpenInNew />
</Link>
</p>
)}
<p>
<Link to={communityUrl}>
Community forum <IconOpenInNew />
</Link>
</p>
<p>
<Link to={tutorialsUrl}>
Tutorials <IconOpenInNew />
</Link>
</p>
</LemonCard>
</div>
</div>
<ProductPricingModal
modalOpen={isPricingModalOpen}
onClose={toggleIsPricingModalOpen}
product={product}
planKey={upgradePlan?.plan_key}
/>
</div>
)
}
34 changes: 34 additions & 0 deletions frontend/src/scenes/onboarding/OnboardingStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { LemonButton } from '@posthog/lemon-ui'
import { BridgePage } from 'lib/components/BridgePage/BridgePage'
import { onboardingLogic } from './onboardingLogic'
import { useActions, useValues } from 'kea'

export const OnboardingStep = ({
title,
subtitle,
children,
}: {
title: string
subtitle?: string
children: React.ReactNode
}): JSX.Element => {
const { onboardingStep, totalOnboardingSteps } = useValues(onboardingLogic)
const { incrementOnboardingStep, completeOnboarding } = useActions(onboardingLogic)
return (
<BridgePage view="onboarding-step" noLogo hedgehog={false} fixedWidth={false}>
<div className="max-w-md">
<h1>{title}</h1>
<p>{subtitle}</p>
{children}
<LemonButton
type="primary"
onClick={() =>
onboardingStep == totalOnboardingSteps ? completeOnboarding() : incrementOnboardingStep()
}
>
{onboardingStep == totalOnboardingSteps ? 'Finish' : 'Continue'}
</LemonButton>
</div>
</BridgePage>
)
}
Loading

0 comments on commit 62b43f9

Please sign in to comment.