1+ import { useQueryClient } from "@tanstack/react-query" ;
12import {
23 createContext ,
34 type ReactNode ,
78 useRef ,
89 useState ,
910} from "react" ;
11+ import { queryKeys } from "@/config/cache.config" ;
1012import { USER_PREFERENCES_KEY } from "@/hooks/useUserPreferences" ;
1113import { CalComAPIService } from "@/services/calcom" ;
1214import {
@@ -19,6 +21,7 @@ import { WebAuthService } from "@/services/webAuth";
1921import { clearQueryCache } from "@/utils/queryPersister" ;
2022import { clearRegion , getRegion , preloadRegion , subscribeRegion } from "@/utils/region" ;
2123import { generalStorage , secureStorage } from "@/utils/storage" ;
24+ import { clearWidgetBookings } from "@/utils/widgetStorage" ;
2225
2326/**
2427 * Simplified user info stored in auth context
@@ -71,6 +74,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
7174 const [ userInfo , setUserInfo ] = useState < AuthUserInfo | null > ( null ) ;
7275 const [ isWebSession , setIsWebSession ] = useState ( false ) ;
7376 const [ loading , setLoading ] = useState ( true ) ;
77+ // AuthProvider is mounted inside QueryProvider (see app/_layout.tsx), so
78+ // useQueryClient() resolves the live client we need to wipe on logout and
79+ // on cross-user cache rehydration.
80+ const queryClient = useQueryClient ( ) ;
7481 // Construct synchronously on first render so downstream hooks can rely on a
7582 // non-null service immediately and mount-time configuration failures are
7683 // logged right away. The effect below rebuilds only if `preloadRegion()`
@@ -98,25 +105,52 @@ export function AuthProvider({ children }: AuthProviderProps) {
98105 } , [ ] ) ;
99106
100107 // Common post-login setup: configure API service and fetch user profile
101- const setupAfterLogin = useCallback ( async ( token : string , refreshToken ?: string ) => {
102- CalComAPIService . setAccessToken ( token , refreshToken ) ;
108+ const setupAfterLogin = useCallback (
109+ async ( token : string , refreshToken ?: string ) => {
110+ CalComAPIService . setAccessToken ( token , refreshToken ) ;
103111
104- try {
105- const profile = await CalComAPIService . getUserProfile ( ) ;
106- // Store user info for use in the app (e.g., to display "You" in bookings)
107- if ( profile ) {
108- setUserInfo ( {
109- email : profile . email ,
110- name : profile . name ,
111- id : profile . id ,
112- username : profile . username ,
113- } ) ;
112+ try {
113+ const profile = await CalComAPIService . getUserProfile ( ) ;
114+ // Store user info for use in the app (e.g., to display "You" in bookings)
115+ if ( profile ) {
116+ // If the persisted cache was rehydrated for a different user (cold
117+ // start before logout fully ran, or a foreign cache that survived
118+ // restore), wipe everything before the next fetch lands so no PII
119+ // from the previous identity flashes onto screen.
120+ const cachedProfile = queryClient . getQueryData < UserProfile > (
121+ queryKeys . userProfile . current ( )
122+ ) ;
123+ if ( cachedProfile && cachedProfile . id !== profile . id ) {
124+ if ( __DEV__ ) {
125+ console . debug (
126+ "[AuthContext] Rehydrated cache belonged to a different user; clearing"
127+ ) ;
128+ }
129+ try {
130+ queryClient . clear ( ) ;
131+ } catch ( clearError ) {
132+ console . warn ( "Failed to clear stale in-memory cache:" , clearError ) ;
133+ }
134+ try {
135+ await clearQueryCache ( ) ;
136+ } catch ( cacheError ) {
137+ console . warn ( "Failed to clear stale persisted cache:" , cacheError ) ;
138+ }
139+ }
140+ setUserInfo ( {
141+ email : profile . email ,
142+ name : profile . name ,
143+ id : profile . id ,
144+ username : profile . username ,
145+ } ) ;
146+ }
147+ } catch ( profileError ) {
148+ console . error ( "Failed to fetch user profile:" , profileError ) ;
149+ // Don't fail login if profile fetch fails
114150 }
115- } catch ( profileError ) {
116- console . error ( "Failed to fetch user profile:" , profileError ) ;
117- // Don't fail login if profile fetch fails
118- }
119- } , [ ] ) ;
151+ } ,
152+ [ queryClient ]
153+ ) ;
120154
121155 // Plain async fn (no useCallback): identity doesn't need to be stable for
122156 // memoization, and taking `service` explicitly lets cold-start callers pass
@@ -161,38 +195,59 @@ export function AuthProvider({ children }: AuthProviderProps) {
161195 } , [ ] ) ;
162196
163197 const logout = useCallback ( async ( ) => {
198+ // Each cleanup step has its own try/catch so a failure in one does not
199+ // skip the others. resetAuthState() always runs last to put the UI in a
200+ // known logged-out state even if disk writes failed.
164201 try {
165202 await clearAuth ( ) ;
166- // Clear user preferences to ensure fresh state for next user
167- try {
168- await generalStorage . removeItem ( USER_PREFERENCES_KEY ) ;
169- } catch ( prefsError ) {
170- console . warn ( "Failed to clear user preferences during logout:" , prefsError ) ;
171- }
172- // Reset the persisted data region so the next user is prompted via the
173- // login-screen picker rather than silently inheriting this session's
174- // region (the extension background worker clears `cal_region` on logout
175- // too — keep the two surfaces in sync).
176- try {
177- await clearRegion ( ) ;
178- } catch ( regionError ) {
179- console . warn ( "Failed to clear data region during logout:" , regionError ) ;
180- }
181- // Clear all cached queries to ensure fresh data on re-login
182- try {
183- await clearQueryCache ( ) ;
184- } catch ( cacheError ) {
185- console . warn ( "Failed to clear query cache during logout:" , cacheError ) ;
186- }
187- resetAuthState ( ) ;
188- } catch ( error ) {
189- const message = getErrorMessage ( error ) ;
190- console . error ( "Failed to logout" , message ) ;
203+ } catch ( clearAuthError ) {
204+ const message = getErrorMessage ( clearAuthError ) ;
205+ console . warn ( "Failed to clear auth tokens during logout:" , message ) ;
191206 if ( __DEV__ ) {
192- console . debug ( "[AuthContext] logout failed" , { message, stack : getErrorStack ( error ) } ) ;
207+ console . debug ( "[AuthContext] clearAuth failed" , {
208+ message,
209+ stack : getErrorStack ( clearAuthError ) ,
210+ } ) ;
193211 }
194212 }
195- } , [ clearAuth , resetAuthState ] ) ;
213+ // Clear user preferences to ensure fresh state for next user
214+ try {
215+ await generalStorage . removeItem ( USER_PREFERENCES_KEY ) ;
216+ } catch ( prefsError ) {
217+ console . warn ( "Failed to clear user preferences during logout:" , prefsError ) ;
218+ }
219+ // Reset the persisted data region so the next user is prompted via the
220+ // login-screen picker rather than silently inheriting this session's
221+ // region (the extension background worker clears `cal_region` on logout
222+ // too — keep the two surfaces in sync).
223+ try {
224+ await clearRegion ( ) ;
225+ } catch ( regionError ) {
226+ console . warn ( "Failed to clear data region during logout:" , regionError ) ;
227+ }
228+ // Memory first, then disk. PersistQueryClient throttles persists
229+ // (cache.config.ts: throttleTime = 1000ms); clearing the in-memory cache
230+ // before the disk wipe prevents an in-flight persist from re-writing the
231+ // previous user's data right after we erased it.
232+ try {
233+ queryClient . clear ( ) ;
234+ } catch ( clearError ) {
235+ console . warn ( "Failed to clear in-memory query cache during logout:" , clearError ) ;
236+ }
237+ try {
238+ await clearQueryCache ( ) ;
239+ } catch ( cacheError ) {
240+ console . warn ( "Failed to clear persisted query cache during logout:" , cacheError ) ;
241+ }
242+ // Wipe the home-screen widget so the previous user's meetings don't
243+ // linger after sign-out (no-op on web).
244+ try {
245+ await clearWidgetBookings ( ) ;
246+ } catch ( widgetError ) {
247+ console . warn ( "Failed to clear widget bookings during logout:" , widgetError ) ;
248+ }
249+ resetAuthState ( ) ;
250+ } , [ clearAuth , resetAuthState , queryClient ] ) ;
196251
197252 // Handle OAuth authentication. `service` is passed explicitly so the boot
198253 // effect can hand in a freshly-rebuilt service on cold-start region
0 commit comments