build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import SplashPage from '../pages/SplashPage';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { supabase } from '../supabase';
|
||||
import {
|
||||
buildCacheKey,
|
||||
readCache,
|
||||
writeCache,
|
||||
clearUserCache,
|
||||
} from '../utils/localCache';
|
||||
|
||||
export interface Profile {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
full_name: string;
|
||||
phone: string;
|
||||
country_of_residence: string;
|
||||
avatar_path: string | null;
|
||||
fcm_token: string | null;
|
||||
notification_push_enabled: boolean;
|
||||
notification_email_enabled: boolean;
|
||||
notification_sms_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type ProfileStatus = 'idle' | 'loading' | 'loaded' | 'missing' | 'error';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
profile: Profile | null;
|
||||
profileStatus: ProfileStatus;
|
||||
refreshProfile: () => Promise<Profile | null>;
|
||||
setProfile: (profile: Profile | null) => void;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const BYPASS_AUTH = false;
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
profile: null,
|
||||
profileStatus: BYPASS_AUTH ? 'idle' : 'loading',
|
||||
refreshProfile: async () => null,
|
||||
setProfile: () => {},
|
||||
signOut: async () => {},
|
||||
});
|
||||
|
||||
const withTimeout = async <T,>(
|
||||
request: PromiseLike<T>,
|
||||
timeoutMs: number,
|
||||
label: string
|
||||
): Promise<T> => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`${label} timed out. Please check your connection and try again.`
|
||||
)
|
||||
);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([Promise.resolve(request), timeout]);
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [profile, setProfileState] = useState<Profile | null>(null);
|
||||
const [profileStatus, setProfileStatus] = useState<ProfileStatus>(
|
||||
BYPASS_AUTH ? 'idle' : 'loading'
|
||||
);
|
||||
const [initializing, setInitializing] = useState(!BYPASS_AUTH);
|
||||
const activeProfileFetchRef = useRef(0);
|
||||
const mountedRef = useRef(false);
|
||||
const currentUserIdRef = useRef<string | null>(null);
|
||||
|
||||
const applyProfile = useCallback((nextProfile: Profile | null) => {
|
||||
if (!mountedRef.current) return;
|
||||
setProfileState(nextProfile);
|
||||
setProfileStatus(nextProfile ? 'loaded' : 'missing');
|
||||
}, []);
|
||||
|
||||
const setProfile = useCallback(
|
||||
(nextProfile: Profile | null) => {
|
||||
activeProfileFetchRef.current += 1;
|
||||
applyProfile(nextProfile);
|
||||
},
|
||||
[applyProfile]
|
||||
);
|
||||
|
||||
const fetchProfile = useCallback(
|
||||
async (
|
||||
userId: string,
|
||||
options?: { showLoading?: boolean }
|
||||
): Promise<Profile | null> => {
|
||||
const fetchId = activeProfileFetchRef.current + 1;
|
||||
activeProfileFetchRef.current = fetchId;
|
||||
|
||||
if ((options?.showLoading ?? true) && mountedRef.current) {
|
||||
setProfileStatus('loading');
|
||||
}
|
||||
|
||||
const cacheKey = buildCacheKey(userId, 'profile');
|
||||
try {
|
||||
const cachedProfile = await readCache<Profile>(cacheKey);
|
||||
if (
|
||||
cachedProfile &&
|
||||
mountedRef.current &&
|
||||
fetchId === activeProfileFetchRef.current &&
|
||||
currentUserIdRef.current === userId
|
||||
) {
|
||||
applyProfile(cachedProfile);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AuthProvider] Failed to read cached profile', err);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await withTimeout(
|
||||
supabase.from('profiles').select('*').eq('id', userId).maybeSingle(),
|
||||
8000,
|
||||
'Profile loading'
|
||||
);
|
||||
|
||||
if (
|
||||
!mountedRef.current ||
|
||||
fetchId !== activeProfileFetchRef.current ||
|
||||
currentUserIdRef.current !== userId
|
||||
) {
|
||||
return data ? (data as Profile) : null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('[AuthProvider] Failed to fetch profile', error);
|
||||
setProfileState(null);
|
||||
setProfileStatus('error');
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextProfile = data ? (data as Profile) : null;
|
||||
applyProfile(nextProfile);
|
||||
if (nextProfile) {
|
||||
void writeCache(cacheKey, nextProfile);
|
||||
}
|
||||
return nextProfile;
|
||||
} catch (error) {
|
||||
if (
|
||||
mountedRef.current &&
|
||||
fetchId === activeProfileFetchRef.current &&
|
||||
currentUserIdRef.current === userId
|
||||
) {
|
||||
console.error('[AuthProvider] Profile fetch crashed', error);
|
||||
setProfileState(null);
|
||||
setProfileStatus('error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[applyProfile]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
|
||||
if (BYPASS_AUTH) {
|
||||
setInitializing(false);
|
||||
setProfileStatus('idle');
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
activeProfileFetchRef.current += 1;
|
||||
};
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
supabase.auth
|
||||
.getSession()
|
||||
.then(async ({ data }) => {
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
const currentUser = data.session?.user ?? null;
|
||||
|
||||
const keepSignedIn = localStorage.getItem('kumusha_keep_signed_in');
|
||||
if (currentUser && keepSignedIn === 'false') {
|
||||
currentUserIdRef.current = null;
|
||||
setUser(null);
|
||||
setProfileState(null);
|
||||
setProfileStatus('idle');
|
||||
await clearUserCache(currentUser.id);
|
||||
await supabase.auth.signOut();
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setInitializing(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentUserIdRef.current = currentUser?.id ?? null;
|
||||
setUser(currentUser);
|
||||
if (currentUser) {
|
||||
await fetchProfile(currentUser.id, { showLoading: false });
|
||||
} else {
|
||||
setProfileState(null);
|
||||
setProfileStatus('idle');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[AuthProvider] Failed to initialise session', error);
|
||||
if (!cancelled && mountedRef.current) {
|
||||
currentUserIdRef.current = null;
|
||||
setUser(null);
|
||||
setProfileState(null);
|
||||
setProfileStatus('missing');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setInitializing(false);
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(async (_event, session) => {
|
||||
const nextUser = session?.user ?? null;
|
||||
if (!mountedRef.current) return;
|
||||
const previousUserId = currentUserIdRef.current;
|
||||
currentUserIdRef.current = nextUser?.id ?? null;
|
||||
|
||||
if (nextUser) {
|
||||
if (nextUser.id !== previousUserId) {
|
||||
setProfileState(null);
|
||||
setProfileStatus('loading');
|
||||
}
|
||||
setUser(nextUser);
|
||||
await fetchProfile(nextUser.id, { showLoading: false });
|
||||
} else {
|
||||
activeProfileFetchRef.current += 1;
|
||||
setUser(null);
|
||||
setProfileState(null);
|
||||
setProfileStatus('idle');
|
||||
if (previousUserId) {
|
||||
await clearUserCache(previousUserId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
mountedRef.current = false;
|
||||
activeProfileFetchRef.current += 1;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [fetchProfile]);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
activeProfileFetchRef.current += 1;
|
||||
const currentUserId = currentUserIdRef.current;
|
||||
currentUserIdRef.current = null;
|
||||
setUser(null);
|
||||
setProfileState(null);
|
||||
setProfileStatus('idle');
|
||||
if (currentUserId) {
|
||||
await clearUserCache(currentUserId);
|
||||
}
|
||||
localStorage.removeItem('kumusha_keep_signed_in');
|
||||
await supabase.auth.signOut();
|
||||
}, []);
|
||||
|
||||
const refreshProfile = useCallback(() => {
|
||||
return user
|
||||
? fetchProfile(user.id, { showLoading: false })
|
||||
: Promise.resolve(null);
|
||||
}, [fetchProfile, user]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
profile,
|
||||
profileStatus,
|
||||
refreshProfile,
|
||||
setProfile,
|
||||
signOut,
|
||||
}),
|
||||
[profile, profileStatus, refreshProfile, setProfile, signOut, user]
|
||||
);
|
||||
|
||||
if (initializing) {
|
||||
return <SplashPage title="Send support to loved ones from anywhere" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user