From c4371f7886dfd0df82feba053ef35b6ddf63a2f2 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Tue, 12 May 2026 05:14:12 -0400 Subject: [PATCH] fixed login race condition --- frontend/src/contexts/AuthContext.jsx | 89 ++++++++++++++++----------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 43701c6..317a570 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect } from "react"; +import { createContext, useContext, useSyncExternalStore } from "react"; import { onAuthStateChanged, signInWithEmailAndPassword, @@ -17,48 +17,67 @@ const DEFAULT_TIER_LIMITS = { allowed_channels: ["email"], }; -export function AuthProvider({ children }) { - const [currentUser, setCurrentUser] = useState(null); - const [userTier, setUserTier] = useState("free"); - const [tierLimits, setTierLimits] = useState(DEFAULT_TIER_LIMITS); - const [isSubscribed, setIsSubscribed] = useState(false); - const [loading, setLoading] = useState(true); +// External auth store - lives outside React +const createAuthStore = () => { + let state = { + currentUser: null, + userTier: "free", + tierLimits: DEFAULT_TIER_LIMITS, + isSubscribed: false, + loading: true, + }; + const listeners = new Set(); - const resetAccountState = () => { - setIsSubscribed(false); + const notify = () => listeners.forEach((fn) => fn()); + + const setState = (partial) => { + state = { ...state, ...partial }; + notify(); }; const fetchAccount = async () => { try { const data = await getAccount(); - setUserTier(data.subscription_tier || "free"); - setTierLimits(data.tier_limits || DEFAULT_TIER_LIMITS); - setIsSubscribed( - data.subscription_status === "active" || - data.subscription_status === "trialing", - ); + setState({ + userTier: data.subscription_tier || "free", + tierLimits: data.tier_limits || DEFAULT_TIER_LIMITS, + isSubscribed: + data.subscription_status === "active" || + data.subscription_status === "trialing", + }); } catch { - resetAccountState(); + setState({ isSubscribed: false }); } }; - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, async (user) => { - setCurrentUser(user); - if (user) { - await fetchAccount(); - } else { - resetAccountState(); - } - setLoading(false); - }); - return () => unsubscribe(); - }, []); + // Set up Firebase listener once, outside React + onAuthStateChanged(auth, async (user) => { + setState({ loading: true, currentUser: user }); + if (user) { + await fetchAccount(); + } else { + setState({ isSubscribed: false, userTier: "free", tierLimits: DEFAULT_TIER_LIMITS }); + } + setState({ loading: false }); + }); + + return { + subscribe: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + getSnapshot: () => state, + refreshAccount: fetchAccount, + }; +}; + +const authStore = createAuthStore(); + +export function AuthProvider({ children }) { + const state = useSyncExternalStore(authStore.subscribe, authStore.getSnapshot); const signup = (email, password) => createUserWithEmailAndPassword(auth, email, password); - const login = (email, password) => signInWithEmailAndPassword(auth, email, password); - const logout = () => signOut(auth); const sendEmailVerification = () => { @@ -70,28 +89,24 @@ export function AuthProvider({ children }) { const reloadUser = async () => { if (auth.currentUser) { await auth.currentUser.reload(); - setCurrentUser({ ...auth.currentUser }); return auth.currentUser; } return null; }; const value = { - currentUser, - userTier, - tierLimits, - isSubscribed, + ...state, signup, login, logout, sendEmailVerification, reloadUser, - refreshAccount: fetchAccount, + refreshAccount: authStore.refreshAccount, }; return ( - {!loading && children} + {!state.loading && children} ); }