an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

at fe8f474c328c61f64bc6e2e22fa1e970707a159e 206 lines 6.7 kB view raw
1// src/providers/UnifiedAuthProvider.tsx 2// Import both Agent and the (soon to be deprecated) AtpAgent 3import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 4import { 5 type OAuthSession, 6 TokenInvalidError, 7 TokenRefreshError, 8 TokenRevokedError, 9} from "@atproto/oauth-client-browser"; 10import React, { 11 createContext, 12 use, 13 useCallback, 14 useEffect, 15 useState, 16} from "react"; 17 18import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed 19 20// Define the unified status and authentication method 21type AuthStatus = "loading" | "signedIn" | "signedOut"; 22type AuthMethod = "password" | "oauth" | null; 23 24interface AuthContextValue { 25 agent: Agent | null; // The agent is typed as the base class `Agent` 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( 29 user: string, 30 password: string, 31 service?: string, 32 ) => Promise<void>; 33 loginWithOAuth: (handleOrPdsUrl: string) => Promise<void>; 34 logout: () => Promise<void>; 35} 36 37const AuthContext = createContext<AuthContextValue>({} as AuthContextValue); 38 39export const UnifiedAuthProvider = ({ 40 children, 41}: { 42 children: React.ReactNode; 43}) => { 44 // The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances. 45 const [agent, setAgent] = useState<Agent | null>(null); 46 const [status, setStatus] = useState<AuthStatus>("loading"); 47 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 48 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 49 50 // Unified Initialization Logic 51 const initialize = useCallback(async () => { 52 // --- 1. Try OAuth initialization first --- 53 try { 54 const oauthResult = await oauthClient.init(); 55 if (oauthResult) { 56 // /*mass comment*/ console.log("OAuth session restored."); 57 const apiAgent = new Agent(oauthResult.session); // Standard Agent 58 setAgent(apiAgent); 59 setOauthSession(oauthResult.session); 60 setAuthMethod("oauth"); 61 setStatus("signedIn"); 62 return; // Success 63 } 64 } catch (e) { 65 console.error("OAuth init failed, checking password session.", e); 66 } 67 68 // --- 2. If no OAuth, try password-based session using AtpAgent --- 69 try { 70 const service = localStorage.getItem("service"); 71 const sessionString = localStorage.getItem("sess"); 72 73 if (service && sessionString) { 74 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 75 // Use the original, working AtpAgent logic 76 const apiAgent = new AtpAgent({ service }); 77 const session: AtpSessionData = JSON.parse(sessionString); 78 await apiAgent.resumeSession(session); 79 80 // /*mass comment*/ console.log("Password-based session resumed successfully."); 81 setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent 82 setAuthMethod("password"); 83 setStatus("signedIn"); 84 return; // Success 85 } 86 } catch (e) { 87 console.error("Failed to resume password-based session.", e); 88 localStorage.removeItem("sess"); 89 localStorage.removeItem("service"); 90 } 91 92 // --- 3. If neither worked, user is signed out --- 93 // /*mass comment*/ console.log("No active session found."); 94 setStatus("signedOut"); 95 setAgent(null); 96 setAuthMethod(null); 97 }, []); 98 99 useEffect(() => { 100 const handleOAuthSessionDeleted = ( 101 event: CustomEvent<{ sub: string; cause: TokenRefreshError | TokenRevokedError | TokenInvalidError }>, 102 ) => { 103 console.error(`OAuth Session for ${event.detail.sub} was deleted.`, event.detail.cause); 104 setAgent(null); 105 setOauthSession(null); 106 setAuthMethod(null); 107 setStatus("signedOut"); 108 }; 109 110 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); 111 initialize(); 112 113 return () => { 114 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 }; 116 }, [initialize]); 117 118 // --- Login Methods --- 119 const loginWithPassword = async ( 120 user: string, 121 password: string, 122 service: string = "https://bsky.social", 123 ) => { 124 if (status !== "signedOut") return; 125 setStatus("loading"); 126 try { 127 let sessionData: AtpSessionData | undefined; 128 // Use the AtpAgent for its simple login and session persistence 129 const apiAgent = new AtpAgent({ 130 service, 131 persistSession: (_evt, sess) => { 132 sessionData = sess; 133 }, 134 }); 135 await apiAgent.login({ identifier: user, password }); 136 137 if (sessionData) { 138 localStorage.setItem("service", service); 139 localStorage.setItem("sess", JSON.stringify(sessionData)); 140 setAgent(apiAgent); // Store the AtpAgent instance in our state 141 setAuthMethod("password"); 142 setStatus("signedIn"); 143 // /*mass comment*/ console.log("Successfully logged in with password."); 144 } else { 145 throw new Error("Session data not persisted after login."); 146 } 147 } catch (e) { 148 console.error("Password login failed:", e); 149 setStatus("signedOut"); 150 throw e; 151 } 152 }; 153 154 const loginWithOAuth = useCallback(async (handleOrPdsUrl: string) => { 155 if (status !== "signedOut") return; 156 try { 157 sessionStorage.setItem("postLoginRedirect", window.location.pathname + window.location.search); 158 await oauthClient.signIn(handleOrPdsUrl); 159 } catch (err) { 160 console.error("OAuth sign-in aborted or failed:", err); 161 } 162 }, [status]); 163 164 // --- Unified Logout --- 165 const logout = useCallback(async () => { 166 if (status !== "signedIn" || !agent) return; 167 setStatus("loading"); 168 169 try { 170 if (authMethod === "oauth" && oauthSession) { 171 await oauthClient.revoke(oauthSession.sub); 172 // /*mass comment*/ console.log("OAuth session revoked."); 173 } else if (authMethod === "password") { 174 localStorage.removeItem("service"); 175 localStorage.removeItem("sess"); 176 // AtpAgent has its own logout methods 177 await (agent as AtpAgent).com.atproto.server.deleteSession(); 178 // /*mass comment*/ console.log("Password-based session deleted."); 179 } 180 } catch (e) { 181 console.error("Logout failed:", e); 182 } finally { 183 setAgent(null); 184 setAuthMethod(null); 185 setOauthSession(null); 186 setStatus("signedOut"); 187 } 188 }, [status, authMethod, agent, oauthSession]); 189 190 return ( 191 <AuthContext 192 value={{ 193 agent, 194 status, 195 authMethod, 196 loginWithPassword, 197 loginWithOAuth, 198 logout, 199 }} 200 > 201 {children} 202 </AuthContext> 203 ); 204}; 205 206export const useAuth = () => use(AuthContext);