@@ -13,59 +13,89 @@ import {
1313type 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
2126const 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+
3768export 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 )
0 commit comments