Skip to content

Commit 3e9540a

Browse files
fix: stabilize auth session persistence and prevent loading deadlocks
1 parent 304c408 commit 3e9540a

4 files changed

Lines changed: 39 additions & 61 deletions

File tree

components/AuthProvider.tsx

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import React, { createContext, useContext, useEffect, useState } from "react";
44
import { User } from "@/lib/types";
55
import {
6-
getCurrentUser,
76
logoutUser,
87
registerUser,
98
authenticateUser,
@@ -54,7 +53,6 @@ const AuthContext = createContext<AuthContextValue | undefined>(undefined);
5453
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
5554
children,
5655
}) => {
57-
// Always start null - this matches the server render and prevents hydration mismatch
5856
const [user, setUser] = useState<User | null>(null);
5957
const [loading, setLoading] = useState(true);
6058

@@ -66,46 +64,40 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
6664
useEffect(() => {
6765
let mounted = true;
6866

69-
// Fast path: restore from sessionStorage immediately (no network, no flash)
67+
// Fast path: show cached user instantly while auth initializes
7068
const cached = readUserCache();
7169
if (cached) {
7270
setUser(cached);
7371
setLoading(false);
7472
}
7573

76-
// Then validate with Supabase in background (with timeout to prevent stuck loading)
77-
const initializeAuth = async () => {
78-
try {
79-
const timeoutPromise = new Promise<null>((resolve) =>
80-
setTimeout(() => resolve(null), 8000)
81-
);
82-
const currentUser = await Promise.race([getCurrentUser(), timeoutPromise]);
83-
if (mounted) setUserAndCache(currentUser);
84-
} catch {
85-
if (mounted) setUserAndCache(null);
86-
} finally {
87-
if (mounted) setLoading(false);
88-
}
89-
};
90-
91-
initializeAuth();
92-
74+
// onAuthStateChange is the single source of truth for auth state.
75+
// It fires INITIAL_SESSION immediately on registration, so no separate
76+
// initializeAuth() call is needed. Having two concurrent getUser() calls
77+
// was causing token refresh races that invalidated sessions via Supabase's
78+
// refresh token reuse detection.
9379
const unsubscribe = onAuthStateChange((updatedUser) => {
94-
if (mounted) setUserAndCache(updatedUser);
80+
if (mounted) {
81+
setUserAndCache(updatedUser);
82+
setLoading(false);
83+
}
9584
});
9685

86+
// Safety fallback: resolve loading if INITIAL_SESSION never fires
87+
const timeout = setTimeout(() => {
88+
if (mounted) setLoading(false);
89+
}, 8000);
90+
9791
return () => {
9892
mounted = false;
93+
clearTimeout(timeout);
9994
unsubscribe();
10095
};
10196
}, []);
10297

10398
const login = async (email: string, password: string) => {
10499
const res = await authenticateUser(email, password);
105-
if (res.ok) {
106-
const currentUser = await getCurrentUser();
107-
setUserAndCache(currentUser);
108-
}
100+
// onAuthStateChange fires SIGNED_IN and updates user state automatically
109101
return res;
110102
};
111103

@@ -115,10 +107,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
115107
displayName?: string
116108
) => {
117109
const res = await registerUser(email, password, displayName);
118-
if (res.ok && !res.needsEmailConfirmation) {
119-
const currentUser = await getCurrentUser();
120-
setUserAndCache(currentUser);
121-
}
122110
return res;
123111
};
124112

lib/PromptsContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function PromptsProvider({ children }: { children: React.ReactNode }) {
5151
useEffect(() => {
5252
loadPrompts();
5353
// eslint-disable-next-line react-hooks/exhaustive-deps
54-
}, [user]);
54+
}, [user?.id]);
5555

5656
const addOptimistic = (prompt: Prompt) => {
5757
setPrompts((prev) => [prompt, ...prev]);

lib/auth.ts

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -187,27 +187,9 @@ export async function logoutUser(): Promise<void> {
187187
console.error("[auth] Logout error:", error);
188188
}
189189

190-
// Clear all session storage (including auth tokens)
191-
if (typeof window !== 'undefined') {
192-
try {
193-
window.sessionStorage.clear();
194-
console.log("[auth] Session storage cleared");
195-
} catch (e) {
196-
console.warn("[auth] Could not clear session storage:", e);
197-
}
198-
}
199-
200190
console.log("[auth] Logout successful");
201191
} catch (err) {
202192
console.error("[auth] Logout error:", err);
203-
// Even if logout fails, clear session storage
204-
if (typeof window !== 'undefined') {
205-
try {
206-
window.sessionStorage.clear();
207-
} catch (e) {
208-
console.warn("[auth] Could not clear session storage:", e);
209-
}
210-
}
211193
}
212194
}
213195

@@ -245,27 +227,36 @@ export async function deleteAccount(): Promise<
245227

246228

247229
// Listen to auth state changes
248-
230+
249231
export function onAuthStateChange(
250232
callback: (user: User | null) => void
251233
): () => void {
252234
const {
253235
data: { subscription },
254-
} = supabase.auth.onAuthStateChange(async (event, session) => {
236+
} = supabase.auth.onAuthStateChange((event, session) => {
255237
console.log("[auth] Auth state change event:", event, "Session exists:", !!session);
256-
257-
if (event === 'SIGNED_OUT') {
258-
console.log("[auth] User signed out");
238+
239+
if (!session?.user) {
259240
callback(null);
260241
return;
261242
}
262-
263-
if (session?.user) {
264-
const user = await getCurrentUser();
265-
callback(user);
266-
} else {
267-
callback(null);
268-
}
243+
244+
// Build user directly from the session — no extra network calls.
245+
// All needed data (id, email, display_name, timestamps) is already present
246+
// in the session object. Doing an async DB fetch here caused timing bugs:
247+
// multiple in-flight fetches on rapid page loads led to stale callbacks
248+
// calling setLoading(false) out of order and leaving the app stuck.
249+
const authUser = session.user;
250+
callback({
251+
id: authUser.id,
252+
email: authUser.email || "",
253+
displayName:
254+
(authUser.user_metadata?.display_name as string | undefined) ||
255+
authUser.email?.split("@")[0] ||
256+
"User",
257+
createdAt: authUser.created_at,
258+
updatedAt: authUser.updated_at || authUser.created_at,
259+
});
269260
});
270261

271262
return () => {

lib/supabase.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export const supabase = createClient<Database>(
3535
storageKey: 'closednote-auth',
3636
autoRefreshToken: true,
3737
detectSessionInUrl: true,
38-
flowType: 'pkce',
3938
},
4039
global: {
4140
headers: {

0 commit comments

Comments
 (0)