👁️
1import type { ActorIdentifier, Did } from "@atcute/lexicons";
2import {
3 createAuthorizationUrl,
4 getSession,
5 OAuthUserAgent,
6 type Session,
7} from "@atcute/oauth-browser-client";
8import { useNavigate } from "@tanstack/react-router";
9import {
10 createContext,
11 type ReactNode,
12 useContext,
13 useEffect,
14 useState,
15} from "react";
16import { toast } from "sonner";
17
18const STORAGE_KEY = "deckbelcher:last-did";
19export const RETURN_TO_KEY = "deckbelcher:return-to";
20
21interface AuthContextValue {
22 session: Session | null;
23 agent: OAuthUserAgent | null;
24 signIn: (handle: string) => Promise<void>;
25 signUp: (pdsUrl: string) => Promise<void>;
26 signOut: () => Promise<void>;
27 setAuthSession: (session: Session) => void;
28 isLoading: boolean;
29}
30
31const AuthContext = createContext<AuthContextValue | null>(null);
32
33export function AuthProvider({ children }: { children: ReactNode }) {
34 const [session, setSession] = useState<Session | null>(null);
35 const [agent, setAgent] = useState<OAuthUserAgent | null>(null);
36 const [isLoading, setIsLoading] = useState(true);
37 const navigate = useNavigate();
38
39 useEffect(() => {
40 const restoreSession = async () => {
41 try {
42 const lastDid = localStorage.getItem(STORAGE_KEY);
43 if (lastDid) {
44 const stored = await getSession(lastDid as Did);
45 if (stored) {
46 setSession(stored);
47 setAgent(new OAuthUserAgent(stored));
48 }
49 }
50 } catch (error) {
51 console.warn("Session restoration failed:", error);
52 const lastDid = localStorage.getItem(STORAGE_KEY);
53 if (lastDid) {
54 localStorage.removeItem(STORAGE_KEY);
55 toast.error("Your session expired. Please sign in again.", {
56 duration: 5000,
57 action: {
58 label: "Sign In",
59 onClick: () => {
60 navigate({ to: "/signin" });
61 },
62 },
63 });
64 }
65 } finally {
66 setIsLoading(false);
67 }
68 };
69
70 restoreSession();
71 }, [navigate]);
72
73 const signIn = async (handle: string) => {
74 const authUrl = await createAuthorizationUrl({
75 target: { type: "account", identifier: handle as ActorIdentifier },
76 scope: import.meta.env.VITE_OAUTH_SCOPE,
77 });
78
79 window.location.assign(authUrl);
80 };
81
82 const signUp = async (pdsUrl: string) => {
83 // TODO: add `prompt: "create"` when more PDS implementations support it
84 // to hint that signup UI should be shown instead of login
85 const authUrl = await createAuthorizationUrl({
86 target: { type: "pds", serviceUrl: pdsUrl },
87 scope: import.meta.env.VITE_OAUTH_SCOPE,
88 });
89
90 window.location.assign(authUrl);
91 };
92
93 const signOut = async () => {
94 if (agent) {
95 await agent.signOut();
96 }
97 localStorage.removeItem(STORAGE_KEY);
98 setSession(null);
99 setAgent(null);
100 };
101
102 const setAuthSession = (newSession: Session) => {
103 localStorage.setItem(STORAGE_KEY, newSession.info.sub);
104 setSession(newSession);
105 setAgent(new OAuthUserAgent(newSession));
106 };
107
108 return (
109 <AuthContext.Provider
110 value={{
111 session,
112 agent,
113 signIn,
114 signUp,
115 signOut,
116 isLoading,
117 setAuthSession,
118 }}
119 >
120 {children}
121 </AuthContext.Provider>
122 );
123}
124
125export function useAuth() {
126 const context = useContext(AuthContext);
127 if (!context) {
128 throw new Error("useAuth must be used within an AuthProvider");
129 }
130 return context;
131}