+26
-23
src/providers/UnifiedAuthProvider.tsx
+26
-23
src/providers/UnifiedAuthProvider.tsx
···
1
-
// src/providers/UnifiedAuthProvider.tsx
2
-
// Import both Agent and the (soon to be deprecated) AtpAgent
3
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
4
import {
5
type OAuthSession,
···
7
TokenRefreshError,
8
TokenRevokedError,
9
} from "@atproto/oauth-client-browser";
10
import React, {
11
createContext,
12
use,
···
15
useState,
16
} from "react";
17
18
-
import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
19
20
-
// Define the unified status and authentication method
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
type AuthMethod = "password" | "oauth" | null;
23
24
interface AuthContextValue {
25
-
agent: Agent | null; // The agent is typed as the base class `Agent`
26
status: AuthStatus;
27
authMethod: AuthMethod;
28
loginWithPassword: (
···
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);
···
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 = (
···
105
setOauthSession(null);
106
setAuthMethod(null);
107
setStatus("signedOut");
108
};
109
110
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
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,
···
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) => {
···
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.");
···
147
} catch (e) {
148
console.error("Password login failed:", e);
149
setStatus("signedOut");
150
throw e;
151
}
152
};
···
161
}
162
}, [status]);
163
164
-
// --- Unified Logout ---
165
const logout = useCallback(async () => {
166
if (status !== "signedIn" || !agent) return;
167
setStatus("loading");
···
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
}
···
184
setAuthMethod(null);
185
setOauthSession(null);
186
setStatus("signedOut");
187
}
188
-
}, [status, authMethod, agent, oauthSession]);
189
190
return (
191
<AuthContext
···
1
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
2
import {
3
type OAuthSession,
···
5
TokenRefreshError,
6
TokenRevokedError,
7
} from "@atproto/oauth-client-browser";
8
+
import { useAtom } from "jotai";
9
import React, {
10
createContext,
11
use,
···
14
useState,
15
} from "react";
16
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
+
19
+
import { oauthClient } from "../utils/oauthClient";
20
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
type AuthMethod = "password" | "oauth" | null;
23
24
interface AuthContextValue {
25
+
agent: Agent | null;
26
status: AuthStatus;
27
authMethod: AuthMethod;
28
loginWithPassword: (
···
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);
···
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 = (
···
108
setOauthSession(null);
109
setAuthMethod(null);
110
setStatus("signedOut");
111
+
setQuickAuth(null);
112
};
113
114
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
117
return () => {
118
oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
119
};
120
+
}, [initialize, setQuickAuth]);
121
122
const loginWithPassword = async (
123
user: string,
124
password: string,
···
128
setStatus("loading");
129
try {
130
let sessionData: AtpSessionData | undefined;
131
const apiAgent = new AtpAgent({
132
service,
133
persistSession: (_evt, sess) => {
···
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.");
···
150
} catch (e) {
151
console.error("Password login failed:", e);
152
setStatus("signedOut");
153
+
setQuickAuth(null);
154
throw e;
155
}
156
};
···
165
}
166
}, [status]);
167
168
const logout = useCallback(async () => {
169
if (status !== "signedIn" || !agent) return;
170
setStatus("loading");
···
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
}
···
186
setAuthMethod(null);
187
setOauthSession(null);
188
setStatus("signedOut");
189
+
setQuickAuth(null);
190
}
191
+
}, [status, agent, authMethod, oauthSession, setQuickAuth]);
192
193
return (
194
<AuthContext
+39
-40
src/routes/index.tsx
+39
-40
src/routes/index.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
-
import { useEffect, useLayoutEffect } from "react";
5
6
import { Header } from "~/components/Header";
7
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
import {
10
-
agentAtom,
11
-
authedAtom,
12
feedScrollPositionsAtom,
13
isAtTopAtom,
14
selectedFeedUriAtom,
15
-
store,
16
} from "~/utils/atoms";
17
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
18
import {
···
107
} = useAuth();
108
const authed = !!agent?.did;
109
110
-
useEffect(() => {
111
-
if (agent?.did) {
112
-
store.set(authedAtom, true);
113
-
} else {
114
-
store.set(authedAtom, false);
115
-
}
116
-
}, [status, agent, authed]);
117
-
useEffect(() => {
118
-
if (agent) {
119
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
-
// @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
121
-
store.set(agentAtom, agent);
122
-
} else {
123
-
store.set(agentAtom, null);
124
-
}
125
-
}, [status, agent, authed]);
126
127
//const { get, set } = usePersistentStore();
128
// const [feed, setFeed] = React.useState<any[]>([]);
···
162
163
// const savedFeeds = savedFeedsPref?.items || [];
164
165
-
const identityresultmaybe = useQueryIdentity(agent?.did);
166
const identity = identityresultmaybe?.data;
167
168
const prefsresultmaybe = useQueryPreferences({
169
-
agent: agent ?? undefined,
170
-
pdsUrl: identity?.pds,
171
});
172
const prefs = prefsresultmaybe?.data;
173
···
178
return savedFeedsPref?.items || [];
179
}, [prefs]);
180
181
-
const [persistentSelectedFeed, setPersistentSelectedFeed] =
182
-
useAtom(selectedFeedUriAtom); // React.useState<string | null>(null);
183
-
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState(
184
-
persistentSelectedFeed
185
-
); // React.useState<string | null>(null);
186
const selectedFeed = agent?.did
187
? persistentSelectedFeed
188
: unauthedSelectedFeed;
···
306
}, [scrollPositions]);
307
308
useLayoutEffect(() => {
309
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
310
311
window.scrollTo({ top: savedPosition, behavior: "instant" });
312
// eslint-disable-next-line react-hooks/exhaustive-deps
313
-
}, [selectedFeed]);
314
315
useLayoutEffect(() => {
316
-
if (!selectedFeed) return;
317
318
const handleScroll = () => {
319
scrollPositionsRef.current = {
···
328
329
setScrollPositions(scrollPositionsRef.current);
330
};
331
-
}, [selectedFeed, setScrollPositions]);
332
333
-
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
334
-
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
335
336
// const {
337
// data: feedData,
···
347
348
// const feed = feedData?.feed || [];
349
350
-
const isReadyForAuthedFeed =
351
-
authed && agent && identity?.pds && feedServiceDid;
352
-
const isReadyForUnauthedFeed = !authed && selectedFeed;
353
354
355
const [isAtTop] = useAtom(isAtTopAtom);
···
358
<div
359
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
360
>
361
-
{savedFeeds.length > 0 ? (
362
<div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}>
363
{savedFeeds.map((item: any, idx: number) => {
364
const label = item.value.split("/").pop() || item.value;
···
410
/>
411
))} */}
412
413
-
{authed && (!identity?.pds || !feedServiceDid) && (
414
<div className="p-4 text-center text-gray-500">
415
Preparing your feed...
416
</div>
417
)}
418
419
-
{isReadyForAuthedFeed || isReadyForUnauthedFeed ? (
420
<InfiniteCustomFeed
421
key={selectedFeed!}
422
feedUri={selectedFeed!}
···
425
/>
426
) : (
427
<div className="p-4 text-center text-gray-500">
428
-
Select a feed to get started.
429
</div>
430
)}
431
{/* {false && restoringScrollPosition && (
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
+
import { useLayoutEffect, useState } from "react";
5
6
import { Header } from "~/components/Header";
7
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
import {
10
feedScrollPositionsAtom,
11
isAtTopAtom,
12
+
quickAuthAtom,
13
selectedFeedUriAtom,
14
} from "~/utils/atoms";
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
import {
···
105
} = useAuth();
106
const authed = !!agent?.did;
107
108
+
// i dont remember why this is even here
109
+
// useEffect(() => {
110
+
// if (agent?.did) {
111
+
// store.set(authedAtom, true);
112
+
// } else {
113
+
// store.set(authedAtom, false);
114
+
// }
115
+
// }, [status, agent, authed]);
116
+
// useEffect(() => {
117
+
// if (agent) {
118
+
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
119
+
// // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
120
+
// store.set(agentAtom, agent);
121
+
// } else {
122
+
// store.set(agentAtom, null);
123
+
// }
124
+
// }, [status, agent, authed]);
125
126
//const { get, set } = usePersistentStore();
127
// const [feed, setFeed] = React.useState<any[]>([]);
···
161
162
// const savedFeeds = savedFeedsPref?.items || [];
163
164
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
165
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
166
+
167
+
const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined);
168
const identity = identityresultmaybe?.data;
169
170
const prefsresultmaybe = useQueryPreferences({
171
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
172
+
pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined,
173
});
174
const prefs = prefsresultmaybe?.data;
175
···
180
return savedFeedsPref?.items || [];
181
}, [prefs]);
182
183
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
+
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
185
const selectedFeed = agent?.did
186
? persistentSelectedFeed
187
: unauthedSelectedFeed;
···
305
}, [scrollPositions]);
306
307
useLayoutEffect(() => {
308
+
if (isAuthRestoring) return;
309
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
310
311
window.scrollTo({ top: savedPosition, behavior: "instant" });
312
// eslint-disable-next-line react-hooks/exhaustive-deps
313
+
}, [selectedFeed, isAuthRestoring]);
314
315
useLayoutEffect(() => {
316
+
if (!selectedFeed || isAuthRestoring) return;
317
318
const handleScroll = () => {
319
scrollPositionsRef.current = {
···
328
329
setScrollPositions(scrollPositionsRef.current);
330
};
331
+
}, [isAuthRestoring, selectedFeed, setScrollPositions]);
332
333
+
const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined);
334
+
const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined;
335
336
// const {
337
// data: feedData,
···
347
348
// const feed = feedData?.feed || [];
349
350
+
const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
351
+
const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed;
352
353
354
const [isAtTop] = useAtom(isAtTopAtom);
···
357
<div
358
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
359
>
360
+
{!isAuthRestoring && savedFeeds.length > 0 ? (
361
<div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}>
362
{savedFeeds.map((item: any, idx: number) => {
363
const label = item.value.split("/").pop() || item.value;
···
409
/>
410
))} */}
411
412
+
{isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && (
413
<div className="p-4 text-center text-gray-500">
414
Preparing your feed...
415
</div>
416
)}
417
418
+
{!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? (
419
<InfiniteCustomFeed
420
key={selectedFeed!}
421
feedUri={selectedFeed!}
···
424
/>
425
) : (
426
<div className="p-4 text-center text-gray-500">
427
+
Loading.......
428
</div>
429
)}
430
{/* {false && restoringScrollPosition && (
+7
-3
src/utils/atoms.ts
+7
-3
src/utils/atoms.ts
···
1
-
import type Agent from "@atproto/api";
2
import { atom, createStore, useAtomValue } from "jotai";
3
import { atomWithStorage } from "jotai/utils";
4
import { useEffect } from "react";
5
6
export const store = createStore();
7
8
export const selectedFeedUriAtom = atomWithStorage<string | null>(
9
"selectedFeedUri",
···
52
| { kind: "quote"; subject: string };
53
export const composerAtom = atom<ComposerState>({ kind: "closed" });
54
55
-
export const agentAtom = atom<Agent | null>(null);
56
-
export const authedAtom = atom<boolean>(false);
57
58
export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) {
59
const value = useAtomValue(atom);
···
1
import { atom, createStore, useAtomValue } from "jotai";
2
import { atomWithStorage } from "jotai/utils";
3
import { useEffect } from "react";
4
5
export const store = createStore();
6
+
7
+
export const quickAuthAtom = atomWithStorage<string | null>(
8
+
"quickAuth",
9
+
null
10
+
);
11
12
export const selectedFeedUriAtom = atomWithStorage<string | null>(
13
"selectedFeedUri",
···
56
| { kind: "quote"; subject: string };
57
export const composerAtom = atom<ComposerState>({ kind: "closed" });
58
59
+
//export const agentAtom = atom<Agent | null>(null);
60
+
//export const authedAtom = atom<boolean>(false);
61
62
export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) {
63
const value = useAtomValue(atom);