forked from
samuel.fm/statusphere-react
the statusphere demo reworked into a vite/react app in a monorepo
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