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 0883da1a4296a01fdc1c7cb6415262e311d38029 209 lines 6.5 kB view raw
1import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 2import { 3 type OAuthSession, 4 TokenInvalidError, 5 TokenRefreshError, 6 TokenRevokedError, 7} from "@atproto/oauth-client-browser"; 8import { useAtom } from "jotai"; 9import React, { 10 createContext, 11 use, 12 useCallback, 13 useEffect, 14 useState, 15} from "react"; 16 17import { quickAuthAtom } from "~/utils/atoms"; 18 19import { oauthClient } from "../utils/oauthClient"; 20 21type AuthStatus = "loading" | "signedIn" | "signedOut"; 22type AuthMethod = "password" | "oauth" | null; 23 24interface AuthContextValue { 25 agent: Agent | null; 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 const [agent, setAgent] = useState<Agent | null>(null); 45 const [status, setStatus] = useState<AuthStatus>("loading"); 46 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 47 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 48 const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 49 50 const initialize = useCallback(async () => { 51 try { 52 const oauthResult = await oauthClient.init(); 53 if (oauthResult) { 54 // /*mass comment*/ console.log("OAuth session restored."); 55 const apiAgent = new Agent(oauthResult.session); 56 setAgent(apiAgent); 57 setOauthSession(oauthResult.session); 58 setAuthMethod("oauth"); 59 setStatus("signedIn"); 60 setQuickAuth(apiAgent?.did || null); 61 return; 62 } 63 } catch (e) { 64 console.error("OAuth init failed, checking password session.", e); 65 if (!quickAuth) { 66 // quickAuth restoration. if last used method is oauth we immediately call for oauth redo 67 // (and set a persistent atom somewhere to not retry again if it failed) 68 } 69 } 70 71 try { 72 const service = localStorage.getItem("service"); 73 const sessionString = localStorage.getItem("sess"); 74 75 if (service && sessionString) { 76 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 77 const apiAgent = new AtpAgent({ service }); 78 const session: AtpSessionData = JSON.parse(sessionString); 79 await apiAgent.resumeSession(session); 80 81 // /*mass comment*/ console.log("Password-based session resumed successfully."); 82 setAgent(apiAgent); 83 setAuthMethod("password"); 84 setStatus("signedIn"); 85 setQuickAuth(apiAgent?.did || null); 86 return; 87 } 88 } catch (e) { 89 console.error("Failed to resume password-based session.", e); 90 localStorage.removeItem("sess"); 91 localStorage.removeItem("service"); 92 } 93 94 // /*mass comment*/ console.log("No active session found."); 95 setStatus("signedOut"); 96 setAgent(null); 97 setAuthMethod(null); 98 // do we want to null it here? 99 setQuickAuth(null); 100 }, [quickAuth, setQuickAuth]); 101 102 useEffect(() => { 103 const handleOAuthSessionDeleted = ( 104 event: CustomEvent<{ sub: string; cause: TokenRefreshError | TokenRevokedError | TokenInvalidError }>, 105 ) => { 106 console.error(`OAuth Session for ${event.detail.sub} was deleted.`, event.detail.cause); 107 setAgent(null); 108 setOauthSession(null); 109 setAuthMethod(null); 110 setStatus("signedOut"); 111 setQuickAuth(null); 112 }; 113 114 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 initialize(); 116 117 return () => { 118 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 119 }; 120 }, [initialize, setQuickAuth]); 121 122 const loginWithPassword = async ( 123 user: string, 124 password: string, 125 service: string = "https://bsky.social", 126 ) => { 127 if (status !== "signedOut") return; 128 setStatus("loading"); 129 try { 130 let sessionData: AtpSessionData | undefined; 131 const apiAgent = new AtpAgent({ 132 service, 133 persistSession: (_evt, sess) => { 134 sessionData = sess; 135 }, 136 }); 137 await apiAgent.login({ identifier: user, password }); 138 139 if (sessionData) { 140 localStorage.setItem("service", service); 141 localStorage.setItem("sess", JSON.stringify(sessionData)); 142 setAgent(apiAgent); 143 setAuthMethod("password"); 144 setStatus("signedIn"); 145 setQuickAuth(apiAgent?.did || null); 146 // /*mass comment*/ console.log("Successfully logged in with password."); 147 } else { 148 throw new Error("Session data not persisted after login."); 149 } 150 } catch (e) { 151 console.error("Password login failed:", e); 152 setStatus("signedOut"); 153 setQuickAuth(null); 154 throw e; 155 } 156 }; 157 158 const loginWithOAuth = useCallback(async (handleOrPdsUrl: string) => { 159 if (status !== "signedOut") return; 160 try { 161 sessionStorage.setItem("postLoginRedirect", window.location.pathname + window.location.search); 162 await oauthClient.signIn(handleOrPdsUrl); 163 } catch (err) { 164 console.error("OAuth sign-in aborted or failed:", err); 165 } 166 }, [status]); 167 168 const logout = useCallback(async () => { 169 if (status !== "signedIn" || !agent) return; 170 setStatus("loading"); 171 172 try { 173 if (authMethod === "oauth" && oauthSession) { 174 await oauthClient.revoke(oauthSession.sub); 175 // /*mass comment*/ console.log("OAuth session revoked."); 176 } else if (authMethod === "password") { 177 localStorage.removeItem("service"); 178 localStorage.removeItem("sess"); 179 await (agent as AtpAgent).com.atproto.server.deleteSession(); 180 // /*mass comment*/ console.log("Password-based session deleted."); 181 } 182 } catch (e) { 183 console.error("Logout failed:", e); 184 } finally { 185 setAgent(null); 186 setAuthMethod(null); 187 setOauthSession(null); 188 setStatus("signedOut"); 189 setQuickAuth(null); 190 } 191 }, [status, agent, authMethod, oauthSession, setQuickAuth]); 192 193 return ( 194 <AuthContext 195 value={{ 196 agent, 197 status, 198 authMethod, 199 loginWithPassword, 200 loginWithOAuth, 201 logout, 202 }} 203 > 204 {children} 205 </AuthContext> 206 ); 207}; 208 209export const useAuth = () => use(AuthContext);