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);