Skip to content

Commit 17eb114

Browse files
authored
feat(product tours): add tour wait period config (#48224)
## Problem closes #48209 (comment) <!-- Who are we building for, what are their needs, why is this important? --> <!-- Does this fix an issue? Uncomment the line below with the issue ID to automatically close it when merged --> <!-- Closes #ISSUE_ID --> SDK PR: PostHog/posthog-js#3109 ## Changes - adds new wait period config in product tour UI - updates API to handle wait period by adding person property checks to targeting flags <!-- If there are frontend changes, please include screenshots. --> <!-- If a reference design was involved, include a link to the relevant Figma frame! --> ## How did you test this code? <!-- Briefly describe the steps you took. --> <!-- Include automated tests if possible, otherwise describe the manual testing routine. --> <!-- Docs reminder: If this change requires updated docs, please do that! Engineers are the primary people responsible for their documentation. 🙌 --> 👉 _Stay up-to-date with_ [_PostHog coding conventions_](https://posthog.com/docs/contribute/coding-conventions) _for a smoother review._ ## Publish to changelog? <!-- For features only --> <!-- If publishing, you must provide changelog details in the #changelog Slack channel. You will receive a follow-up PR comment or notification. --> <!-- If not, write "no" or "do not publish to changelog" to explicitly opt-out of posting to #changelog. Removing this entire section will not prevent posting. -->
1 parent e1e5fe3 commit 17eb114

File tree

7 files changed

+418
-70
lines changed

7 files changed

+418
-70
lines changed

frontend/src/scenes/product-tours/components/AutoShowSection.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { EventSelect } from 'lib/components/EventSelect/EventSelect'
1717
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
1818
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
19+
import { toSentenceCase } from 'lib/utils'
1920
import { AddEventButton } from 'scenes/surveys/AddEventButton'
2021
import { SurveyMatchTypeLabels } from 'scenes/surveys/constants'
2122
import {
@@ -28,6 +29,7 @@ import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel'
2829
import {
2930
ActionType,
3031
AnyPropertyFilter,
32+
EffectiveProductTourType,
3133
ProductTourDisplayConditions,
3234
ProductTourDisplayFrequency,
3335
PropertyDefinitionType,
@@ -41,6 +43,13 @@ import { getDefaultDisplayFrequency, getDisplayFrequencyOptions, isAnnouncement
4143

4244
type TourTriggerType = 'immediate' | 'event' | 'action'
4345

46+
const ALL_TOUR_TYPES: EffectiveProductTourType[] = ['tour', 'announcement', 'banner']
47+
48+
const TOUR_TYPE_OPTIONS = ALL_TOUR_TYPES.map((type) => ({
49+
key: type,
50+
label: toSentenceCase(type),
51+
}))
52+
4453
/**
4554
* this should probably be re-used from surveys!!
4655
*
@@ -399,6 +408,66 @@ export function AutoShowSection({ id }: { id: string }): JSX.Element | null {
399408
/>
400409
<span className="text-sm">seconds before showing the {entityKeyword}</span>
401410
</div>
411+
412+
<div className="mt-4">
413+
<div className="flex flex-row gap-2 items-center">
414+
<LemonCheckbox
415+
checked={!!conditions.seenTourWaitPeriod}
416+
onChange={(checked) => {
417+
onChange({
418+
...conditions,
419+
seenTourWaitPeriod: checked ? { days: 7, types: [...ALL_TOUR_TYPES] } : undefined,
420+
})
421+
}}
422+
/>
423+
<span className="text-sm">Don't show to users who recently saw another...</span>
424+
</div>
425+
{conditions.seenTourWaitPeriod && (
426+
<div className="ml-7 mt-2 flex flex-row gap-2 items-center">
427+
<div className="w-80 max-w-80">
428+
<LemonInputSelect
429+
mode="multiple"
430+
value={conditions.seenTourWaitPeriod.types || []}
431+
onChange={(values) => {
432+
onChange({
433+
...conditions,
434+
seenTourWaitPeriod: {
435+
...conditions.seenTourWaitPeriod!,
436+
types: values as EffectiveProductTourType[],
437+
},
438+
})
439+
}}
440+
options={TOUR_TYPE_OPTIONS}
441+
placeholder="Select types..."
442+
disablePrompting
443+
size="small"
444+
/>
445+
</div>
446+
<span className="text-sm whitespace-nowrap">in the last</span>
447+
<LemonInput
448+
type="number"
449+
size="small"
450+
min={1}
451+
max={365}
452+
value={conditions.seenTourWaitPeriod.days ?? NaN}
453+
onChange={(newValue) => {
454+
if (!conditions.seenTourWaitPeriod) {
455+
return
456+
}
457+
onChange({
458+
...conditions,
459+
seenTourWaitPeriod: {
460+
...conditions.seenTourWaitPeriod,
461+
days: newValue as number,
462+
},
463+
})
464+
}}
465+
className="w-12"
466+
/>
467+
<span className="text-sm">days</span>
468+
</div>
469+
)}
470+
</div>
402471
</div>
403472

404473
<div>

frontend/src/scenes/product-tours/components/NewProductTourModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useState } from 'react'
44
import { IconCursorClick, IconMegaphone } from '@posthog/icons'
55
import { LemonButton, LemonInput, LemonLabel, LemonModal } from '@posthog/lemon-ui'
66

7-
type TourType = 'tour' | 'announcement' | 'banner' | undefined
7+
import { EffectiveProductTourType } from '~/types'
88

99
export interface NewProductTourModalProps {
1010
isOpen: boolean
@@ -23,7 +23,7 @@ export function NewProductTourModal({
2323
onCreateTour,
2424
existingTourNames,
2525
}: NewProductTourModalProps): JSX.Element {
26-
const [tourType, setTourType] = useState<TourType>()
26+
const [tourType, setTourType] = useState<EffectiveProductTourType>()
2727
const [tourName, setTourName] = useState<string | undefined>()
2828
const [tourNameError, setTourNameError] = useState<string | undefined>()
2929

frontend/src/scenes/product-tours/productTourLogic.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,15 @@ export const productTourLogic = kea<productTourLogicType>([
388388
}
389389
}
390390

391+
const waitPeriod = content.conditions?.seenTourWaitPeriod
392+
if (waitPeriod) {
393+
if (!waitPeriod.types || waitPeriod.types.length === 0) {
394+
errors._form = 'Wait period requires at least one type selected'
395+
} else if (!waitPeriod.days || waitPeriod.days < 1) {
396+
errors._form = 'Wait period requires a number of days'
397+
}
398+
}
399+
391400
return errors
392401
},
393402
submit: async (formValues: ProductTourForm) => {

frontend/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3474,6 +3474,13 @@ export interface StepOrderVersion {
34743474
created_at: string
34753475
}
34763476

3477+
export type EffectiveProductTourType = 'tour' | 'announcement' | 'banner'
3478+
3479+
export interface ProductTourWaitPeriod {
3480+
days: number
3481+
types: EffectiveProductTourType[]
3482+
}
3483+
34773484
export interface ProductTourDisplayConditions {
34783485
url?: string
34793486
urlMatchType?: SurveyMatchType
@@ -3492,6 +3499,7 @@ export interface ProductTourDisplayConditions {
34923499
values: SurveyEventsWithProperties[]
34933500
} | null
34943501
linkedFlagVariant?: string
3502+
seenTourWaitPeriod?: ProductTourWaitPeriod
34953503
}
34963504

34973505
export interface ProductTourAppearance {

0 commit comments

Comments
 (0)