an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
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);