import { useState, useCallback, useRef, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { authApi, settingsApi } from "@/lib/api"; import type { ManagedAuthProvider, ManagedAuthStatus, ManagedAuthDeviceCodeResponse, } from "@/lib/api"; type PollingState = "idle" | "polling" | "success" | "error"; export function useManagedAuth(authProvider: ManagedAuthProvider) { const queryClient = useQueryClient(); const queryKey = ["managed-auth-status", authProvider]; const [pollingState, setPollingState] = useState("idle"); const [deviceCode, setDeviceCode] = useState(null); const [error, setError] = useState(null); const pollingIntervalRef = useRef | null>( null, ); const pollingTimeoutRef = useRef | null>(null); const { data: authStatus, isLoading: isLoadingStatus, refetch: refetchStatus, } = useQuery({ queryKey, queryFn: () => authApi.authGetStatus(authProvider), staleTime: 30000, }); const stopPolling = useCallback(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (pollingTimeoutRef.current) { clearTimeout(pollingTimeoutRef.current); pollingTimeoutRef.current = null; } }, []); useEffect(() => { return () => { stopPolling(); }; }, [stopPolling]); const startLoginMutation = useMutation({ mutationFn: () => authApi.authStartLogin(authProvider), onSuccess: async (response) => { setDeviceCode(response); setPollingState("polling"); setError(null); try { await navigator.clipboard.writeText(response.user_code); } catch (e) { console.debug("[ManagedAuth] Failed to copy user code:", e); } try { await settingsApi.openExternal(response.verification_uri); } catch (e) { console.debug("[ManagedAuth] Failed to open browser:", e); } // Add a small buffer on top of GitHub's suggested interval to avoid // hitting slow_down responses too aggressively during device polling. const interval = Math.max((response.interval || 5) + 3, 8) * 1000; const expiresAt = Date.now() + response.expires_in * 1000; const pollOnce = async () => { if (Date.now() > expiresAt) { stopPolling(); setPollingState("error"); setError("Device code expired. Please try again."); return; } try { const newAccount = await authApi.authPollForAccount( authProvider, response.device_code, ); if (newAccount) { stopPolling(); setPollingState("success"); await refetchStatus(); await queryClient.invalidateQueries({ queryKey }); setPollingState("idle"); setDeviceCode(null); } } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); if ( !errorMessage.includes("pending") && !errorMessage.includes("slow_down") ) { stopPolling(); setPollingState("error"); setError(errorMessage); } } }; void pollOnce(); pollingIntervalRef.current = setInterval(pollOnce, interval); pollingTimeoutRef.current = setTimeout(() => { stopPolling(); setPollingState("error"); setError("Device code expired. Please try again."); }, response.expires_in * 1000); }, onError: (e) => { setPollingState("error"); setError(e instanceof Error ? e.message : String(e)); }, }); const logoutMutation = useMutation({ mutationFn: () => authApi.authLogout(authProvider), onSuccess: async () => { setPollingState("idle"); setDeviceCode(null); setError(null); queryClient.setQueryData(queryKey, { provider: authProvider, authenticated: false, default_account_id: null, accounts: [], }); await queryClient.invalidateQueries({ queryKey }); }, onError: async (e) => { console.error("[ManagedAuth] Failed to logout:", e); setError(e instanceof Error ? e.message : String(e)); await refetchStatus(); }, }); const removeAccountMutation = useMutation({ mutationFn: (accountId: string) => authApi.authRemoveAccount(authProvider, accountId), onSuccess: async () => { setPollingState("idle"); setDeviceCode(null); setError(null); await refetchStatus(); await queryClient.invalidateQueries({ queryKey }); }, onError: (e) => { console.error("[ManagedAuth] Failed to remove account:", e); setError(e instanceof Error ? e.message : String(e)); }, }); const setDefaultAccountMutation = useMutation({ mutationFn: (accountId: string) => authApi.authSetDefaultAccount(authProvider, accountId), onSuccess: async () => { await refetchStatus(); await queryClient.invalidateQueries({ queryKey }); }, onError: (e) => { console.error("[ManagedAuth] Failed to set default account:", e); setError(e instanceof Error ? e.message : String(e)); }, }); const startAuth = useCallback(() => { setPollingState("idle"); setDeviceCode(null); setError(null); stopPolling(); startLoginMutation.mutate(); }, [startLoginMutation, stopPolling]); const cancelAuth = useCallback(() => { stopPolling(); setPollingState("idle"); setDeviceCode(null); setError(null); }, [stopPolling]); const logout = useCallback(() => { logoutMutation.mutate(); }, [logoutMutation]); const removeAccount = useCallback( (accountId: string) => { removeAccountMutation.mutate(accountId); }, [removeAccountMutation], ); const setDefaultAccount = useCallback( (accountId: string) => { setDefaultAccountMutation.mutate(accountId); }, [setDefaultAccountMutation], ); const accounts = authStatus?.accounts ?? []; return { authStatus, isLoadingStatus, accounts, hasAnyAccount: accounts.length > 0, isAuthenticated: authStatus?.authenticated ?? false, defaultAccountId: authStatus?.default_account_id ?? null, migrationError: authStatus?.migration_error ?? null, pollingState, deviceCode, error, isPolling: pollingState === "polling", isAddingAccount: startLoginMutation.isPending || pollingState === "polling", isRemovingAccount: removeAccountMutation.isPending, isSettingDefaultAccount: setDefaultAccountMutation.isPending, startAuth, addAccount: startAuth, cancelAuth, logout, removeAccount, setDefaultAccount, refetchStatus, }; }