mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
Email gated signup integration (#1742)
* remove nested info from HubSpot request * introduce feature flags to constants file * set experiment variant on mount * routing and flag logic for email flow * old flow (skipDeploymentOptions) * try routing everything through signup page * add a call to reload feature flags; show that it still does not work * logic to set, persist, and update active experiment variants * changes and hydration from feature flags * selector for activeVariant, minor cleanup * bugfix * capture with set_once * send variant name as an event prop, use a more description user prop name * render nothing on the intermediate page if a redirect should occur Co-authored-by: kunal <kunal@kunals-MacBook-Pro.local>
This commit is contained in:
3
kea.js
3
kea.js
@@ -3,10 +3,11 @@ import { Provider } from 'react-redux'
|
||||
import { getContext, resetContext } from 'kea'
|
||||
import { loadersPlugin } from 'kea-loaders'
|
||||
import { routerPlugin } from 'kea-router'
|
||||
import { localStoragePlugin } from 'kea-localstorage'
|
||||
|
||||
export function initKea(isServer = false, location = '') {
|
||||
resetContext({
|
||||
plugins: [loadersPlugin, routerPlugin(isServer ? { location } : {})],
|
||||
plugins: [loadersPlugin, routerPlugin(isServer ? { location } : {}), localStoragePlugin],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"katex": "^0.12.0",
|
||||
"kea": "^2.2.2",
|
||||
"kea-loaders": "^0.3.0",
|
||||
"kea-localstorage": "^1.1.1",
|
||||
"kea-router": "^0.5.1",
|
||||
"kea-typegen": "^0.5.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
|
||||
@@ -68,9 +68,7 @@ export const DeploymentOptionsGrid = (): JSX.Element => {
|
||||
'No third-party cookies',
|
||||
]}
|
||||
/>
|
||||
<ButtonBlock
|
||||
onClick={() => reportDeploymentTypeSelected(Realm.hosted, '/docs/self-host/overview')}
|
||||
>
|
||||
<ButtonBlock onClick={() => reportDeploymentTypeSelected(Realm.hosted, '/docs/self-host')}>
|
||||
Select
|
||||
</ButtonBlock>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ const PrimaryCta = ({ children, className = '' }: { children: any; className?: s
|
||||
<li className="leading-none">
|
||||
<button
|
||||
onClick={() => {
|
||||
window.location.href = 'https://app.posthog.com/signup?src=header'
|
||||
window.location.pathname = '/sign-up'
|
||||
}}
|
||||
className={classList}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import { CallToAction } from 'components/CallToAction'
|
||||
|
||||
export const LandingPageCallToAction = () => {
|
||||
export const LandingPageCallToAction = (): JSX.Element => {
|
||||
return (
|
||||
<div className="ctas flex flex-col items-center justify-center w-full max-w-xl sm:mx-auto sm:flex-row">
|
||||
<CallToAction icon="rocket" href="https://app.posthog.com/signup?src=home-hero">
|
||||
<CallToAction icon="rocket" type="primary" to="/sign-up?src=home-hero">
|
||||
Get Started
|
||||
</CallToAction>
|
||||
<CallToAction icon="calendar" type="secondary" className="mt-3 sm:mt-0 sm:ml-4" to="/schedule-demo">
|
||||
|
||||
@@ -5,9 +5,10 @@ import './SignupModal.scss'
|
||||
import { Button, Input } from 'antd'
|
||||
import { ButtonBlock } from 'components/ButtonBlock/ButtonBlock'
|
||||
import { signupLogic } from 'logic/signupLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
export const SignupModal = (): JSX.Element => {
|
||||
const { email } = useValues(signupLogic)
|
||||
const { email, activeVariant } = useValues(signupLogic)
|
||||
const { setEmail, submitForm, skipEmailEntry } = useActions(signupLogic)
|
||||
|
||||
return (
|
||||
@@ -42,9 +43,11 @@ export const SignupModal = (): JSX.Element => {
|
||||
<Button htmlType="submit" type="link">
|
||||
<strong>Next: </strong> Choose your edition
|
||||
</Button>
|
||||
{activeVariant !== FEATURE_FLAGS.EMAIL_GATED_SIGNUP_NOT_SKIPPABLE && (
|
||||
<Button type="link" onClick={() => skipEmailEntry()}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
8
src/lib/constants.ts
Normal file
8
src/lib/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const FEATURE_FLAGS = {
|
||||
EMAIL_GATED_SIGNUP_CONTROL: 'email-gated-signup-control',
|
||||
EMAIL_GATED_SIGNUP_OLD_FLOW: 'email-gated-signup-old-flow',
|
||||
EMAIL_GATED_SIGNUP_NOT_SKIPPABLE: 'email-gated-signup-not-skippable',
|
||||
EMAIL_GATED_SIGNUP_SKIPPABLE: 'email-gated-signup-skippable',
|
||||
}
|
||||
|
||||
export const EMAIL_GATED_SIGNUP_PREFIX = 'email-gated-signup'
|
||||
@@ -1,4 +1,5 @@
|
||||
import { kea } from 'kea'
|
||||
import { getCookie } from 'lib/utils'
|
||||
|
||||
export const posthogAnalyticsLogic = kea({
|
||||
actions: {
|
||||
@@ -57,4 +58,26 @@ export const posthogAnalyticsLogic = kea({
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
selectors: {
|
||||
activeFeatureFlags: [
|
||||
(s) => [s.featureFlags],
|
||||
(featureFlags) =>
|
||||
Object.entries(featureFlags)
|
||||
.filter(([, value]) => !!value)
|
||||
.map(([key]) => key),
|
||||
],
|
||||
isLoggedIn: [
|
||||
(s) => [s.posthog],
|
||||
(posthog) => {
|
||||
const token = posthog?.config?.token
|
||||
if (token) {
|
||||
const rawCookie = getCookie(`ph_${token}_posthog`)
|
||||
if (!rawCookie) return false
|
||||
const cookie = JSON.parse(rawCookie)
|
||||
return !!cookie['$user_id']
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { kea } from 'kea'
|
||||
import { EMAIL_GATED_SIGNUP_PREFIX, FEATURE_FLAGS } from 'lib/constants'
|
||||
import { isValidEmailAddress } from 'lib/utils'
|
||||
import { posthogAnalyticsLogic } from './posthogAnalyticsLogic'
|
||||
|
||||
@@ -12,18 +13,8 @@ export enum Realm {
|
||||
cloud = 'cloud',
|
||||
}
|
||||
|
||||
type HubSpotContact = {
|
||||
id: string // Hubspot contact ID (number as string)
|
||||
properties: Record<string, string>
|
||||
created_at: string // ISO 8601
|
||||
updated_at: string // ISO 8601
|
||||
archived: boolean
|
||||
archived_at: string | null // ISO 8601
|
||||
}
|
||||
|
||||
export type HubSpotContactResponse = {
|
||||
success: boolean
|
||||
result: string | HubSpotContact
|
||||
}
|
||||
|
||||
async function createContact(email: string) {
|
||||
@@ -54,6 +45,7 @@ async function updateContact(email: string, properties: Record<string, string>)
|
||||
}
|
||||
|
||||
export const signupLogic = kea({
|
||||
path: typeof window === undefined ? undefined : () => ['signup'],
|
||||
actions: {
|
||||
setModalView: (view: SignupModalView) => ({ view }),
|
||||
setEmail: (email: string) => ({ email }),
|
||||
@@ -61,7 +53,14 @@ export const signupLogic = kea({
|
||||
skipEmailEntry: true,
|
||||
reportModalShown: true,
|
||||
reportDeploymentOptionsShown: true,
|
||||
reportDeploymentTypeSelected: (deploymentType: Realm, nextHref?: string) => ({ deploymentType, nextHref }),
|
||||
onRenderSignupPage: true,
|
||||
reportDeploymentTypeSelected: (deploymentType: Realm, nextHref?: string) => ({
|
||||
deploymentType,
|
||||
nextHref,
|
||||
}),
|
||||
setVariants: (newVariants: string[]) => ({ newVariants }),
|
||||
setActiveVariant: (activeVariant: string) => ({ activeVariant }),
|
||||
updateAvailableVariants: true,
|
||||
},
|
||||
reducers: {
|
||||
modalView: [
|
||||
@@ -76,21 +75,56 @@ export const signupLogic = kea({
|
||||
setEmail: (_, { email }: { email: string }) => email,
|
||||
},
|
||||
],
|
||||
experimentVariants: [
|
||||
{} as Record<string, boolean>,
|
||||
{ persist: true },
|
||||
{
|
||||
setVariants: (state: Record<string, boolean>, { newVariants }: { newVariants: string[] }) => {
|
||||
const nextState = {} as Record<string, boolean>
|
||||
const existingVariants = Object.keys(state)
|
||||
newVariants.forEach((variant) => {
|
||||
if (existingVariants.includes(variant)) {
|
||||
// Don't overwrite an existing value
|
||||
nextState[variant] = state[variant]
|
||||
} else {
|
||||
nextState[variant] = false
|
||||
}
|
||||
})
|
||||
return nextState
|
||||
},
|
||||
setActiveVariant: (state: Record<string, boolean>, { activeVariant }: { activeVariant: string }) => {
|
||||
const nextState = {} as Record<string, boolean>
|
||||
const existingVariants = Object.keys(state)
|
||||
existingVariants.forEach((variant) => {
|
||||
nextState[variant] = variant === activeVariant
|
||||
})
|
||||
return nextState
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
connect: {
|
||||
values: [posthogAnalyticsLogic, ['posthog']],
|
||||
values: [posthogAnalyticsLogic, ['posthog', 'activeFeatureFlags', 'isLoggedIn']],
|
||||
actions: [posthogAnalyticsLogic, ['setFeatureFlags']],
|
||||
},
|
||||
listeners: ({ actions, values }) => ({
|
||||
submitForm: async () => {
|
||||
const { posthog, email } = values
|
||||
const { posthog, email, activeVariant } = values
|
||||
const skipDeploymentOptions = activeVariant === FEATURE_FLAGS.EMAIL_GATED_SIGNUP_OLD_FLOW
|
||||
if (email && isValidEmailAddress(email)) {
|
||||
try {
|
||||
posthog.identify(email, { email: email.toLowerCase() }) // use email as distinct ID; also set it as property
|
||||
posthog.capture('signup: submit email')
|
||||
if (!skipDeploymentOptions) {
|
||||
actions.setModalView(SignupModalView.DEPLOYMENT_OPTIONS)
|
||||
}
|
||||
await createContact(email)
|
||||
} catch (err) {
|
||||
posthog.capture('signup: failed to create contact', { message: err })
|
||||
} finally {
|
||||
if (skipDeploymentOptions) {
|
||||
window.location.replace(`https://app.posthog.com/signup?email=${encodeURIComponent(email)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -119,7 +153,9 @@ export const signupLogic = kea({
|
||||
reportDeploymentTypeSelected: async ({ deploymentType, nextHref }) => {
|
||||
const { posthog, email } = values
|
||||
try {
|
||||
posthog.capture('signup: deployment type selected', { selected_deployment_type: deploymentType })
|
||||
posthog.capture('signup: deployment type selected', {
|
||||
selected_deployment_type: deploymentType,
|
||||
})
|
||||
await updateContact(email, { selected_deployment_type: deploymentType })
|
||||
} catch (err) {
|
||||
posthog.capture('signup: failed to update contact', { message: err })
|
||||
@@ -128,5 +164,68 @@ export const signupLogic = kea({
|
||||
window.location.replace(nextHref)
|
||||
}
|
||||
},
|
||||
onRenderSignupPage: () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (values.shouldAutoRedirect) {
|
||||
window.location.replace(`https://app.posthog.com/signup${window.location.search || ''}`)
|
||||
} else {
|
||||
actions.reportModalShown()
|
||||
}
|
||||
}
|
||||
},
|
||||
setVariants: () => {
|
||||
const { experimentVariants, posthog } = values
|
||||
const variantEntries: [string, boolean][] = Object.entries(experimentVariants)
|
||||
if (variantEntries.length && !variantEntries.some(([, status]) => status === true)) {
|
||||
// If all available variants are inactive, we need to randomly pick one to activate.
|
||||
const randomIndex = Math.floor(Math.random() * variantEntries.length)
|
||||
const [name] = variantEntries[randomIndex]
|
||||
actions.setActiveVariant(name)
|
||||
posthog.capture('set email experiment variant', {
|
||||
variant: name,
|
||||
$set_once: {
|
||||
email_experiment_variant: name,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
updateAvailableVariants: () => {
|
||||
const { activeFeatureFlags } = values
|
||||
const variantFlags: string[] = activeFeatureFlags.filter((flag: string) =>
|
||||
flag.includes(EMAIL_GATED_SIGNUP_PREFIX)
|
||||
)
|
||||
actions.setVariants(variantFlags)
|
||||
},
|
||||
[actions.setFeatureFlags]: () => {
|
||||
actions.updateAvailableVariants()
|
||||
},
|
||||
}),
|
||||
events: ({ actions, values }) => ({
|
||||
afterMount: () => {
|
||||
if (typeof window !== undefined && !!values.posthog) {
|
||||
actions.updateAvailableVariants()
|
||||
}
|
||||
},
|
||||
}),
|
||||
selectors: {
|
||||
hasEmailGatedSignup: [
|
||||
(s) => [s.activeVariant],
|
||||
(activeVariant: string | null) =>
|
||||
activeVariant && activeVariant !== FEATURE_FLAGS.EMAIL_GATED_SIGNUP_CONTROL,
|
||||
],
|
||||
shouldAutoRedirect: [
|
||||
(s) => [s.hasEmailGatedSignup, s.isLoggedIn],
|
||||
(hasEmailGatedSignup: boolean, isLoggedIn: boolean) => {
|
||||
return !hasEmailGatedSignup || isLoggedIn
|
||||
},
|
||||
],
|
||||
activeVariant: [
|
||||
(s) => [s.experimentVariants],
|
||||
(experimentVariants: Record<string, boolean>) => {
|
||||
const variantEntries: [string, boolean][] = Object.entries(experimentVariants)
|
||||
const [name] = variantEntries.find(([, value]) => value) || []
|
||||
return name || null
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -9,12 +9,15 @@ import { Tutorials } from '../components/LandingPage/Tutorials'
|
||||
import { RecentBlogPosts } from '../components/LandingPage/RecentBlogPosts'
|
||||
import { Footer } from '../components/Footer/Footer'
|
||||
import { GetStartedModal } from 'components/GetStartedModal'
|
||||
import { posthogAnalyticsLogic } from 'logic/posthogAnalyticsLogic'
|
||||
import { useValues } from 'kea'
|
||||
|
||||
import { SEO } from '../components/seo'
|
||||
|
||||
import '../components/LandingPage/styles/index.scss'
|
||||
|
||||
const IndexPage = () => {
|
||||
useValues(posthogAnalyticsLogic) // mount this logic
|
||||
return (
|
||||
<div className="w-screen overflow-x-hidden">
|
||||
<SEO
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { SEO } from '../components/seo'
|
||||
import '../components/Pricing/styles/index.scss'
|
||||
import Header from 'components/Header'
|
||||
@@ -8,14 +8,13 @@ import { useActions, useValues } from 'kea'
|
||||
import { signupLogic, SignupModalView } from 'logic/signupLogic'
|
||||
import './styles/sign-up.scss'
|
||||
import { DeploymentOptionsGrid } from 'components/DeploymentOptionsGrid/DeploymentOptionsGrid'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const SignUpPage = (): JSX.Element => {
|
||||
const { modalView } = useValues(signupLogic)
|
||||
const { reportModalShown } = useActions(signupLogic)
|
||||
const { modalView, hasEmailGatedSignup } = useValues(signupLogic)
|
||||
const { onRenderSignupPage } = useActions(signupLogic)
|
||||
|
||||
useEffect(() => {
|
||||
reportModalShown()
|
||||
onRenderSignupPage()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -24,6 +23,8 @@ const SignUpPage = (): JSX.Element => {
|
||||
title="Sign Up • PostHog"
|
||||
description="Unlock insights. Our Open Source, Scale, and Cloud editions provide flexible deployment of reliable analytics."
|
||||
/>
|
||||
{hasEmailGatedSignup && (
|
||||
<>
|
||||
<Header onPostPage={false} logoOnly transparentBackground />
|
||||
<div className="w-full h-full relative flex items-center justify-center">
|
||||
{modalView === SignupModalView.EMAIL_PROMPT && (
|
||||
@@ -49,6 +50,8 @@ const SignUpPage = (): JSX.Element => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@@ -3932,9 +3932,9 @@ caniuse-lite@^1.0.30001219:
|
||||
integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==
|
||||
|
||||
caniuse-lite@^1.0.30001243:
|
||||
version "1.0.30001248"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce"
|
||||
integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw==
|
||||
version "1.0.30001249"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
|
||||
integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
@@ -10435,6 +10435,11 @@ kea-loaders@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/kea-loaders/-/kea-loaders-0.3.0.tgz"
|
||||
integrity sha512-aXrUjQf/GdJVDQqLtTWoY4gF1F+dTXv5U1LfCLffPhgrWRYHGTnaBV/K6pWxy1Qh+vMbepy80Nx9hFiK1owtHw==
|
||||
|
||||
kea-localstorage@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/kea-localstorage/-/kea-localstorage-1.1.1.tgz#6edf69476779e002708fb10f2360e48333707406"
|
||||
integrity sha512-YijSF33Y1QpfHAq1hGvulWWoGC9Kckd4lVa+XStHar4t7GfoaDa1aWnTeJxzz9bWnJovfk+MnG8ekP4rWIqINA==
|
||||
|
||||
kea-router@^0.5.1:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/kea-router/-/kea-router-0.5.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user