The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
1'use client';
2
3import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
4import { OAuthSession } from '@atproto/oauth-client-browser';
5
6interface AuthContextType {
7 session: OAuthSession | null;
8 isAuthenticated: boolean;
9 isLoading: boolean;
10 signIn: (handle: string) => Promise<void>;
11 signOut: () => Promise<void>;
12 restoreSession: (did: string) => Promise<OAuthSession>;
13 // Legacy compatibility properties for existing code
14 accessToken: string | null;
15 refreshToken: string | null;
16 did: string | null;
17 handle: string | null;
18 pdsEndpoint: string | null;
19}
20
21const AuthContext = createContext<AuthContextType | undefined>(undefined);
22
23interface AuthProviderProps {
24 children: ReactNode;
25}
26
27export function AuthProvider({ children }: AuthProviderProps) {
28 const [session, setSession] = useState<OAuthSession | null>(null);
29 const [isLoading, setIsLoading] = useState(true);
30 const [isClient, setIsClient] = useState(false);
31
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)
38 useEffect(() => {
39 if (!isClient) return;
40
41 async function initialize() {
42 try {
43 setIsLoading(true);
44
45 // Dynamic import to ensure client-side only execution
46 const { initializeOAuthClient } = await import('./oauth-client');
47 const result = await initializeOAuthClient();
48
49 if (result) {
50 console.log('Initialized with existing session:', result.session.sub);
51 setSession(result.session);
52 }
53 } catch (error) {
54 console.error('Failed to initialize OAuth client:', error);
55 } finally {
56 setIsLoading(false);
57 }
58 }
59
60 initialize();
61 }, [isClient]);
62
63 // Set up session deletion listener (client-side only)
64 useEffect(() => {
65 if (!isClient) return;
66
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]);
84
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
90 try {
91 const { signIn } = await import('./oauth-client');
92 await signIn(handle);
93 // Note: This will redirect, so we won't reach this point
94 } catch (error) {
95 console.error('Sign in failed:', error);
96 throw error;
97 }
98 };
99
100 const handleSignOut = async () => {
101 if (!isClient) {
102 throw new Error('Sign out can only be called on the client side');
103 }
104
105 try {
106 const { signOut } = await import('./oauth-client');
107 await signOut();
108 setSession(null);
109 } catch (error) {
110 console.error('Sign out failed:', error);
111 throw error;
112 }
113 };
114
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
120 try {
121 const { restoreSession } = await import('./oauth-client');
122 const restoredSession = await restoreSession(did);
123 setSession(restoredSession);
124 return restoredSession;
125 } catch (error) {
126 console.error('Failed to restore session:', error);
127 throw error;
128 }
129 };
130
131 const contextValue: AuthContextType = {
132 session,
133 isAuthenticated: !!session,
134 isLoading: isLoading || !isClient, // Keep loading until client-side hydration
135 signIn: handleSignIn,
136 signOut: handleSignOut,
137 restoreSession: handleRestoreSession,
138 // Legacy compatibility - provide basic properties
139 accessToken: session ? 'available' : null, // Session manages tokens internally
140 refreshToken: session ? 'available' : null, // Session manages tokens internally
141 did: session?.sub || null,
142 handle: null, // Will be fetched by components when needed
143 pdsEndpoint: null // Not exposed by the new OAuth client
144 };
145
146 return (
147 <AuthContext.Provider value={contextValue}>
148 {children}
149 </AuthContext.Provider>
150 );
151}
152
153export function useAuth() {
154 const context = useContext(AuthContext);
155 if (context === undefined) {
156 throw new Error('useAuth must be used within an AuthProvider');
157 }
158 return context;
159}