Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
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<SessionData> { 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<SessionData | null>(null); const [loading, setLoading] = useState(true); const [authError, setAuthError] = useState<string | null>(null); const [accounts, setAccounts] = useState<KnownAccount[]>(() => 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 ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> );}