the statusphere demo reworked into a vite/react app in a monorepo
at main 138 lines 3.7 kB view raw
1import { createContext, ReactNode, useContext, useState } from 'react' 2import { useQuery, useQueryClient } from '@tanstack/react-query' 3import { XRPCError } from '@atproto/xrpc' 4import { XyzStatusphereGetUser } from '@statusphere/lexicon' 5 6import api from '#/services/api' 7 8interface AuthContextType { 9 user: XyzStatusphereGetUser.OutputSchema | null 10 loading: boolean 11 error: string | null 12 login: (handle: string) => Promise<{ redirectUrl: string }> 13 logout: () => Promise<void> 14} 15 16const AuthContext = createContext<AuthContextType | undefined>(undefined) 17 18export function AuthProvider({ children }: { children: ReactNode }) { 19 const [error, setError] = useState<string | null>(null) 20 const queryClient = useQueryClient() 21 22 // Use React Query to fetch and manage user data 23 const { 24 data: user, 25 isLoading: loading, 26 error: queryError, 27 } = useQuery({ 28 queryKey: ['currentUser'], 29 queryFn: async () => { 30 // Check for error parameter in URL (from OAuth redirect) 31 const urlParams = new URLSearchParams(window.location.search) 32 const errorParam = urlParams.get('error') 33 34 if (errorParam) { 35 setError('Authentication failed. Please try again.') 36 37 // Remove the error parameter from the URL 38 const newUrl = window.location.pathname 39 window.history.replaceState({}, document.title, newUrl) 40 return null 41 } 42 43 try { 44 const { data: userData } = await api.getCurrentUser({}) 45 46 // Clean up URL if needed 47 if (window.location.search && userData) { 48 window.history.replaceState( 49 {}, 50 document.title, 51 window.location.pathname, 52 ) 53 } 54 55 return userData 56 } catch (apiErr) { 57 if ( 58 apiErr instanceof XRPCError && 59 apiErr.error === 'AuthenticationRequired' 60 ) { 61 return null 62 } 63 64 console.error('🚫 API error during auth check:', apiErr) 65 66 // If it's a network error, provide a more helpful message 67 if ( 68 apiErr instanceof TypeError && 69 apiErr.message.includes('Failed to fetch') 70 ) { 71 throw new Error( 72 'Cannot connect to API server. Please check your network connection or server status.', 73 ) 74 } 75 76 throw apiErr 77 } 78 }, 79 retry: false, 80 staleTime: 5 * 60 * 1000, // 5 minutes 81 }) 82 83 const login = async (handle: string) => { 84 setError(null) 85 86 try { 87 return await api.login(handle) 88 } catch (err) { 89 const message = err instanceof Error ? err.message : 'Login failed' 90 setError(message) 91 throw err 92 } 93 } 94 95 const logout = async () => { 96 try { 97 await api.logout() 98 // Reset the user data in React Query cache 99 queryClient.setQueryData(['currentUser'], null) 100 // Invalidate any user-dependent queries 101 queryClient.invalidateQueries({ queryKey: ['statuses'] }) 102 } catch (err) { 103 const message = err instanceof Error ? err.message : 'Logout failed' 104 setError(message) 105 throw err 106 } 107 } 108 109 // Combine state error with query error 110 const combinedError = 111 error || (queryError instanceof Error ? queryError.message : null) 112 113 return ( 114 <AuthContext.Provider 115 value={{ 116 user: user || null, 117 loading, 118 error: combinedError, 119 login, 120 logout, 121 }} 122 > 123 {children} 124 </AuthContext.Provider> 125 ) 126} 127 128export function useAuth() { 129 const context = useContext(AuthContext) 130 131 if (context === undefined) { 132 throw new Error('useAuth must be used within an AuthProvider') 133 } 134 135 return context 136} 137 138export default useAuth