Improve authentication state management and user session handling

Refactors the `AuthProviderComponent` in `useAuth.tsx` to enhance the handling of Supabase authentication state changes. Updates console logs for better debugging, prioritizes setting pending email state before early returns, and refines the logic for `SIGNED_IN`, `INITIAL_SESSION`, and `SIGNED_OUT` events. Also defers Novu updates and profile fetches to prevent blocking the authentication process.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e7030bef-c72b-4948-9378-ee5e345d91ce
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/e7030bef-c72b-4948-9378-ee5e345d91ce/zQglahT
This commit is contained in:
pac7
2025-10-12 17:05:15 +00:00
parent da3a5247a1
commit 0b8fe60823
2 changed files with 40 additions and 55 deletions

View File

@@ -37,3 +37,7 @@ externalPort = 80
[[ports]]
localPort = 5001
externalPort = 3000
[[ports]]
localPort = 37143
externalPort = 3001

View File

@@ -157,21 +157,25 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
}, [loading]);
useEffect(() => {
console.log('[Auth] Initializing auth provider');
// CRITICAL: Set up listener FIRST to catch all events
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none');
if (!isMountedRef.current) return;
console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none', 'Has session:', !!session);
// Extract email info early for cleanup
const currentEmail = session?.user?.email;
const newEmailPending = session?.user?.new_email;
// CRITICAL: Always clear/update pending email state BEFORE any early returns
setPendingEmail(newEmailPending ?? null);
// Clear any error
setSessionError(null);
// Explicitly handle SIGNED_IN and INITIAL_SESSION events
// Update session and user state based on event
if (event === 'SIGNED_IN' && session) {
console.log('[Auth] SIGNED_IN detected, setting session and user');
setSession(session);
@@ -179,33 +183,33 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
sessionVerifiedRef.current = true;
} else if (event === 'INITIAL_SESSION') {
if (session?.user) {
console.log('[Auth] INITIAL_SESSION detected with session, setting user');
console.log('[Auth] INITIAL_SESSION with user, setting session');
setSession(session);
setUser(session.user);
sessionVerifiedRef.current = true;
} else {
console.log('[Auth] INITIAL_SESSION detected with NO session - user not logged in');
console.log('[Auth] INITIAL_SESSION with no user - setting loading to false');
setSession(null);
setUser(null);
setProfile(null);
sessionVerifiedRef.current = false;
// CRITICAL: Set loading to false immediately when no session
setLoading(false);
return; // Exit early, no need to fetch profile
}
} else if (event === 'SIGNED_OUT') {
console.log('[Auth] SIGNED_OUT detected, clearing session');
console.log('[Auth] SIGNED_OUT detected, clearing all state');
setSession(null);
setUser(null);
setProfile(null);
sessionVerifiedRef.current = false;
setLoading(false);
return; // Exit early, no need to fetch profile
} else {
setSession(session);
setUser(session?.user ?? null);
}
// Track pending email changes
setPendingEmail(newEmailPending ?? null);
// Detect confirmed email change: email changed AND no longer pending
if (
session?.user &&
@@ -222,8 +226,6 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Defer Novu update and notifications to avoid blocking auth
const oldEmail = previousEmailRef.current;
novuUpdateTimeoutRef.current = setTimeout(async () => {
if (!isMountedRef.current) return;
try {
// Update Novu subscriber with confirmed email
const { notificationService } = await import('@/lib/notificationService');
@@ -271,19 +273,20 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
previousEmailRef.current = currentEmail;
}
// Handle profile fetching for authenticated users
if (session?.user) {
// Clear any existing profile fetch timeout
if (profileFetchTimeoutRef.current) {
clearTimeout(profileFetchTimeoutRef.current);
}
// Defer profile fetch to avoid deadlock
const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session !== null;
console.log('[Auth] Profile fetch deferred, shouldWaitForProfile:', shouldWaitForProfile, 'event:', event);
// Only wait for profile on initial auth events
const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION');
console.log('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile);
profileFetchTimeoutRef.current = setTimeout(() => {
if (!isMountedRef.current) return;
fetchProfile(session.user.id, 0, () => {
if (isMountedRef.current && shouldWaitForProfile) {
if (shouldWaitForProfile) {
console.log('[Auth] Profile fetch complete, setting loading to false');
setLoading(false);
}
@@ -291,27 +294,15 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
profileFetchTimeoutRef.current = null;
}, 0);
} else {
if (isMountedRef.current) {
// No session/user - clear profile and resolve loading
setProfile(null);
// CRITICAL: Always resolve loading when there's no session
if (event === 'INITIAL_SESSION' || event === 'SIGNED_OUT') {
console.log('[Auth] No session for event:', event, '- setting loading to false');
setLoading(false);
}
}
}
// CRITICAL: Always set loading false for non-session events
if (event !== 'SIGNED_IN' && event !== 'INITIAL_SESSION') {
console.log('[Auth] Setting loading to false immediately for event:', event);
console.log('[Auth] No user, setting loading to false');
setLoading(false);
}
});
// THEN get initial session
// THEN get initial session (this may trigger INITIAL_SESSION event)
supabase.auth.getSession().then(({ data: { session }, error }) => {
if (!isMountedRef.current) return;
if (error) {
console.error('[Auth] Initial session fetch error:', error);
setSessionError(error.message);
@@ -319,41 +310,30 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
return;
}
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
sessionVerifiedRef.current = true;
// Fetch profile with completion callback
fetchProfile(session.user.id, 0, () => {
if (isMountedRef.current) {
setLoading(false);
}
});
} else {
setLoading(false);
}
// Note: onAuthStateChange will handle the INITIAL_SESSION event
// This is just a backup in case the event doesn't fire
console.log('[Auth] getSession completed, session exists:', !!session);
});
// Add a safety timeout to force loading to resolve
// Add a STRONG safety timeout to force loading to resolve
loadingTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && loadingStateRef.current) {
console.warn('[Auth] Loading timeout reached after 5 seconds, forcing loading to false');
if (loadingStateRef.current) {
console.warn('[Auth] ⚠️ SAFETY TIMEOUT: Forcing loading to false after 3 seconds');
setLoading(false);
}
}, 5000);
}, 3000);
// Session verification fallback after 2 seconds
// Session verification fallback
const verificationTimeout = setTimeout(() => {
if (!sessionVerifiedRef.current && isMountedRef.current) {
console.log('[Auth] Session not verified, attempting manual verification');
if (!sessionVerifiedRef.current) {
console.log('[Auth] Session not verified after 2s, attempting manual verification');
verifySession();
}
}, 2000);
// Handle page visibility changes
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && isMountedRef.current) {
if (document.visibilityState === 'visible') {
console.log('[Auth] Tab became visible, verifying session');
verifySession();
}
@@ -362,6 +342,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
console.log('[Auth] Cleaning up auth provider');
isMountedRef.current = false;
subscription.unsubscribe();
clearTimeout(verificationTimeout);