import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import { supabase, isAuthConfigured, SUPABASE_URL, type Session, type User } from "../lib/supabase";
interface AuthState {
user: User | null;
session: Session | null;
isLoading: boolean;
isAuthenticated: boolean;
isAuthConfigured: boolean;
}
interface AuthContextValue extends AuthState {
/** Get the current access token for API calls */
getAccessToken: () => string | null;
/** Sign in with email and password */
signIn: (email: string, password: string) => Promise<{ error: Error | null }>;
/** Sign up with email and password */
signUp: (email: string, password: string) => Promise<{ error: Error | null }>;
/** Sign out */
signOut: () => Promise<void>;
/** Sign in with OAuth provider */
signInWithOAuth: (provider: "github" | "google") => Promise<{ error: Error | null }>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
user: null,
session: null,
isLoading: true,
isAuthenticated: false,
isAuthConfigured: isAuthConfigured(),
});
// Initialize auth state
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setState({
user: session?.user ?? null,
session,
isLoading: false,
isAuthenticated: !!session,
isAuthConfigured: true,
});
});
// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setState((prev) => ({
...prev,
user: session?.user ?? null,
session,
isAuthenticated: !!session,
}));
});
return () => subscription.unsubscribe();
}, []);
const getAccessToken = useCallback((): string | null => {
return state.session?.access_token ?? null;
}, [state.session]);
const signIn = useCallback(
async (email: string, password: string): Promise<{ error: Error | null }> => {
const { error } = await supabase.auth.signInWithPassword({ email, password });
return { error: error ? new Error(error.message) : null };
},
[]
);
const signUp = useCallback(
async (email: string, password: string): Promise<{ error: Error | null }> => {
const { error } = await supabase.auth.signUp({ email, password });
return { error: error ? new Error(error.message) : null };
},
[]
);
const signOut = useCallback(async () => {
// Always clear local state first
setState((prev) => ({
...prev,
user: null,
session: null,
isAuthenticated: false,
}));
// Clear Supabase storage directly in case signOut API fails
const storageKey = `sb-${SUPABASE_URL.split('//')[1]?.split('.')[0]}-auth-token`;
localStorage.removeItem(storageKey);
// Try to call signOut API (may fail if token is invalid, that's OK)
await supabase.auth.signOut({ scope: 'local' }).catch(() => {});
}, []);
const signInWithOAuth = useCallback(
async (provider: "github" | "google"): Promise<{ error: Error | null }> => {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: window.location.origin,
},
});
return { error: error ? new Error(error.message) : null };
},
[]
);
const value: AuthContextValue = {
...state,
getAccessToken,
signIn,
signUp,
signOut,
signInWithOAuth,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}