@@ -17,6 +17,7 @@ import { ANALYTICS_EVENTS } from "@/types/analytics";
1717const log = logger . scope ( "auth-store" ) ;
1818
1919let refreshPromise : Promise < void > | null = null ;
20+ let initializePromise : Promise < boolean > | null = null ;
2021
2122const REFRESH_MAX_RETRIES = 3 ;
2223const REFRESH_INITIAL_DELAY_MS = 1000 ;
@@ -305,124 +306,138 @@ export const useAuthStore = create<AuthState>()(
305306 } ,
306307
307308 initializeOAuth : async ( ) => {
308- // Wait for zustand hydration from async storage
309- if ( ! useAuthStore . persist . hasHydrated ( ) ) {
310- await new Promise < void > ( ( resolve ) => {
311- useAuthStore . persist . onFinishHydration ( ( ) => resolve ( ) ) ;
312- } ) ;
309+ // If initialization is already in progress, wait for it
310+ if ( initializePromise ) {
311+ log . debug ( "OAuth initialization already in progress, waiting..." ) ;
312+ return initializePromise ;
313313 }
314314
315- const state = get ( ) ;
316-
317- if ( state . storedTokens ) {
318- const tokens = state . storedTokens ;
319- const now = Date . now ( ) ;
320- const isExpired = tokens . expiresAt <= now ;
321-
322- set ( {
323- oauthAccessToken : tokens . accessToken ,
324- oauthRefreshToken : tokens . refreshToken ,
325- tokenExpiry : tokens . expiresAt ,
326- cloudRegion : tokens . cloudRegion ,
327- } ) ;
328-
329- if ( isExpired ) {
330- try {
331- await get ( ) . refreshAccessToken ( ) ;
332- } catch ( error ) {
333- log . error ( "Failed to refresh expired token:" , error ) ;
334- set ( { storedTokens : null , isAuthenticated : false } ) ;
335- return false ;
336- }
315+ const doInitialize = async ( ) : Promise < boolean > => {
316+ // Wait for zustand hydration from async storage
317+ if ( ! useAuthStore . persist . hasHydrated ( ) ) {
318+ await new Promise < void > ( ( resolve ) => {
319+ useAuthStore . persist . onFinishHydration ( ( ) => resolve ( ) ) ;
320+ } ) ;
337321 }
338322
339- // Re-fetch tokens after potential refresh to get updated values
340- const currentTokens = get ( ) . storedTokens ;
341- if ( ! currentTokens ) {
342- return false ;
343- }
323+ const state = get ( ) ;
344324
345- const apiHost = getCloudUrlFromRegion ( currentTokens . cloudRegion ) ;
346- const projectId = currentTokens . scopedTeams ?. [ 0 ] ;
325+ if ( state . storedTokens ) {
326+ const tokens = state . storedTokens ;
327+ const now = Date . now ( ) ;
328+ const isExpired = tokens . expiresAt <= now ;
347329
348- if ( ! projectId ) {
349- log . error ( "No project ID found in stored tokens" ) ;
350- get ( ) . logout ( ) ;
351- return false ;
352- }
330+ set ( {
331+ oauthAccessToken : tokens . accessToken ,
332+ oauthRefreshToken : tokens . refreshToken ,
333+ tokenExpiry : tokens . expiresAt ,
334+ cloudRegion : tokens . cloudRegion ,
335+ } ) ;
353336
354- const client = new PostHogAPIClient (
355- currentTokens . accessToken ,
356- apiHost ,
357- async ( ) => {
358- await get ( ) . refreshAccessToken ( ) ;
359- const token = get ( ) . oauthAccessToken ;
360- if ( ! token ) {
361- throw new Error ( "No access token after refresh" ) ;
337+ if ( isExpired ) {
338+ try {
339+ await get ( ) . refreshAccessToken ( ) ;
340+ } catch ( error ) {
341+ log . error ( "Failed to refresh expired token:" , error ) ;
342+ set ( { storedTokens : null , isAuthenticated : false } ) ;
343+ return false ;
362344 }
363- return token ;
364- } ,
365- projectId ,
366- ) ;
367-
368- try {
369- const user = await client . getCurrentUser ( ) ;
345+ }
370346
371- set ( {
372- isAuthenticated : true ,
373- client ,
374- projectId ,
375- } ) ;
347+ // Re-fetch tokens after potential refresh to get updated values
348+ const currentTokens = get ( ) . storedTokens ;
349+ if ( ! currentTokens ) {
350+ return false ;
351+ }
376352
377- get ( ) . scheduleTokenRefresh ( ) ;
353+ const apiHost = getCloudUrlFromRegion ( currentTokens . cloudRegion ) ;
354+ const projectId = currentTokens . scopedTeams ?. [ 0 ] ;
378355
379- // Use distinct_id to match web sessions (same as PostHog web app)
380- const distinctId = user . distinct_id || user . email ;
381- identifyUser ( distinctId , {
382- email : user . email ,
383- uuid : user . uuid ,
384- project_id : projectId . toString ( ) ,
385- region : tokens . cloudRegion ,
386- } ) ;
356+ if ( ! projectId ) {
357+ log . error ( "No project ID found in stored tokens" ) ;
358+ get ( ) . logout ( ) ;
359+ return false ;
360+ }
387361
388- trpcVanilla . analytics . setUserId . mutate ( {
389- userId : distinctId ,
390- properties : {
391- email : user . email ,
392- uuid : user . uuid ,
393- project_id : projectId . toString ( ) ,
394- region : tokens . cloudRegion ,
362+ const client = new PostHogAPIClient (
363+ currentTokens . accessToken ,
364+ apiHost ,
365+ async ( ) => {
366+ await get ( ) . refreshAccessToken ( ) ;
367+ const token = get ( ) . oauthAccessToken ;
368+ if ( ! token ) {
369+ throw new Error ( "No access token after refresh" ) ;
370+ }
371+ return token ;
395372 } ,
396- } ) ;
397-
398- return true ;
399- } catch ( error ) {
400- log . error ( "Failed to validate OAuth session:" , error ) ;
373+ projectId ,
374+ ) ;
401375
402- // Network errors from fetch are TypeError, wrapped by fetcher.ts as cause
403- const isNetworkError =
404- error instanceof Error && error . cause instanceof TypeError ;
376+ try {
377+ const user = await client . getCurrentUser ( ) ;
405378
406- if ( isNetworkError ) {
407- log . warn (
408- "Network error during session validation - keeping session active" ,
409- ) ;
410379 set ( {
411380 isAuthenticated : true ,
412381 client,
413382 projectId,
414383 } ) ;
384+
415385 get ( ) . scheduleTokenRefresh ( ) ;
386+
387+ // Use distinct_id to match web sessions (same as PostHog web app)
388+ const distinctId = user . distinct_id || user . email ;
389+ identifyUser ( distinctId , {
390+ email : user . email ,
391+ uuid : user . uuid ,
392+ project_id : projectId . toString ( ) ,
393+ region : tokens . cloudRegion ,
394+ } ) ;
395+
396+ trpcVanilla . analytics . setUserId . mutate ( {
397+ userId : distinctId ,
398+ properties : {
399+ email : user . email ,
400+ uuid : user . uuid ,
401+ project_id : projectId . toString ( ) ,
402+ region : tokens . cloudRegion ,
403+ } ,
404+ } ) ;
405+
416406 return true ;
417- }
407+ } catch ( error ) {
408+ log . error ( "Failed to validate OAuth session:" , error ) ;
409+
410+ // Network errors from fetch are TypeError, wrapped by fetcher.ts as cause
411+ const isNetworkError =
412+ error instanceof Error && error . cause instanceof TypeError ;
413+
414+ if ( isNetworkError ) {
415+ log . warn (
416+ "Network error during session validation - keeping session active" ,
417+ ) ;
418+ set ( {
419+ isAuthenticated : true ,
420+ client,
421+ projectId,
422+ } ) ;
423+ get ( ) . scheduleTokenRefresh ( ) ;
424+ return true ;
425+ }
418426
419- // For auth errors (401/403) or unknown errors, clear the session
420- set ( { storedTokens : null , isAuthenticated : false } ) ;
421- return false ;
427+ // For auth errors (401/403) or unknown errors, clear the session
428+ set ( { storedTokens : null , isAuthenticated : false } ) ;
429+ return false ;
430+ }
422431 }
423- }
424432
425- return state . isAuthenticated ;
433+ return state . isAuthenticated ;
434+ } ;
435+
436+ initializePromise = doInitialize ( ) . finally ( ( ) => {
437+ initializePromise = null ;
438+ } ) ;
439+
440+ return initializePromise ;
426441 } ,
427442
428443 logout : ( ) => {
0 commit comments