Skip to content

Commit 1d71822

Browse files
authored
show CTA up to 3 dismissals every 24 hours or until AI Search is made (#55741)
1 parent 39beae4 commit 1d71822

File tree

2 files changed

+57
-26
lines changed

2 files changed

+57
-26
lines changed

src/frame/components/context/CTAContext.tsx

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,59 +13,89 @@ import {
1313
type CTAPopoverState = {
1414
isOpen: boolean
1515
initializeCTA: () => void // Call to "open" the CTA if it's not already been dismissed by the user
16-
dismiss: () => void // Call to "close" the CTA and store the dismissal in local storage
16+
dismiss: () => void // Call to "close" the CTA and store the dismissal in local storage. Will be shown again after 24 hours for a max of 3 times
17+
permanentDismiss: () => void // Call to permanently dismiss the CTA and store the dismissal in local storage
1718
}
1819

19-
type StoredValue = { dismissed: true }
20+
type StoredValue = {
21+
dismissedCount: number
22+
lastDismissedAt: number | null
23+
permanentlyDismissed: boolean
24+
}
2025

2126
const CTAPopoverContext = createContext<CTAPopoverState | undefined>(undefined)
2227

23-
const STORAGE_KEY = 'ctaPopoverDismissed'
28+
const STORAGE_KEY = 'ctaPopoverState'
29+
const MAX_DISMISSES = 3
30+
const HIDE_CTA_FOR_MS = 24 * 60 * 60 * 1000 // Every 24 hours we show the CTA again, unless permanently dismissed
2431

25-
const isDismissed = (): boolean => {
32+
const shouldHide = (): boolean => {
2633
if (typeof window === 'undefined') return false // SSR guard
2734
try {
2835
const raw = localStorage.getItem(STORAGE_KEY)
2936
if (!raw) return false
3037
const parsed = JSON.parse(raw) as StoredValue
31-
return parsed?.dismissed
38+
if (parsed.permanentlyDismissed) return true
39+
if (parsed.dismissedCount >= MAX_DISMISSES) return true
40+
if (parsed.lastDismissedAt && Date.now() - parsed.lastDismissedAt < HIDE_CTA_FOR_MS) return true
41+
return false
3242
} catch {
3343
return false // corruption / quota / disabled storage
3444
}
3545
}
3646

47+
const readStored = (): StoredValue => {
48+
const emptyValue = { dismissedCount: 0, lastDismissedAt: null, permanentlyDismissed: false }
49+
try {
50+
const raw = localStorage.getItem(STORAGE_KEY)
51+
if (!raw) {
52+
return emptyValue
53+
}
54+
return JSON.parse(raw) as StoredValue
55+
} catch {
56+
return emptyValue // corruption / quota / disabled storage
57+
}
58+
}
59+
60+
const writeStored = (v: StoredValue) => {
61+
try {
62+
localStorage.setItem(STORAGE_KEY, JSON.stringify(v))
63+
} catch {
64+
/* ignore */
65+
}
66+
}
67+
3768
export function CTAPopoverProvider({ children }: PropsWithChildren) {
3869
// We start closed because we might only want to "turn on" the CTA if an experiment is active
3970
const [isOpen, setIsOpen] = useState(false)
4071

41-
const persistDismissal = useCallback(() => {
72+
const dismiss = useCallback(() => {
73+
const stored = readStored()
74+
writeStored({
75+
...stored,
76+
dismissedCount: stored.dismissedCount + 1,
77+
lastDismissedAt: Date.now(),
78+
})
79+
setIsOpen(false)
80+
}, [])
81+
82+
const permanentDismiss = useCallback(() => {
83+
const stored = readStored()
84+
writeStored({ ...stored, permanentlyDismissed: true })
4285
setIsOpen(false)
43-
try {
44-
const obj: StoredValue = { dismissed: true }
45-
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj))
46-
} catch {
47-
/* ignore */
48-
}
4986
}, [])
5087

51-
const dismiss = useCallback(() => persistDismissal(), [persistDismissal])
5288
const initializeCTA = useCallback(() => {
53-
const dismissed = isDismissed()
54-
if (dismissed) {
55-
setIsOpen(false)
56-
} else {
57-
setIsOpen(true)
58-
}
59-
}, [isDismissed])
89+
setIsOpen(!shouldHide())
90+
}, [])
6091

6192
// Wrap in a useEffect to avoid a hydration mismatch (SSR guard)
6293
useEffect(() => {
63-
const stored = isDismissed()
64-
setIsOpen(!stored)
94+
setIsOpen(!shouldHide())
6595
}, [])
6696

6797
return (
68-
<CTAPopoverContext.Provider value={{ isOpen, initializeCTA, dismiss }}>
98+
<CTAPopoverContext.Provider value={{ isOpen, initializeCTA, dismiss, permanentDismiss }}>
6999
{children}
70100
</CTAPopoverContext.Provider>
71101
)

src/search/components/input/AskAIResults.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export function AskAIResults({
8383
aiCouldNotAnswer: boolean
8484
connectedEventId?: string
8585
}>('ai-query-cache', 1000, 7)
86-
const { isOpen: isCTAOpen, dismiss: dismissCTA } = useCTAPopoverContext()
86+
const { isOpen: isCTAOpen, permanentDismiss: permanentlyDismissCTA } = useCTAPopoverContext()
8787

8888
const [isCopied, setCopied] = useClipboard(message, { successDuration: 1400 })
8989
const [feedbackSelected, setFeedbackSelected] = useState<null | 'up' | 'down'>(null)
@@ -138,9 +138,10 @@ export function AskAIResults({
138138
setResponseLoading(true)
139139
disclaimerRef.current?.focus()
140140

141-
// Upon performing an AI Search, dismiss the CTA if it is open
141+
// We permanently dismiss the CTA after performing an AI Search because the
142+
// user has tried it and doesn't require additional CTA prompting to try it
142143
if (isCTAOpen) {
143-
dismissCTA()
144+
permanentlyDismissCTA()
144145
}
145146

146147
const cachedData = getItem(query, version, router.locale || 'en')

0 commit comments

Comments
 (0)