/** * Authentication Context for ATProtocol OAuth * Manages auth state, login/logout, and token refresh */ import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode, } from 'react'; import { ATProtoOAuthClient } from '../lib/atproto-oauth-client'; import { XRPCClient } from '../lib/xrpc-client'; import type { OAuthSession, UserProfile, AuthContextType, } from '../types/auth'; const AuthContext = createContext(null); interface AuthProviderProps { children: ReactNode; } export const AuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null); const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Create stable client instances (only once) const [oauthClient] = useState(() => new ATProtoOAuthClient()); const [xrpcClient] = useState(() => new XRPCClient()); /** * Initialize auth state on mount */ useEffect(() => { const initAuth = async () => { try { const storedSession = await oauthClient.getSession(); if (storedSession) { // Session exists, restore user from session data setSession(storedSession); setUser({ did: storedSession.did, handle: storedSession.handle, }); } } catch (err) { console.error('Auth initialization error:', err); setError(err as Error); } finally { setLoading(false); } }; initAuth(); }, [oauthClient, xrpcClient]); /** * Start OAuth login flow * @param handle - User's handle */ const login = useCallback(async (handle: string) => { try { setLoading(true); setError(null); const authUrl = await oauthClient.authorize(handle); // Redirect to authorization server window.location.href = authUrl; } catch (err) { console.error('Login error:', err); setError(err as Error); setLoading(false); throw err; } }, [oauthClient]); /** * Handle OAuth callback * @param code - Authorization code * @param state - State parameter * @param iss - Optional issuer parameter */ const handleCallback = useCallback(async ( code: string, state: string, iss?: string ) => { try { setLoading(true); setError(null); const newSession = await oauthClient.callback(code, state, iss); // Set session and user from the session data setSession(newSession); setUser({ did: newSession.did, handle: newSession.handle, }); } catch (err) { console.error('Callback error:', err); setError(err as Error); throw err; } finally { setLoading(false); } }, [oauthClient, xrpcClient]); /** * Logout and clear session */ const logout = useCallback(async () => { try { setLoading(true); await oauthClient.clearSession(); setUser(null); setSession(null); setError(null); } catch (err) { console.error('Logout error:', err); setError(err as Error); } finally { setLoading(false); } }, [oauthClient]); /** * Manually refresh access token */ const refreshToken = useCallback(async () => { if (!session) { throw new Error('No active session'); } try { const newSession = await oauthClient.refreshAccessToken(session); setSession(newSession); } catch (err) { console.error('Token refresh error:', err); setError(err as Error); throw err; } }, [session, oauthClient]); /** * Auto-refresh token before expiry */ useEffect(() => { if (!session) { return; } // Calculate time until token expiry const timeUntilExpiry = session.expiresAt - Date.now(); // If token is already expired or expiring in less than 1 minute if (timeUntilExpiry <= 60000) { refreshToken().catch((err) => { console.error('Auto-refresh failed:', err); // If refresh fails, clear session logout(); }); return; } // Set timeout to refresh 1 minute before expiry const refreshTimeout = setTimeout(() => { refreshToken().catch((err) => { console.error('Auto-refresh failed:', err); logout(); }); }, timeUntilExpiry - 60000); return () => clearTimeout(refreshTimeout); }, [session, refreshToken, logout]); const contextValue: AuthContextType = { user, session, loading, error, isAuthenticated: !!user && !!session, login, handleCallback, logout, refreshToken, }; return ( {children} ); }; /** * Hook to use auth context * @returns Auth context value */ export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within AuthProvider'); } return context; }; /** * Get XRPC client with current session * @returns Configured XRPC client */ export const useXRPC = (): XRPCClient | null => { const { session } = useAuth(); if (!session) { return null; } const client = new XRPCClient(); client.setSession(session); return client; };