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:
Sam Winslow
2021-08-11 17:22:53 -04:00
committed by GitHub
parent 0103a0d732
commit 126f21e8cd
12 changed files with 202 additions and 58 deletions

3
kea.js
View File

@@ -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],
})
}

View File

@@ -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",

View File

@@ -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>

View File

@@ -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}
>

View File

@@ -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">

View File

@@ -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
View 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'

View File

@@ -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']
}
},
],
},
})

View File

@@ -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
},
],
},
})

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -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"