Scrapboard.org client
at main 142 lines 4.2 kB view raw
1"use client"; 2 3import { 4 createContext, 5 useContext, 6 useEffect, 7 useState, 8 useCallback, 9 ReactNode, 10} from "react"; 11import { 12 BrowserOAuthClient, 13 AtprotoDohHandleResolver, 14 type OAuthSession, 15} from "@atproto/oauth-client-browser"; 16import { Agent } from "@atproto/api"; 17import { th } from "zod/v4/locales"; 18 19type AuthContextType = { 20 session: OAuthSession | null; 21 agent: Agent; 22 loading: boolean; 23 login: (handle: string) => Promise<void>; 24 logout: () => void; 25}; 26 27const AuthContext = createContext<AuthContextType | null>(null); 28 29export function AuthProvider({ children }: { children: ReactNode }) { 30 const defaultAgent = new Agent({ service: "https://bsky.social" }); 31 const [session, setSession] = useState<OAuthSession | null>(null); 32 const [agent, setAgent] = useState<Agent>(defaultAgent); 33 const [loading, setLoading] = useState(true); 34 const [client, setClient] = useState<BrowserOAuthClient | null>(null); 35 36 useEffect(() => { 37 const initClient = async () => { 38 const isDev = process.env.NODE_ENV === "development"; 39 40 const c = isDev 41 ? new BrowserOAuthClient({ 42 handleResolver: "https://bsky.social", 43 clientMetadata: { 44 client_name: "Statusphere React App", 45 client_id: `http://localhost?scope=${encodeURI( 46 "atproto transition:generic transition:chat.bsky" 47 )}`, 48 client_uri: "http://127.0.0.1:3000", 49 redirect_uris: ["http://127.0.0.1:3000"], 50 scope: "atproto transition:generic", 51 grant_types: ["authorization_code", "refresh_token"], 52 response_types: ["code"], 53 application_type: "web", 54 token_endpoint_auth_method: "none", 55 dpop_bound_access_tokens: true, 56 }, // loopback client 57 }) 58 : await BrowserOAuthClient.load({ 59 handleResolver: "https://bsky.social", 60 clientId: "https://pin.to.it/client-metadata.json", 61 }); 62 63 setClient(c); 64 65 try { 66 const result = await c.init(); 67 if (result?.session) { 68 console.log("session found", result); 69 localStorage.setItem("did", result.session.did); 70 const ag = new Agent(result.session); 71 setSession(result.session); 72 setAgent(ag); 73 const prefs = await ag.getPreferences(); 74 if (!prefs) return; 75 } else { 76 const did = localStorage.getItem("did"); 77 78 console.log("restoring", did); 79 if (did != null) { 80 const result = await c.restore(did); 81 const ag = new Agent(result); 82 setSession(result); 83 setAgent(ag); 84 } 85 } 86 } catch (err) { 87 console.error("OAuth init failed", err); 88 } finally { 89 setLoading(false); 90 } 91 92 c.addEventListener("deleted", (event: CustomEvent) => { 93 console.warn("Session invalidated", event.detail); 94 setSession(null); 95 setAgent(defaultAgent); 96 }); 97 }; 98 99 initClient(); 100 }, []); 101 102 const login = useCallback( 103 async (handle: string) => { 104 if (!client) return; 105 try { 106 await client.signIn(handle, { 107 scope: "atproto transition:generic", 108 ui_locales: "en", // Only supported by some OAuth servers (requires OpenID Connect support + i18n support) 109 signal: new AbortController().signal, 110 }); 111 } catch (e) { 112 console.warn("Login aborted or failed", e); 113 throw new Error( 114 `Login failed: ${e instanceof Error ? e.message : "Unknown error"}` 115 ); 116 } 117 }, 118 [client] 119 ); 120 121 const logout = useCallback(() => { 122 if (client && session) { 123 client.revoke(session.sub); 124 setSession(null); 125 setAgent(defaultAgent); 126 // refresh page 127 window.location.reload(); 128 } 129 }, [client, session]); 130 131 return ( 132 <AuthContext.Provider value={{ session, agent, loading, login, logout }}> 133 {children} 134 </AuthContext.Provider> 135 ); 136} 137 138export function useAuth() { 139 const ctx = useContext(AuthContext); 140 if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>"); 141 return ctx; 142}