11// hooks/useFirstTimeUse.ts
22import { useEffect , useState } from 'react' ;
3- import { useCookies } from 'react-cookie' ;
43import { DEFAULT_EXPIRATION_DAYS , FTU_KEY , FTU_VALUE } from './constants' ;
54
65type StorageError = {
76 store : string ;
87 error : unknown ;
98} ;
109
10+ type StoredData = {
11+ value : string ;
12+ expiresAt : number ;
13+ } ;
14+
1115function logStorageError ( { store, error } : StorageError ) {
1216 console . warn ( `[useFirstTimeUse] Failed to access ${ store } :` , error ) ;
1317}
1418
15- function getCookieExpiry ( days : number ) : Date {
16- const expires = new Date ( ) ;
17- expires . setDate ( expires . getDate ( ) + days ) ;
18- return expires ;
19+ function getExpirationTimestamp ( days : number ) : number {
20+ return Date . now ( ) + days * 24 * 60 * 60 * 1000 ;
1921}
2022
21- function setLocalStorage ( ) : void {
23+ function setLocalStorage ( expirationDays : number ) : void {
2224 try {
23- localStorage . setItem ( FTU_KEY , FTU_VALUE ) ;
25+ const data : StoredData = {
26+ value : FTU_VALUE ,
27+ expiresAt : getExpirationTimestamp ( expirationDays ) ,
28+ } ;
29+ localStorage . setItem ( FTU_KEY , JSON . stringify ( data ) ) ;
2430 } catch ( error ) {
2531 logStorageError ( { store : 'localStorage' , error } ) ;
2632 }
@@ -34,7 +40,7 @@ function setSessionStorage(): void {
3440 }
3541}
3642
37- function setIndexedDB ( ) : void {
43+ function setIndexedDB ( expirationDays : number ) : void {
3844 try {
3945 const req = indexedDB . open ( 'app_prefs' , 1 ) ;
4046
@@ -49,7 +55,11 @@ function setIndexedDB(): void {
4955 try {
5056 const db = ( e . target as IDBOpenDBRequest ) . result ;
5157 const tx = db . transaction ( 'flags' , 'readwrite' ) ;
52- tx . objectStore ( 'flags' ) . put ( FTU_VALUE , FTU_KEY ) ;
58+ const data : StoredData = {
59+ value : FTU_VALUE ,
60+ expiresAt : getExpirationTimestamp ( expirationDays ) ,
61+ } ;
62+ tx . objectStore ( 'flags' ) . put ( data , FTU_KEY ) ;
5363 tx . onerror = ( ) =>
5464 logStorageError ( { store : 'indexedDB.transaction' , error : tx . error } ) ;
5565 } catch ( error ) {
@@ -72,7 +82,34 @@ function setIndexedDB(): void {
7282
7383function checkLocalStorage ( ) : boolean {
7484 try {
75- return localStorage . getItem ( FTU_KEY ) === FTU_VALUE ;
85+ const item = localStorage . getItem ( FTU_KEY ) ;
86+ if ( ! item ) return false ;
87+
88+ // Try parsing as JSON (new format with expiration)
89+ try {
90+ const data = JSON . parse ( item ) ;
91+ // Check if it's an object with the expected structure
92+ if ( typeof data === 'object' && data !== null && 'value' in data ) {
93+ const storedData = data as StoredData ;
94+ if ( storedData . expiresAt && Date . now ( ) > storedData . expiresAt ) {
95+ // Expired, remove it
96+ localStorage . removeItem ( FTU_KEY ) ;
97+ return false ;
98+ }
99+ return storedData . value === FTU_VALUE ;
100+ }
101+ // Parsed as JSON but not the expected format - treat as legacy
102+ if ( item === FTU_VALUE ) {
103+ return true ;
104+ }
105+ return false ;
106+ } catch {
107+ // JSON parse failed - treat as legacy format (plain string)
108+ if ( item === FTU_VALUE ) {
109+ return true ; // Still valid
110+ }
111+ return false ;
112+ }
76113 } catch ( error ) {
77114 logStorageError ( { store : 'localStorage.read' , error } ) ;
78115 return false ;
@@ -98,7 +135,29 @@ function checkIndexedDB(): Promise<boolean> {
98135 . transaction ( 'flags' )
99136 . objectStore ( 'flags' )
100137 . get ( FTU_KEY ) ;
101- getReq . onsuccess = ( ) => resolve ( getReq . result === FTU_VALUE ) ;
138+ getReq . onsuccess = ( ) => {
139+ const result = getReq . result ;
140+ if ( ! result ) {
141+ resolve ( false ) ;
142+ return ;
143+ }
144+
145+ // Check if it's the new format with expiration
146+ if ( typeof result === 'object' && 'expiresAt' in result ) {
147+ const data = result as StoredData ;
148+ if ( Date . now ( ) > data . expiresAt ) {
149+ // Expired, remove it
150+ const deleteTx = db . transaction ( 'flags' , 'readwrite' ) ;
151+ deleteTx . objectStore ( 'flags' ) . delete ( FTU_KEY ) ;
152+ resolve ( false ) ;
153+ return ;
154+ }
155+ resolve ( data . value === FTU_VALUE ) ;
156+ } else {
157+ // Legacy format
158+ resolve ( result === FTU_VALUE ) ;
159+ }
160+ } ;
102161 getReq . onerror = ( ) => {
103162 logStorageError ( { store : 'indexedDB.get' , error : getReq . error } ) ;
104163 resolve ( false ) ;
@@ -129,14 +188,12 @@ function checkIndexedDB(): Promise<boolean> {
129188}
130189
131190export function useFirstTimeUse ( ) {
132- const [ cookies , setCookie ] = useCookies ( [ FTU_KEY ] ) ;
133191 const [ showModal , setShowModal ] = useState ( false ) ;
134192 const [ isLoading , setIsLoading ] = useState ( true ) ;
135193
136194 useEffect ( ( ) => {
137195 const checkAllStores = async ( ) : Promise < boolean > => {
138- // react-cookie handles the cookie check reactively
139- if ( cookies [ FTU_KEY ] === FTU_VALUE ) return true ;
196+ // Check localStorage and IndexedDB (persists across logout)
140197 if ( checkLocalStorage ( ) ) return true ;
141198 if ( await checkIndexedDB ( ) ) return true ;
142199 return false ;
@@ -146,17 +203,12 @@ export function useFirstTimeUse() {
146203 setShowModal ( ! seen ) ;
147204 setIsLoading ( false ) ;
148205 } ) ;
149- } , [ cookies ] ) ;
206+ } , [ ] ) ;
150207
151208 const markSeen = ( expirationDays : number = DEFAULT_EXPIRATION_DAYS ) => {
152- setCookie ( FTU_KEY , FTU_VALUE , {
153- path : '/' ,
154- expires : getCookieExpiry ( expirationDays ) ,
155- sameSite : 'lax' ,
156- } ) ;
157- setLocalStorage ( ) ;
209+ setLocalStorage ( expirationDays ) ;
158210 setSessionStorage ( ) ;
159- setIndexedDB ( ) ;
211+ setIndexedDB ( expirationDays ) ;
160212 setShowModal ( false ) ;
161213 } ;
162214
0 commit comments