The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.

Fix SSR issues with OAuth client - make completely client-side only to prevent indexedDB errors on Vercel

damedotblog 26d5d897 5611406a

+134 -40
.DS_Store

This is a binary file and will not be displayed.

+12 -10
app/src/app/layout.tsx
··· 54 54 <link rel="stylesheet" href="https://use.typekit.net/gik3riw.css" /> 55 55 </head> 56 56 <body> 57 - <AuthProvider> 58 - <ThemeProvider> 59 - <header> 60 - <ClientOnly> 61 - <NavigationBar /> 62 - </ClientOnly> 63 - </header> 64 - <main>{children}</main> 65 - </ThemeProvider> 66 - </AuthProvider> 57 + <ClientOnly> 58 + <AuthProvider> 59 + <ThemeProvider> 60 + <header> 61 + <ClientOnly> 62 + <NavigationBar /> 63 + </ClientOnly> 64 + </header> 65 + <main>{children}</main> 66 + </ThemeProvider> 67 + </AuthProvider> 68 + </ClientOnly> 67 69 <Analytics /> 68 70 </body> 69 71 </html>
+5
app/src/lib/api-client.ts
··· 14 14 langs?: string[]; 15 15 createdAt?: string; 16 16 }) { 17 + // Ensure we're on the client side 18 + if (typeof window === 'undefined') { 19 + throw new Error('API client can only be used on the client side'); 20 + } 21 + 17 22 try { 18 23 // For now, we'll make a direct API call to our existing endpoint 19 24 // Later this can be improved to use the OAuth session directly
+47 -11
app/src/lib/auth-context.tsx
··· 2 2 3 3 import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 4 4 import { OAuthSession } from '@atproto/oauth-client-browser'; 5 - import { initializeOAuthClient, oauthClient, signIn, restoreSession, signOut, onSessionDeleted } from './oauth-client'; 6 5 7 6 interface AuthContextType { 8 7 session: OAuthSession | null; ··· 28 27 export function AuthProvider({ children }: AuthProviderProps) { 29 28 const [session, setSession] = useState<OAuthSession | null>(null); 30 29 const [isLoading, setIsLoading] = useState(true); 30 + const [isClient, setIsClient] = useState(false); 31 31 32 - // Initialize the OAuth client on mount 32 + // Track if we're on the client side 33 + useEffect(() => { 34 + setIsClient(true); 35 + }, []); 36 + 37 + // Initialize the OAuth client on mount (client-side only) 33 38 useEffect(() => { 39 + if (!isClient) return; 40 + 34 41 async function initialize() { 35 42 try { 36 43 setIsLoading(true); 37 44 45 + // Dynamic import to ensure client-side only execution 46 + const { initializeOAuthClient } = await import('./oauth-client'); 38 47 const result = await initializeOAuthClient(); 39 48 40 49 if (result) { ··· 49 58 } 50 59 51 60 initialize(); 52 - }, []); 61 + }, [isClient]); 53 62 54 - // Set up session deletion listener 63 + // Set up session deletion listener (client-side only) 55 64 useEffect(() => { 56 - const handleSessionDeleted = ({ sub, cause }: { sub: string; cause: any }) => { 57 - console.error(`Session for ${sub} was invalidated:`, cause); 58 - setSession(null); 59 - }; 65 + if (!isClient) return; 60 66 61 - onSessionDeleted(handleSessionDeleted); 62 - }, []); 67 + async function setupListener() { 68 + try { 69 + const { onSessionDeleted } = await import('./oauth-client'); 70 + 71 + const handleSessionDeleted = ({ sub, cause }: { sub: string; cause: any }) => { 72 + console.error(`Session for ${sub} was invalidated:`, cause); 73 + setSession(null); 74 + }; 75 + 76 + onSessionDeleted(handleSessionDeleted); 77 + } catch (error) { 78 + console.error('Failed to set up session listener:', error); 79 + } 80 + } 81 + 82 + setupListener(); 83 + }, [isClient]); 63 84 64 85 const handleSignIn = async (handle: string) => { 86 + if (!isClient) { 87 + throw new Error('Sign in can only be called on the client side'); 88 + } 89 + 65 90 try { 91 + const { signIn } = await import('./oauth-client'); 66 92 await signIn(handle); 67 93 // Note: This will redirect, so we won't reach this point 68 94 } catch (error) { ··· 72 98 }; 73 99 74 100 const handleSignOut = async () => { 101 + if (!isClient) { 102 + throw new Error('Sign out can only be called on the client side'); 103 + } 104 + 75 105 try { 106 + const { signOut } = await import('./oauth-client'); 76 107 await signOut(); 77 108 setSession(null); 78 109 } catch (error) { ··· 82 113 }; 83 114 84 115 const handleRestoreSession = async (did: string) => { 116 + if (!isClient) { 117 + throw new Error('Restore session can only be called on the client side'); 118 + } 119 + 85 120 try { 121 + const { restoreSession } = await import('./oauth-client'); 86 122 const restoredSession = await restoreSession(did); 87 123 setSession(restoredSession); 88 124 return restoredSession; ··· 95 131 const contextValue: AuthContextType = { 96 132 session, 97 133 isAuthenticated: !!session, 98 - isLoading, 134 + isLoading: isLoading || !isClient, // Keep loading until client-side hydration 99 135 signIn: handleSignIn, 100 136 signOut: handleSignOut, 101 137 restoreSession: handleRestoreSession,
+70 -19
app/src/lib/oauth-client.ts
··· 17 17 "token_endpoint_auth_method": "none" as const 18 18 } 19 19 20 - // Create the OAuth client instance 21 - export const oauthClient = new BrowserOAuthClient({ 22 - clientMetadata: CLIENT_METADATA as any, // Type assertion to avoid strict typing issues 23 - // Use Bluesky's public handle resolver 24 - handleResolver: 'https://bsky.social', 25 - // Use fragment for better SPA support 26 - responseMode: 'fragment' 27 - }) 20 + // Lazy OAuth client - only initialize on client side 21 + let _oauthClient: BrowserOAuthClient | null = null 22 + 23 + // Get or create the OAuth client instance - client-side only 24 + function getOAuthClient(): BrowserOAuthClient { 25 + // Ensure we're on the client side 26 + if (typeof window === 'undefined') { 27 + throw new Error('OAuth client can only be used on the client side') 28 + } 29 + 30 + if (!_oauthClient) { 31 + _oauthClient = new BrowserOAuthClient({ 32 + clientMetadata: CLIENT_METADATA as any, 33 + handleResolver: 'https://bsky.social', 34 + responseMode: 'fragment' 35 + }) 36 + } 37 + 38 + return _oauthClient 39 + } 40 + 41 + // Export the getter function instead of the instance 42 + export const oauthClient = { 43 + get instance() { 44 + return getOAuthClient() 45 + } 46 + } 28 47 29 48 // Initialize the client - this should be called once when the app loads 30 49 export async function initializeOAuthClient() { 50 + // Only run on client side 51 + if (typeof window === 'undefined') { 52 + console.log('Skipping OAuth client initialization on server side') 53 + return null 54 + } 55 + 31 56 try { 32 - const result = await oauthClient.init() 57 + const client = getOAuthClient() 58 + const result = await client.init() 33 59 34 60 if (result) { 35 61 const { session } = result ··· 58 84 state?: string 59 85 signal?: AbortSignal 60 86 }) { 87 + // Only run on client side 88 + if (typeof window === 'undefined') { 89 + throw new Error('Sign in can only be called on the client side') 90 + } 91 + 61 92 try { 62 93 console.log(`Initiating OAuth flow for ${handle}`) 63 94 64 - await oauthClient.signIn(handle, { 95 + const client = getOAuthClient() 96 + await client.signIn(handle, { 65 97 state: options?.state || `signin-${Date.now()}`, 66 98 signal: options?.signal 67 99 }) ··· 75 107 76 108 // Restore a specific session by DID 77 109 export async function restoreSession(did: string) { 110 + // Only run on client side 111 + if (typeof window === 'undefined') { 112 + throw new Error('Restore session can only be called on the client side') 113 + } 114 + 78 115 try { 79 116 console.log(`Restoring session for ${did}`) 80 - const session = await oauthClient.restore(did) 117 + const client = getOAuthClient() 118 + const session = await client.restore(did) 81 119 console.log(`Successfully restored session for ${session.sub}`) 82 120 return session 83 121 } catch (error) { ··· 88 126 89 127 // Sign out the current session 90 128 export async function signOut() { 129 + // Only run on client side 130 + if (typeof window === 'undefined') { 131 + throw new Error('Sign out can only be called on the client side') 132 + } 133 + 91 134 try { 92 - // The BrowserOAuthClient doesn't expose a direct signOut method 93 - // We need to manually clear the session and redirect 94 - // For now, we'll clear local storage and redirect 95 135 console.log('Signing out user') 96 136 97 137 // Clear any remaining localStorage items from the old implementation ··· 117 157 118 158 // Event listener for session deletion/invalidation 119 159 export function onSessionDeleted(callback: (event: { sub: string, cause: any }) => void) { 120 - oauthClient.addEventListener('deleted', (event: any) => { 121 - const { sub, cause } = event.detail 122 - console.error(`Session for ${sub} was invalidated:`, cause) 123 - callback({ sub, cause }) 124 - }) 160 + // Only run on client side 161 + if (typeof window === 'undefined') { 162 + console.log('Skipping session deleted listener setup on server side') 163 + return 164 + } 165 + 166 + try { 167 + const client = getOAuthClient() 168 + client.addEventListener('deleted', (event: any) => { 169 + const { sub, cause } = event.detail 170 + console.error(`Session for ${sub} was invalidated:`, cause) 171 + callback({ sub, cause }) 172 + }) 173 + } catch (error) { 174 + console.error('Failed to set up session deleted listener:', error) 175 + } 125 176 }