A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 5.0 kB View raw
1import { 2 createContext, 3 useCallback, 4 useEffect, 5 useRef, 6 useState, 7 type ReactNode, 8} from 'react'; 9import { Agent } from '@atproto/api'; 10import type { BrowserOAuthClient, OAuthSession } from '@atproto/oauth-client-browser'; 11import { createOAuthClient } from './oauth'; 12import { 13 isValidAccountInput, 14 normalizeHandle, 15 getClientMode, 16 redirectUriForLocation, 17} from './config'; 18import { resolvePdsUrl } from '../media/pds'; 19import { fetchViewerProfile } from './profile'; 20 21export type AuthStatus = 'loading' | 'signed-out' | 'signed-in' | 'error'; 22 23export interface AuthContextValue { 24 status: AuthStatus; 25 agent: Agent | null; 26 did: string | null; 27 handle: string | null; 28 displayName: string | null; 29 avatar: string | null; 30 /** The writer's PDS endpoint (for getBlob URLs); null until resolved. */ 31 pdsUrl: string | null; 32 error: string | null; 33 signIn: ( input: string ) => Promise< void >; 34 signOut: () => Promise< void >; 35} 36 37export const AuthContext = createContext< AuthContextValue | null >( null ); 38 39export function AuthProvider( { children }: { children: ReactNode } ) { 40 const clientRef = useRef< BrowserOAuthClient | null >( null ); 41 const [ status, setStatus ] = useState< AuthStatus >( 'loading' ); 42 const [ agent, setAgent ] = useState< Agent | null >( null ); 43 const [ did, setDid ] = useState< string | null >( null ); 44 const [ handle, setHandle ] = useState< string | null >( null ); 45 const [ displayName, setDisplayName ] = useState< string | null >( null ); 46 const [ avatar, setAvatar ] = useState< string | null >( null ); 47 const [ pdsUrl, setPdsUrl ] = useState< string | null >( null ); 48 const [ error, setError ] = useState< string | null >( null ); 49 50 const adoptSession = useCallback( async ( session: OAuthSession ) => { 51 const nextAgent = new Agent( session ); 52 setAgent( nextAgent ); 53 setDid( session.did ); 54 setStatus( 'signed-in' ); 55 setError( null ); 56 const profile = await fetchViewerProfile( nextAgent, session.did ); 57 setHandle( profile.handle ); 58 setDisplayName( profile.displayName ); 59 setAvatar( profile.avatar ); 60 try { 61 setPdsUrl( await resolvePdsUrl( session.did ) ); 62 } catch { 63 setPdsUrl( null ); // image upload stays disabled if the PDS can't be resolved 64 } 65 }, [] ); 66 67 // Initialise once: process an OAuth callback or restore an existing session. 68 useEffect( () => { 69 let cancelled = false; 70 71 ( async () => { 72 try { 73 const client = await createOAuthClient(); 74 if ( cancelled ) { 75 return; 76 } 77 clientRef.current = client; 78 79 const result = await client.init(); 80 if ( cancelled ) { 81 return; 82 } 83 if ( result?.session ) { 84 await adoptSession( result.session ); 85 } else { 86 setStatus( 'signed-out' ); 87 } 88 } catch ( err ) { 89 if ( ! cancelled ) { 90 setError( err instanceof Error ? err.message : String( err ) ); 91 setStatus( 'error' ); 92 } 93 } 94 } )(); 95 96 return () => { 97 cancelled = true; 98 }; 99 }, [ adoptSession ] ); 100 101 const signIn = useCallback( async ( input: string ) => { 102 const client = clientRef.current; 103 if ( ! client ) { 104 return; 105 } 106 if ( ! isValidAccountInput( input ) ) { 107 setError( 'Enter a handle (alice.bsky.social), a DID, or a PDS URL.' ); 108 return; 109 } 110 setError( null ); 111 try { 112 // Hosted (prod) registers several redirect URIs (one per OAuth-island route), 113 // and atproto defaults to the first — so without this, signing in from /write 114 // always returned the writer to /editor, losing the in-progress draft. Pin the 115 // round-trip to the page sign-in started from. In loopback (dev) the client's 116 // single redirect URI already equals the current page, so we pass nothing. 117 // atproto brands `redirect_uri` as a URL template type; the value is a registered 118 // app URL (and atproto re-validates it against the client metadata), so cast the 119 // options object to the `signIn` parameter type. 120 const options = 121 getClientMode( window.location.hostname ) === 'hosted' 122 ? ( { 123 redirect_uri: redirectUriForLocation( 124 window.location.origin, 125 window.location.pathname 126 ), 127 } as Parameters< BrowserOAuthClient[ 'signIn' ] >[ 1 ] ) 128 : undefined; 129 // Redirects to the user's authorization server; the promise never resolves. 130 await client.signIn( normalizeHandle( input ), options ); 131 } catch ( err ) { 132 setError( err instanceof Error ? err.message : String( err ) ); 133 } 134 }, [] ); 135 136 const signOut = useCallback( async () => { 137 const client = clientRef.current; 138 if ( client && did ) { 139 try { 140 await client.revoke( did ); 141 } catch { 142 // best effort — fall through to local reset 143 } 144 } 145 setAgent( null ); 146 setDid( null ); 147 setHandle( null ); 148 setDisplayName( null ); 149 setAvatar( null ); 150 setPdsUrl( null ); 151 setStatus( 'signed-out' ); 152 }, [ did ] ); 153 154 return ( 155 <AuthContext.Provider 156 value={ { status, agent, did, handle, displayName, avatar, pdsUrl, error, signIn, signOut } } 157 > 158 { children } 159 </AuthContext.Provider> 160 ); 161}