import { useState, useCallback, useEffect, useRef, type ReactNode, } from 'react'; import { useNavigate } from 'react-router-dom'; import { atprotoClient } from '../api/client'; import type { SessionData } from '../api/types'; import { loadKnownAccounts, upsertAccount, removeKnownAccount, touchAccount, saveLastUsedDid, loadLastUsedDid, clearLastUsedDid, saveTestAccountCredentials, loadTestAccountCredentials, clearTestAccountCredentials, isTestAccount, saveDemoAccount, loadDemoAccount, clearDemoAccount, isDemoSession, DEMO_HANDLE, DEMO_DID, type KnownAccount, } from './accounts'; import { AuthContext, type AuthState } from './authContext'; import { prefetchLabelerDids } from '../labels/labelCache'; function prefetchLabelersFromPrefs(): void { const agent = atprotoClient.api; if (!agent) return; agent.getPreferences().then((prefs) => { const dids = prefs.moderationPrefs.labelers.map((l) => l.did); if (dids.length > 0) { prefetchLabelerDids(dids).catch(() => { }); } }).catch(() => { }); } async function fetchAndRegisterSession( sessionData: SessionData, ): Promise { try { const profile = await atprotoClient.getProfile(); const updated = { ...sessionData, handle: profile.handle }; upsertAccount({ did: updated.did, handle: updated.handle, displayName: profile.displayName, avatar: profile.avatar, }); prefetchLabelersFromPrefs(); return updated; } catch { upsertAccount({ did: sessionData.did, handle: sessionData.handle || sessionData.did, }); return sessionData; } } export function AuthProvider({ children }: { children: ReactNode }) { const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const [authError, setAuthError] = useState(null); const [accounts, setAccounts] = useState(() => loadKnownAccounts()); const navigate = useNavigate(); const initialRedirectDone = useRef(false); useEffect(() => { let cancelled = false; (async () => { try { await atprotoClient.initialize(); } catch (err: unknown) { if (!cancelled) { const message = err instanceof Error ? err.message : 'Failed to initialize OAuth client'; setAuthError(message); setLoading(false); } return; } if (cancelled) return; try { const result = await atprotoClient.initSession(); if (cancelled) return; if (result.error) { if (result.error === 'Redirecting to loopback IP...') { return; } setAuthError(result.error); } else if (result.session) { const updated = await fetchAndRegisterSession(result.session); if (!cancelled) { setSession(updated); setAccounts(loadKnownAccounts()); saveLastUsedDid(updated.did); } if (!cancelled && !initialRedirectDone.current && result.redirectUrl) { initialRedirectDone.current = true; const currentPath = window.location.pathname + window.location.search; if (currentPath === '/' || currentPath === '/login') { navigate(result.redirectUrl, { replace: true }); } } if (window.location.hash) { const cleanUrl = window.location.pathname + window.location.search; window.history.replaceState({}, '', cleanUrl); } } else { // No active OAuth session — try demo, test, or saved account // Check for demo test account first const isDemo = loadDemoAccount(); if (isDemo) { if (!cancelled) { // Re-initialize the public agent for unauthenticated API access await atprotoClient.initPublicAgent(); upsertAccount({ did: DEMO_DID, handle: DEMO_HANDLE, displayName: 'Test Account', }); saveLastUsedDid(DEMO_DID); setSession({ did: DEMO_DID, handle: DEMO_HANDLE, accessJwt: '', refreshJwt: '', }); setAccounts(loadKnownAccounts()); } } else { const testCreds = loadTestAccountCredentials(); const lastDid = loadLastUsedDid(); if (testCreds && lastDid) { try { const sessionData = await atprotoClient.restoreWithPassword( testCreds.service, testCreds.identifier, testCreds.password, ); if (!cancelled) { let updated = sessionData; try { const profile = await atprotoClient.getProfile(); updated = { ...sessionData, handle: profile.handle }; upsertAccount({ did: updated.did, handle: updated.handle, displayName: profile.displayName, avatar: profile.avatar, }); prefetchLabelersFromPrefs(); } catch {} touchAccount(updated.did); setSession(updated); setAccounts(loadKnownAccounts()); } } catch { clearTestAccountCredentials(); clearLastUsedDid(); } } else if (lastDid) { try { const sessionData = await atprotoClient.restoreSession(lastDid); if (!cancelled) { const updated = await fetchAndRegisterSession(sessionData); touchAccount(lastDid); setSession(updated); setAccounts(loadKnownAccounts()); } } catch { clearLastUsedDid(); } } } // end non-demo account restore } if (!cancelled) setLoading(false); } catch (err: unknown) { if (!cancelled) { const message = err instanceof Error ? err.message : 'Failed to initialize session'; setAuthError(message); setLoading(false); } } })(); return () => { cancelled = true; }; }, [navigate]); const login = useCallback(async (handle: string) => { setAuthError(null); try { await atprotoClient.signIn(handle.trim()); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Sign-in failed'; setAuthError(message); } }, []); const loginWithPassword = useCallback(async ( service: string, identifier: string, password: string, ) => { setAuthError(null); setLoading(true); try { const sessionData = await atprotoClient.loginWithPassword( service, identifier, password, ); let updated = sessionData; try { const profile = await atprotoClient.getProfile(); updated = { ...sessionData, handle: profile.handle }; upsertAccount({ did: updated.did, handle: updated.handle, displayName: profile.displayName, avatar: profile.avatar, }); prefetchLabelersFromPrefs(); } catch {} saveTestAccountCredentials({ service, identifier, password }); saveLastUsedDid(updated.did); setSession(updated); setAccounts(loadKnownAccounts()); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Login failed. Check your credentials.'; setAuthError(message); } finally { setLoading(false); } }, []); const loginAsTest = useCallback(async () => { setAuthError(null); setLoading(true); try { // Initialize public agent for unauthenticated API access await atprotoClient.initPublicAgent(); saveDemoAccount(); upsertAccount({ did: DEMO_DID, handle: DEMO_HANDLE, displayName: 'Test Account', }); saveLastUsedDid(DEMO_DID); setSession({ did: DEMO_DID, handle: DEMO_HANDLE, accessJwt: '', refreshJwt: '', }); setAccounts(loadKnownAccounts()); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to start test account'; setAuthError(message); } finally { setLoading(false); } }, []); const addAccount = useCallback(async (handle: string) => { setAuthError(null); try { await atprotoClient.signIn(handle.trim()); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to add account'; setAuthError(message); } }, []); const switchAccount = useCallback(async (did: string) => { setAuthError(null); setLoading(true); try { if (isDemoSession(did)) { // Switch back to demo account await atprotoClient.initPublicAgent(); saveLastUsedDid(DEMO_DID); setSession({ did: DEMO_DID, handle: DEMO_HANDLE, accessJwt: '', refreshJwt: '', }); setAccounts(loadKnownAccounts()); } else { const sessionData = await atprotoClient.restoreSession(did); const updated = await fetchAndRegisterSession(sessionData); touchAccount(did); setSession(updated); setAccounts(loadKnownAccounts()); } } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to switch account'; setAuthError(message); } finally { setLoading(false); } }, []); const logout = useCallback(() => { const currentSession = session; if (currentSession && isDemoSession(currentSession.did)) { clearDemoAccount(); } atprotoClient.logout().catch(() => {}); if (currentSession && isTestAccount(currentSession.did)) { clearTestAccountCredentials(); } clearLastUsedDid(); setSession(null); setAuthError(null); }, [session]); const removeAccount = useCallback(async (did: string) => { try { await atprotoClient.revokeSession(did); } catch { } removeKnownAccount(did); setAccounts(loadKnownAccounts()); if (session && session.did === did) { setSession(null); clearLastUsedDid(); } }, [session]); const clearError = useCallback(() => { setAuthError(null); }, []); const value: AuthState = { loading, session, authError, accounts, login, loginWithPassword, loginAsTest, logout, removeAccount, switchAccount, addAccount, clearError, }; return ( {children} ); }