/** * User Profile Store - Platform-aware user profile management * Uses Firebase abstraction layer for cross-platform support */ import { create } from 'zustand'; import { doc } from '../firebase'; import type { UserProfile } from '../services/authServices'; export interface ProfileCacheEntry { profile: UserProfile | null; loading: boolean; error: string | null; lastFetched?: number; } interface UserProfileStoreState { profiles: Record; subscriptions: Record void>; ensureSubscription: (uid: string) => void; refreshProfile: (uid: string) => Promise; invalidateProfile: (uid: string) => Promise; removeProfile: (uid: string) => void; clearAll: () => void; } const buildEntry = ( existing: ProfileCacheEntry | undefined, overrides: Partial ): ProfileCacheEntry => ({ profile: existing?.profile ?? null, loading: existing?.loading ?? false, error: existing?.error ?? null, lastFetched: existing?.lastFetched, ...overrides, }); const snapshotExists = (docSnap: any): boolean => { const existsValue = docSnap?.exists; if (typeof existsValue === 'function') { try { return !!existsValue.call(docSnap); } catch { return false; } } return !!existsValue; }; export const useUserProfileStore = create((set, get) => ({ profiles: {}, subscriptions: {}, ensureSubscription: (uid: string) => { if (!uid) { return; } // If already subscribed, don't create another subscription if (get().subscriptions[uid]) { return; } const existing = get().profiles[uid]; if (existing?.loading) { return; } // If we have cached data, set up a real-time listener if (existing?.lastFetched) { const userDocRef = doc('users', uid); const unsubscribe = userDocRef.onSnapshot( (docSnap: any) => { const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { profile, loading: false, error: profile ? null : 'Profile not found', lastFetched: Date.now(), }), }, })); }, (error: any) => { set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: false, error: error.message || 'Failed to fetch profile', }), }, })); } ); set((state) => ({ subscriptions: { ...state.subscriptions, [uid]: unsubscribe, }, })); return; } // Initial fetch void get().refreshProfile(uid).catch((error) => { set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch profile', }), }, })); }); }, refreshProfile: async (uid: string) => { if (!uid) { return null; } set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: true, error: null, }), }, })); try { const userDocRef = doc('users', uid); const docSnap = await userDocRef.get(); const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { profile, loading: false, error: profile ? null : 'Profile not found', lastFetched: Date.now(), }), }, })); // Set up real-time listener after initial fetch if not already subscribed if (!get().subscriptions[uid] && profile) { const unsubscribe = userDocRef.onSnapshot( (docSnap: any) => { const updatedProfile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { profile: updatedProfile, loading: false, error: updatedProfile ? null : 'Profile not found', lastFetched: Date.now(), }), }, })); }, (error: any) => { set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: false, error: error.message || 'Failed to fetch profile', }), }, })); } ); set((state) => ({ subscriptions: { ...state.subscriptions, [uid]: unsubscribe, }, })); } return profile; } catch (error) { set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch profile', }), }, })); throw error; } }, invalidateProfile: async (uid: string) => { if (!uid) { return null; } set((state) => ({ profiles: { ...state.profiles, [uid]: buildEntry(state.profiles[uid], { loading: true, error: null, lastFetched: undefined, }), }, })); return get().refreshProfile(uid); }, removeProfile: (uid: string) => { // Unsubscribe from listener if exists const unsubscribe = get().subscriptions[uid]; if (unsubscribe) { unsubscribe(); } set((state) => { const { [uid]: _removedProfile, ...restProfiles } = state.profiles; const { [uid]: _removedSub, ...restSubs } = state.subscriptions; return { profiles: restProfiles, subscriptions: restSubs, }; }); }, clearAll: () => { // Unsubscribe from all listeners const subscriptions = get().subscriptions; Object.values(subscriptions).forEach((unsubscribe) => { unsubscribe(); }); set({ profiles: {}, subscriptions: {} }); }, })); export const getUserProfileFromCache = (uid: string): UserProfile | null => { if (!uid) { return null; } return useUserProfileStore.getState().profiles[uid]?.profile ?? null; };