import { useState, useCallback, useEffect } from 'react'; import toast from 'react-hot-toast'; import { useAuthenticatedFetch } from './useAuthenticatedFetch'; import type { UserPreferences } from '../types'; import { COLOR_SCHEMES } from '../types'; const LOCAL_API_KEYS_STORAGE = 'kaboot.apiKeys'; type LocalApiKeys = Pick; const normalizeApiKey = (value: unknown): string | undefined => { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); return trimmed ? trimmed : undefined; }; const readLocalApiKeys = (): LocalApiKeys => { if (typeof window === 'undefined') return {}; try { const raw = window.localStorage.getItem(LOCAL_API_KEYS_STORAGE); if (!raw) return {}; const parsed = JSON.parse(raw) as Record; return { geminiApiKey: normalizeApiKey(parsed.geminiApiKey), openRouterApiKey: normalizeApiKey(parsed.openRouterApiKey), openAIApiKey: normalizeApiKey(parsed.openAIApiKey), }; } catch { return {}; } }; const writeLocalApiKeys = (keys: LocalApiKeys) => { if (typeof window === 'undefined') return; const normalized = { geminiApiKey: normalizeApiKey(keys.geminiApiKey), openRouterApiKey: normalizeApiKey(keys.openRouterApiKey), openAIApiKey: normalizeApiKey(keys.openAIApiKey), }; const hasAny = Boolean( normalized.geminiApiKey || normalized.openRouterApiKey || normalized.openAIApiKey ); if (!hasAny) { window.localStorage.removeItem(LOCAL_API_KEYS_STORAGE); return; } window.localStorage.setItem(LOCAL_API_KEYS_STORAGE, JSON.stringify(normalized)); }; const mergePreferencesWithLocalKeys = (prefs: UserPreferences, localKeys: LocalApiKeys): UserPreferences => ({ ...prefs, geminiApiKey: localKeys.geminiApiKey ?? prefs.geminiApiKey, openRouterApiKey: localKeys.openRouterApiKey ?? prefs.openRouterApiKey, openAIApiKey: localKeys.openAIApiKey ?? prefs.openAIApiKey, }); const DEFAULT_PREFERENCES: UserPreferences = { colorScheme: 'blue', aiProvider: 'gemini', }; export const applyColorScheme = (schemeId: string) => { const scheme = COLOR_SCHEMES.find(s => s.id === schemeId) || COLOR_SCHEMES[0]; document.documentElement.style.setProperty('--theme-primary', scheme.primary); document.documentElement.style.setProperty('--theme-primary-dark', scheme.primaryDark); document.documentElement.style.setProperty('--theme-primary-darker', scheme.primaryDarker); }; interface SubscriptionInfo { hasAccess: boolean; accessType: 'group' | 'subscription' | 'none'; generationCount: number | null; generationLimit: number | null; generationsRemaining: number | null; } interface UseUserPreferencesReturn { preferences: UserPreferences; hasAIAccess: boolean; hasEarlyAccess: boolean; subscription: SubscriptionInfo | null; loading: boolean; saving: boolean; fetchPreferences: () => Promise; savePreferences: (prefs: UserPreferences) => Promise; applyColorScheme: (schemeId: string) => void; } export const useUserPreferences = (): UseUserPreferencesReturn => { const { authFetch, isAuthenticated } = useAuthenticatedFetch(); const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); const [hasAIAccess, setHasAIAccess] = useState(false); const [hasEarlyAccess, setHasEarlyAccess] = useState(false); const [subscription, setSubscription] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const fetchPreferences = useCallback(async () => { if (!isAuthenticated) return; setLoading(true); try { const response = await authFetch('/api/users/me/preferences'); if (response.ok) { const data = await response.json(); const serverPrefs: UserPreferences = { colorScheme: data.colorScheme || 'blue', aiProvider: data.aiProvider || 'gemini', geminiApiKey: data.geminiApiKey || undefined, geminiModel: data.geminiModel || undefined, openRouterApiKey: data.openRouterApiKey || undefined, openRouterModel: data.openRouterModel || undefined, openAIApiKey: data.openAIApiKey || undefined, openAIModel: data.openAIModel || undefined, }; const localKeys = readLocalApiKeys(); const mergedPrefs = mergePreferencesWithLocalKeys(serverPrefs, localKeys); const mergedLocalKeys: LocalApiKeys = { geminiApiKey: localKeys.geminiApiKey ?? serverPrefs.geminiApiKey, openRouterApiKey: localKeys.openRouterApiKey ?? serverPrefs.openRouterApiKey, openAIApiKey: localKeys.openAIApiKey ?? serverPrefs.openAIApiKey, }; writeLocalApiKeys(mergedLocalKeys); setPreferences(mergedPrefs); setHasAIAccess(data.hasAIAccess || false); setHasEarlyAccess(data.hasEarlyAccess || false); applyColorScheme(mergedPrefs.colorScheme); const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001'; try { const subResponse = await authFetch(`${backendUrl}/api/payments/status`); if (subResponse.ok) { const subData = await subResponse.json(); setSubscription({ hasAccess: subData.hasAccess, accessType: subData.accessType, generationCount: subData.generationCount, generationLimit: subData.generationLimit, generationsRemaining: subData.generationsRemaining, }); } } catch { // Payments not configured, ignore } } } catch { } finally { setLoading(false); } }, [authFetch, isAuthenticated]); const savePreferences = useCallback(async (prefs: UserPreferences) => { setSaving(true); try { const nextPrefs: UserPreferences = { ...preferences, ...prefs }; const localKeys: LocalApiKeys = { geminiApiKey: nextPrefs.geminiApiKey, openRouterApiKey: nextPrefs.openRouterApiKey, openAIApiKey: nextPrefs.openAIApiKey, }; writeLocalApiKeys(localKeys); const serverPrefs = { ...nextPrefs, geminiApiKey: undefined, openRouterApiKey: undefined, openAIApiKey: undefined, }; const response = await authFetch('/api/users/me/preferences', { method: 'PUT', body: JSON.stringify(serverPrefs), }); if (!response.ok) { throw new Error('Failed to save preferences'); } const normalizedLocalKeys = readLocalApiKeys(); const mergedPrefs = mergePreferencesWithLocalKeys(nextPrefs, normalizedLocalKeys); setPreferences(mergedPrefs); applyColorScheme(mergedPrefs.colorScheme); toast.success('Preferences saved!'); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to save preferences'; toast.error(message); throw err; } finally { setSaving(false); } }, [authFetch, preferences]); useEffect(() => { fetchPreferences(); }, [fetchPreferences]); return { preferences, hasAIAccess, hasEarlyAccess, subscription, loading, saving, fetchPreferences, savePreferences, applyColorScheme, }; };