diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 41b299c417f36..3cf1c4989e4c1 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -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, @@ -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) @@ -42,7 +46,8 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele } const createAllSteps = (): void => { - const ProductIntro = + const ProductIntro = + const OtherProductsStep = let steps = [] if (Array.isArray(children)) { steps = [ProductIntro, ...children] @@ -53,6 +58,7 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele const BillingStep = steps = [...steps, BillingStep] } + steps = [...steps, OtherProductsStep] setAllSteps(steps) } @@ -63,22 +69,36 @@ const ProductAnalyticsOnboarding = (): JSX.Element => { return ( + ) } const SessionReplayOnboarding = (): JSX.Element => { + const { updateCurrentTeam } = useActions(teamLogic) return ( - + { + updateCurrentTeam({ + session_recording_opt_in: true, + capture_console_log_opt_in: true, + capture_performance_opt_in: true, + }) + }} + > ) } const FeatureFlagsOnboarding = (): JSX.Element => { - return {/* */} + return ( + + + + ) } export function Onboarding(): JSX.Element | null { diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index 510f91ebcdf8e..6daba12e33fe6 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -50,7 +50,7 @@ export const OnboardingBillingStep = ({ product }: { product: BillingProductV2Ty

Subscribe successful

-

You're all ready to use PostHog.

+

You're all ready to use {product.name}.

diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx new file mode 100644 index 0000000000000..56102c51a7646 --- /dev/null +++ b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx @@ -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 ( + } + > +
+ {suggestedProducts?.map((suggestedProduct) => ( + +
+
+ {suggestedProduct.name} +
+
+

{suggestedProduct.name}

+

{suggestedProduct.description}

+
+
+
+ completeOnboarding(urls.onboarding(suggestedProduct.type))} + > + Get started + +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx index 144f8d2d82128..4cdb535e242f7 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx @@ -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) @@ -52,7 +58,10 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T
setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)} + onClick={() => { + onStart && onStart() + setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) + }} > Get started diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index 12d523eb78330..b32c9fdc13a3d 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -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 ( - currentOnboardingStepNumber == totalOnboardingSteps + onClick={() => { + onSkip && onSkip() + isLastStep ? completeOnboarding() : setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) - } + }} status="muted" > - Skip for now + Skip {isLastStep ? 'and finish' : 'for now'} )} {continueOverride ? ( diff --git a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx new file mode 100644 index 0000000000000..7b55f2f139bd2 --- /dev/null +++ b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx @@ -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] ? ( + { + reportIngestionContinueWithoutVerifying() + }} + continueOverride={<>} + > +
+ +
+
+ ) : ( + +
+ +
+
+ ) +} diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index c896cff3f396e..3f38d75747981 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -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 @@ -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 @@ -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[] @@ -31,17 +34,17 @@ export const onboardingLogic = kea({ 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: [ @@ -110,6 +113,17 @@ export const onboardingLogic = kea({ 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: () => { @@ -130,8 +144,17 @@ export const onboardingLogic = kea({ 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, diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 737dfceacac1b..9ac4884dd5d40 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -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' @@ -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, @@ -32,7 +33,7 @@ export function SDKs({ >
-
+
{showSourceOptionsSelect && ( ))} + +

Need help with this step?

+

Invite a team member to help you get set up.

+ +
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && (
diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx new file mode 100644 index 0000000000000..6374992792b3e --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx @@ -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, +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx new file mode 100644 index 0000000000000..27d9e5388d04d --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx @@ -0,0 +1,3 @@ +export * from './js-web' +export * from './next-js' +export * from './react' diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx new file mode 100644 index 0000000000000..8ef2865c3b834 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx @@ -0,0 +1,42 @@ +import { JSSnippet } from 'lib/components/JSSnippet' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function JSSetupSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + "import posthog from 'posthog-js'", + '', + `posthog.init('${currentTeam?.api_token}', { api_host: '${window.location.origin}' })`, + ].join('\n')} + + ) +} + +export function JSWebInstructions(): JSX.Element { + return ( + <> +

Option 1. Code snippet

+

+ Just add this snippet to your website within the <head> tag and we'll automatically + capture page views, sessions and all relevant interactions within your website. +

+ + +

Option 2. Javascript Library

+

Install the package

+ +

Initialize

+ + +

Final steps

+ + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx new file mode 100644 index 0000000000000..cda978ee12166 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx @@ -0,0 +1,98 @@ +import { Link } from 'lib/lemon-ui/Link' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function NextEnvVarsSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + `NEXT_PUBLIC_POSTHOG_KEY=${currentTeam?.api_token}`, + `NEXT_PUBLIC_POSTHOG_HOST=${window.location.origin}`, + ].join('\n')} + + ) +} + +function NextPagesRouterCodeSnippet(): JSX.Element { + return ( + + {`// pages/_app.js +... +import posthog from 'posthog-js' // Import PostHog + +if (typeof window !== 'undefined') { // checks that we are client-side + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com', + loaded: (posthog) => { + if (process.env.NODE_ENV === 'development') posthog.debug() // debug mode in development + }, + }) +} + +export default function App({ Component, pageProps }) { + const router = useRouter() + ...`} + + ) +} + +function NextAppRouterCodeSnippet(): JSX.Element { + return ( + + {`// app/providers.js +'use client' +... +import posthog from 'posthog-js' + +if (typeof window !== 'undefined') { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + }) +} +...`} + + ) +} + +export function NextJSInstructions(): JSX.Element { + return ( + <> +

Install posthog-js using your package manager

+ +

Add environment variables

+

+ Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, + Netlify, AWS). You can find your project API key in your project settings. +

+

+ These values need to start with NEXT_PUBLIC_ to be accessible on the + client-side. +

+ + +

Initialize

+

With App router

+

+ If your Next.js app to uses the app router, you can + integrate PostHog by creating a providers file in your app folder. This is because the posthog-js + library needs to be initialized on the client-side using the Next.js{' '} + + 'use client' directive + + . +

+ +

With Pages router

+

+ If your Next.js app uses the pages router, you can + integrate PostHog at the root of your app (pages/_app.js). +

+ + + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx new file mode 100644 index 0000000000000..86fdfc0f527c7 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx @@ -0,0 +1,64 @@ +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function ReactEnvVarsSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + `REACT_APP_POSTHOG_PUBLIC_KEY=${currentTeam?.api_token}`, + `REACT_APP_PUBLIC_POSTHOG_HOST=${window.location.origin}`, + ].join('\n')} + + ) +} + +function ReactSetupSnippet(): JSX.Element { + return ( + + {`// src/index.js +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +import { PostHogProvider} from 'posthog-js/react' + +const options = { + api_host: process.env.REACT_APP_PUBLIC_POSTHOG_HOST, +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + + + +);`} + + ) +} + +export function ReactInstructions(): JSX.Element { + return ( + <> +

Install the package

+ +

Add environment variables

+ +

Initialize

+

+ Integrate PostHog at the root of your app (src/index.js for the default{' '} + create-react-app). +

+ + + + ) +} diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 1b155f0974394..703e904d00a88 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -76,6 +76,8 @@ function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Elemen export function Products(): JSX.Element { const { featureFlags } = useValues(featureFlagLogic) const { billing } = useValues(billingLogic) + const { currentTeam } = useValues(teamLogic) + const isFirstProduct = Object.keys(currentTeam?.has_completed_onboarding_for || {}).length === 0 const products = billing?.products || [] useEffect(() => { @@ -87,19 +89,27 @@ export function Products(): JSX.Element { return (
-

Pick your first product.

+

Pick your {isFirstProduct ? 'first' : 'next'} product.

- Pick your first product to get started with. You can set up any others you'd like later. + Pick your {isFirstProduct ? 'first' : 'next'} product to get started with. You can set up any others + you'd like later.

{products.length > 0 ? ( -
- {products - .filter((product) => !product.contact_support && !product.inclusion_only) - .map((product) => ( - - ))} -
+ <> +
+ {products + .filter((product) => !product.contact_support && !product.inclusion_only) + .map((product) => ( + + ))} +
+
+ + None of these + +
+ ) : ( )}