This repository has no description
1import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2import { BrowserOAuthClient } from '@atproto/oauth-client-browser';
3
4// Create auth context
5export const AuthContext = createContext(null);
6
7
8// Client metadata for Bluesky OAuth
9const clientMetadata = {
10 client_id: `https://cred.blue/client-metadata.json`,
11 client_name: "cred.blue",
12 client_uri: `https://cred.blue`,
13 redirect_uris: [`https://cred.blue/login/callback`],
14 logo_uri: `https://cred.blue/favicon.ico`,
15 scope: "atproto transition:generic",
16 grant_types: ["authorization_code", "refresh_token"],
17 response_types: ["code"],
18 token_endpoint_auth_method: "none",
19 application_type: "web",
20 dpop_bound_access_tokens: true
21};
22
23export const AuthProvider = ({ children }) => {
24 const [client, setClient] = useState(null);
25 const [session, setSession] = useState(null);
26 const [loading, setLoading] = useState(true);
27 const [error, setError] = useState(null);
28 const initializing = useRef(false);
29
30 // Updated initializeAuth for BrowserOAuthClient
31 useEffect(() => {
32 const initializeAuth = async () => {
33 if (initializing.current || client) return; // Prevent multiple initializations
34 initializing.current = true;
35 setLoading(true);
36 setError(null);
37 console.log('(AuthProvider) Initializing BrowserOAuthClient...');
38
39 try {
40 // Create the client instance
41 const oauthClient = new BrowserOAuthClient({
42 clientMetadata: clientMetadata, // Reverted to original clientMetadata
43 handleResolver: 'https://public.api.bsky.app',
44 plcDirectoryUrl: 'https://plc.directory',
45 });
46
47 setClient(oauthClient); // Store the client instance
48
49 // Initialize the client - this handles callbacks and session restoration
50 console.log('(AuthProvider) Initializing OAuth client...');
51 const initResult = await oauthClient.init();
52 console.log('(AuthProvider) Init result:', {
53 hasSession: !!initResult?.session,
54 hasState: !!initResult?.state,
55 did: initResult?.session?.did
56 });
57
58 if (initResult?.session) {
59 setSession(initResult.session);
60 console.log(`(AuthProvider) Session ${initResult.state ? 'established via callback' : 'restored'}:`, initResult.session.did);
61 if (initResult.state) {
62 console.log('(AuthProvider) Original state from callback:', initResult.state);
63 // Optionally, redirect based on state if needed, e.g., using navigate
64 // const returnUrl = initResult.state.returnUrl || '/'; // Example state usage
65 // window.location.href = returnUrl; // Or use React Router navigate
66 }
67 } else {
68 setSession(null);
69 console.log('(AuthProvider) No active session found or callback processed.');
70 }
71 } catch (err) {
72 console.error('(AuthProvider) Error initializing client or handling callback:', err);
73 setError('Authentication initialization failed. Please try refreshing.');
74 setSession(null);
75 } finally {
76 setLoading(false);
77 initializing.current = false;
78 console.log('(AuthProvider) Initialization complete.');
79 }
80 };
81
82 initializeAuth();
83 }, [client]); // Dependency on client ensures it runs only once after client is potentially set
84
85 // Updated login function - uses client.signIn()
86 const login = useCallback(async (handle, returnUrl = '/') => {
87 if (!client) {
88 setError("Client not initialized.");
89 return;
90 }
91 console.log(`(AuthProvider) Initiating client-side login for handle: ${handle || 'none specified'}, returnUrl: ${returnUrl}`);
92 try {
93 // The state can be used to pass information through the redirect, like the return URL
94 const stateData = JSON.stringify({ returnUrl });
95 // signIn redirects the browser, so code execution stops here if successful
96 await client.signIn(handle, { // Pass handle directly for resolution
97 state: stateData,
98 // prompt: 'none', // Uncomment for silent sign-in attempt
99 });
100 } catch (err) {
101 // This catch might run if the user navigates back or cancels
102 console.error('(AuthProvider) Error during signIn initiation or cancellation:', err);
103 setError('Login initiation failed or was cancelled.');
104 }
105 }, [client]);
106
107 // Updated Logout function - uses session.signOut()
108 const logout = useCallback(async () => {
109 if (!session) {
110 console.log('(AuthProvider) No active session to log out');
111 return;
112 }
113
114 console.log('(AuthProvider) Logging out...');
115 try {
116 await session.signOut(); // Use session's signOut method
117 setSession(null);
118 // Optionally clear other app state here
119 console.log('(AuthProvider) Logout complete.');
120 // Redirect to home or login page
121 window.location.href = '/'; // Force reload to ensure clean state
122 } catch (err) {
123 console.error('(AuthProvider) Error during logout:', err);
124 // Still attempt to clear local session on error
125 setSession(null);
126 window.location.href = '/'; // Force reload
127 }
128 }, [session]);
129
130 // Debug effect to log auth state changes
131 useEffect(() => {
132 console.log('(AuthProvider) Auth state updated:', {
133 isAuthenticated: !!session,
134 did: session?.did || null,
135 loading,
136 hasError: !!error
137 });
138
139 // Force a page refresh if session state changes and we're on a protected path
140 // This ensures the latest auth state is always used for route protection
141 // REMOVED: This can cause unwanted flashing/reloads
142 /*
143 if (!loading && window.location.pathname === '/verifier' && !session) {
144 console.log('(AuthProvider) No session on protected page, forcing refresh');
145 window.location.reload();
146 }
147 */
148 }, [session, loading, error]);
149
150 return (
151 <AuthContext.Provider
152 value={{
153 client, // Export the client instance if needed by components (e.g., LoginCallback)
154 session,
155 loading, // Renamed from authLoading for clarity
156 error,
157 isAuthenticated: !!session,
158 login,
159 logout,
160 // Remove checkAuthStatus export
161 }}
162 >
163 {children}
164 </AuthContext.Provider>
165 );
166};
167
168// Custom hook to use the auth context
169export const useAuth = () => {
170 const context = useContext(AuthContext);
171 if (context === null) {
172 throw new Error('useAuth must be used within an AuthProvider');
173 }
174 // Ensure components using the hook get the updated context value
175 // (React handles this, but good to be mindful)
176 return context;
177};