"use client"; import { createContext, useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ApiUser, AuthLoginRequest, AuthSignupResponse, AuthSignupRequest, UserUpdateRequest, } from "@/lib/api/types"; import { fetchCurrentUser, AUTH_SESSION_UPDATED_EVENT, getAuthToken, getStoredUser, loginWithEmailPassword, signupWithEmailPassword, logoutCurrentSession, updateCurrentUser, verifyLoginOtp, verifySignupOtp, } from "@/lib/auth/auth-client"; import { getPublicApiBaseUrl, getPublicDemoMode } from "@/lib/env"; type AuthLoginOptions = { staySignedIn?: boolean; rememberEmail?: boolean; }; type AuthContextValue = { user: ApiUser | null; isLoading: boolean; isDevMode: boolean; error: string | null; refreshUser: () => Promise; login: (payload: AuthLoginRequest, options?: AuthLoginOptions) => Promise; loginWithOtp: ( payload: { email: string; code: string }, options?: AuthLoginOptions, ) => Promise; signup: (payload: AuthSignupRequest) => Promise; verifySignup: (payload: { email: string; code: string }) => Promise; logout: () => void; updateProfile: (payload: UserUpdateRequest) => Promise; }; export const AuthContext = createContext(null); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [hasToken, setHasToken] = useState(false); const refreshSequence = useRef(0); const refreshUser = useCallback(async () => { const sequence = ++refreshSequence.current; setError(null); try { const nextUser = await fetchCurrentUser(); if (sequence !== refreshSequence.current) return; setUser(nextUser); setHasToken(Boolean(getAuthToken())); } catch (caughtError) { if (sequence !== refreshSequence.current) return; const message = caughtError instanceof Error ? caughtError.message : "Could not load user session."; // Distinguish auth errors (401/403) from network/server errors const isAuthError = message.includes("401") || message.includes("403") || message.toLowerCase().includes("unauthorized") || message.toLowerCase().includes("session expired"); if (isAuthError) { // Real auth failure — clear session, show guest state setUser(null); setHasToken(false); setError("Your session expired. Please sign in again."); } else { // Network or server error — preserve existing user to avoid data loss const stored = getStoredUser(); setUser(stored); setHasToken(Boolean(getAuthToken())); if (!stored) { setError("Could not connect to server. Please check your connection."); } // Don't set error if we have a stored user — silent retry is better UX } } finally { if (sequence === refreshSequence.current) setIsLoading(false); } }, []); useEffect(() => { const timeoutId = window.setTimeout(() => void refreshUser(), 0); return () => window.clearTimeout(timeoutId); }, [refreshUser]); // Background wake-up ping: HF Space sleeps on inactivity and takes ~60s to // restart. Fire a silent /health request immediately so the Space is awake // by the time the user tries to send a message. useEffect(() => { if (typeof window === "undefined") return; const base = getPublicApiBaseUrl(); const controller = new AbortController(); const timer = window.setTimeout(() => controller.abort(), 90_000); void fetch(`${base}/health`, { signal: controller.signal }).catch(() => {/* silent */}).finally(() => window.clearTimeout(timer)); return () => { controller.abort(); window.clearTimeout(timer); }; }, []); useEffect(() => { function handleSessionUpdated() { const storedUser = getStoredUser(); const token = getAuthToken(); setUser(storedUser); setHasToken(Boolean(token)); setError(null); setIsLoading(false); // Confirm the just-persisted session with the backend without delaying // navigation to onboarding or the workspace. void refreshUser(); } function handleSessionCleared() { // Session was explicitly cleared (401 received) — show sign-in prompt, not demo data refreshSequence.current += 1; setUser(null); setHasToken(false); setIsLoading(false); setError("Your session expired. Please sign in again."); } window.addEventListener(AUTH_SESSION_UPDATED_EVENT, handleSessionUpdated); window.addEventListener("docdoe-auth-session-cleared", handleSessionCleared); return () => { window.removeEventListener(AUTH_SESSION_UPDATED_EVENT, handleSessionUpdated); window.removeEventListener("docdoe-auth-session-cleared", handleSessionCleared); }; }, [refreshUser]); const value = useMemo( () => ({ user, isLoading, isDevMode: getPublicDemoMode() !== "live" && Boolean(user) && !hasToken, error, refreshUser, login: async (payload, options) => { setError(null); const response = await loginWithEmailPassword(payload, options); setUser(response.user); setHasToken(true); }, loginWithOtp: async (payload, options) => { setError(null); const response = await verifyLoginOtp(payload, options); setUser(response.user); setHasToken(true); }, signup: async (payload) => { setError(null); const response = await signupWithEmailPassword(payload); if ("access_token" in response) { setUser(response.user); setHasToken(true); } return response; }, verifySignup: async (payload) => { setError(null); const response = await verifySignupOtp(payload); setUser(response.user); setHasToken(true); }, logout: () => { refreshSequence.current += 1; logoutCurrentSession(); setUser(null); setHasToken(false); setIsLoading(false); }, updateProfile: async (payload) => { setError(null); const nextUser = await updateCurrentUser(payload); setUser(nextUser); }, }), [error, hasToken, isLoading, refreshUser, user], ); return {children}; }