@clarityops/preferences 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/PreferencesContext.tsx","../src/PreferencesService.ts","../src/avatar-cache.ts"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useState,\n useEffect,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport type {\n UserPreferences,\n AvatarData,\n PreferencesConfig,\n PreferencesContextValue,\n} from './types';\nimport { PreferencesService } from './PreferencesService';\nimport {\n getCachedAvatar,\n setCachedAvatar,\n clearAvatarCache as clearCache,\n} from './avatar-cache';\n\nconst PreferencesContext = createContext<PreferencesContextValue | null>(null);\n\nexport interface PreferencesProviderProps {\n children: ReactNode;\n config: PreferencesConfig;\n /** Optional initial preferences to avoid loading state */\n initialPreferences?: UserPreferences;\n}\n\nexport function PreferencesProvider({\n children,\n config,\n initialPreferences,\n}: PreferencesProviderProps) {\n const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || {});\n const [loading, setLoading] = useState(!initialPreferences);\n const [error, setError] = useState<string | null>(null);\n\n // Avatar state\n const [avatarUrl, setAvatarUrl] = useState<string | null>(null);\n const [hasAvatar, setHasAvatar] = useState(false);\n const [avatarLoading, setAvatarLoading] = useState(true);\n const [avatarError, setAvatarError] = useState<string | null>(null);\n const refreshAttemptRef = useRef(0);\n\n // Store config in a ref to avoid dependency issues\n const configRef = useRef(config);\n configRef.current = config;\n\n // Service instance\n const serviceRef = useRef<PreferencesService | null>(null);\n if (!serviceRef.current) {\n serviceRef.current = new PreferencesService(config);\n }\n \n // Update service config when getToken or onTokenInvalid changes\n useEffect(() => {\n if (serviceRef.current) {\n serviceRef.current.updateConfig({\n getToken: config.getToken,\n onTokenInvalid: config.onTokenInvalid,\n });\n }\n }, [config.getToken, config.onTokenInvalid]);\n\n // Fetch preferences\n const refreshPreferences = useCallback(async () => {\n setLoading(true);\n setError(null);\n\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const response = await service.getPreferences();\n setPreferences(response.data.preferences);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to load preferences';\n setError(msg);\n console.error('Preferences fetch error:', err);\n } finally {\n setLoading(false);\n }\n }, []);\n\n // Fetch avatar\n const fetchAvatar = useCallback(async (skipCache = false) => {\n const cacheKey = 'me';\n\n // Check cache first\n if (!skipCache) {\n const cached = getCachedAvatar(cacheKey);\n if (cached) {\n setAvatarUrl(cached);\n setHasAvatar(true);\n setAvatarLoading(false);\n return;\n }\n }\n\n setAvatarLoading(true);\n setAvatarError(null);\n\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const data = await service.getMyAvatar();\n\n if (data.has_avatar && data.avatar_url) {\n setAvatarUrl(data.avatar_url);\n setHasAvatar(true);\n setCachedAvatar(cacheKey, data.avatar_url);\n refreshAttemptRef.current = 0;\n } else {\n setAvatarUrl(null);\n setHasAvatar(false);\n clearCache(cacheKey);\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to load avatar';\n setAvatarError(msg);\n setAvatarUrl(null);\n setHasAvatar(false);\n\n // Don't log 404s as errors\n if (!msg.includes('404') && !msg.includes('not found')) {\n console.error('Avatar fetch error:', err);\n }\n } finally {\n setAvatarLoading(false);\n }\n }, []);\n\n // Refresh avatar (bypasses cache)\n const refreshAvatar = useCallback(async () => {\n await fetchAvatar(true);\n }, [fetchAvatar]);\n\n // Handle image error (403/expired)\n const handleImageError = useCallback(() => {\n if (refreshAttemptRef.current >= 2) {\n console.warn('Avatar refresh limit reached');\n setAvatarUrl(null);\n setHasAvatar(false);\n return;\n }\n\n refreshAttemptRef.current += 1;\n console.log('Avatar image failed, refreshing (attempt', refreshAttemptRef.current, ')');\n\n clearCache('me');\n fetchAvatar(true);\n }, [fetchAvatar]);\n\n // Update preferences\n const updatePreferences = useCallback(async (prefs: Partial<UserPreferences>) => {\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const response = await service.updatePreferences(prefs);\n setPreferences(response.data.preferences);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to update preferences';\n setError(msg);\n throw err;\n }\n }, []);\n\n // Upload avatar\n const uploadAvatar = useCallback(async (file: File): Promise<string> => {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const url = await service.uploadAvatar(file);\n\n // Clear cache and refresh\n clearCache('me');\n await fetchAvatar(true);\n\n return url;\n }, [fetchAvatar]);\n\n // Clear avatar cache utility\n const clearAvatarCache = useCallback((userId?: string) => {\n clearCache(userId || 'me');\n }, []);\n\n // Apply theme to document\n const applyTheme = useCallback((theme: 'light' | 'dark' | 'system' | undefined) => {\n const root = document.documentElement;\n if (theme === 'dark') {\n root.classList.add('dark');\n } else if (theme === 'light') {\n root.classList.remove('dark');\n } else {\n // System preference\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n if (prefersDark) {\n root.classList.add('dark');\n } else {\n root.classList.remove('dark');\n }\n }\n }, []);\n\n // Sync preferences on login - fetches and applies theme immediately\n const syncOnLogin = useCallback(async () => {\n try {\n const service = serviceRef.current;\n if (!service) return;\n\n const response = await service.getPreferences();\n const prefs = response.data.preferences;\n setPreferences(prefs);\n \n // Apply theme immediately\n applyTheme(prefs.theme);\n \n // Also fetch avatar\n await fetchAvatar(true);\n } catch (err) {\n console.error('Failed to sync preferences on login:', err);\n }\n }, [applyTheme, fetchAvatar]);\n\n // Initial fetch - only if we have a token (run once on mount)\n useEffect(() => {\n const hasToken = !!configRef.current.getToken();\n if (!hasToken) {\n // No token yet, skip initial fetch - will be triggered by syncOnLogin\n setLoading(false);\n setAvatarLoading(false);\n return;\n }\n \n if (!initialPreferences) {\n refreshPreferences();\n }\n fetchAvatar();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Run only on mount - config is accessed via ref\n\n // Apply theme when preferences change (only if we have a token)\n useEffect(() => {\n const hasToken = !!configRef.current.getToken();\n if (hasToken && preferences.theme) {\n applyTheme(preferences.theme);\n }\n }, [preferences.theme, applyTheme]);\n\n const avatar: AvatarData = {\n avatarUrl,\n hasAvatar,\n loading: avatarLoading,\n error: avatarError,\n refresh: refreshAvatar,\n handleImageError,\n };\n\n const value: PreferencesContextValue = {\n preferences,\n loading,\n error,\n avatar,\n updatePreferences,\n refreshPreferences,\n uploadAvatar,\n clearAvatarCache,\n syncOnLogin,\n };\n\n return (\n <PreferencesContext.Provider value={value}>\n {children}\n </PreferencesContext.Provider>\n );\n}\n\n/**\n * Hook to access preferences context\n * Throws if used outside PreferencesProvider\n */\nexport function usePreferences(): PreferencesContextValue {\n const context = useContext(PreferencesContext);\n if (!context) {\n throw new Error('usePreferences must be used within a PreferencesProvider');\n }\n return context;\n}\n\n/**\n * Hook to safely access preferences context\n * Returns null if used outside PreferencesProvider (useful during hot reload)\n */\nexport function usePreferencesSafe(): PreferencesContextValue | null {\n return useContext(PreferencesContext);\n}\n\n/**\n * Hook to access just avatar data\n */\nexport function useAvatar(): AvatarData {\n const { avatar } = usePreferences();\n return avatar;\n}\n","import type {\n UserPreferences,\n PrepareAvatarUploadResponse,\n ConfirmAvatarUploadResponse,\n MyAvatarResponse,\n PreferencesConfig,\n} from './types';\n\n/**\n * Validate if a token is a well-formed JWT with 3 segments\n */\nfunction isValidJwtFormat(token: string | null | undefined): boolean {\n if (!token || typeof token !== 'string') return false;\n // Check for placeholder values\n if (token === 'null' || token === 'undefined' || token.trim() === '') return false;\n // JWT must have exactly 3 segments\n const segments = token.split('.');\n return segments.length === 3;\n}\n\n/**\n * Check if a JWT token is expired\n */\nfunction isTokenExpired(token: string): boolean {\n try {\n const payload = JSON.parse(atob(token.split('.')[1]));\n const currentTime = Math.floor(Date.now() / 1000);\n return payload.exp < currentTime;\n } catch {\n return true;\n }\n}\n\n// Shared refresh lock to prevent concurrent refresh attempts across service instances\nlet refreshInProgress: Promise<string | null> | null = null;\nlet lastRefreshTime = 0;\nconst REFRESH_DEBOUNCE_MS = 5000; // Minimum 5 seconds between refresh attempts\n\n/**\n * Service class for user preferences and avatar management\n * Shared across InsightForge and InsightFlow platforms\n */\nexport class PreferencesService {\n private baseUrl: string;\n private brandingUrl: string;\n private getToken: () => string | null;\n private onTokenInvalid?: () => Promise<string | null>;\n\n constructor(config: PreferencesConfig) {\n this.baseUrl = config.apiBaseUrl;\n this.brandingUrl = config.brandingUrl;\n this.getToken = config.getToken;\n this.onTokenInvalid = config.onTokenInvalid;\n }\n\n /**\n * Update the config (e.g., when token changes)\n */\n updateConfig(config: Partial<PreferencesConfig>) {\n if (config.getToken) {\n this.getToken = config.getToken;\n }\n if (config.onTokenInvalid !== undefined) {\n this.onTokenInvalid = config.onTokenInvalid;\n }\n if (config.apiBaseUrl) {\n this.baseUrl = config.apiBaseUrl;\n }\n if (config.brandingUrl) {\n this.brandingUrl = config.brandingUrl;\n }\n }\n\n /**\n * Refresh token with debounce to prevent multiple simultaneous refresh attempts\n */\n private async refreshWithDebounce(): Promise<string | null> {\n const now = Date.now();\n \n // If a refresh is already in progress, wait for it\n if (refreshInProgress) {\n console.log('[PreferencesService] ⏳ Waiting for existing refresh to complete...');\n return refreshInProgress;\n }\n \n // Debounce: if we just refreshed, get current token from localStorage\n if (now - lastRefreshTime < REFRESH_DEBOUNCE_MS) {\n console.log('[PreferencesService] ⏳ Recent refresh detected, using current token');\n return this.getToken();\n }\n \n // Start a new refresh\n if (this.onTokenInvalid) {\n console.log('[PreferencesService] 🔄 Starting token refresh...');\n lastRefreshTime = now;\n refreshInProgress = this.onTokenInvalid().finally(() => {\n refreshInProgress = null;\n });\n return refreshInProgress;\n }\n \n return null;\n }\n\n /**\n * Get a valid token, refreshing if necessary with debounce protection\n */\n private async getValidToken(): Promise<string> {\n let token = this.getToken();\n \n // Check if token is valid format\n if (!isValidJwtFormat(token)) {\n console.warn('[PreferencesService] Token is missing or malformed, attempting refresh...');\n token = await this.refreshWithDebounce();\n if (!isValidJwtFormat(token)) {\n throw new Error('Authentication required - no valid token available');\n }\n }\n \n // Check if token is expired\n if (isTokenExpired(token!)) {\n console.warn('[PreferencesService] Token is expired, attempting refresh...');\n token = await this.refreshWithDebounce();\n if (!token || isTokenExpired(token)) {\n throw new Error('Session expired - please log in again');\n }\n }\n \n return token!;\n }\n\n private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Request failed' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401, attempting token refresh and retry...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n \n if (retryResponse.ok) {\n return retryResponse.json();\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Request failed');\n }\n\n const data = await response.json();\n return data;\n }\n\n /**\n * Get user preferences\n */\n async getPreferences(): Promise<{ data: { preferences: UserPreferences } }> {\n return this.request('/forge/users/me/preferences');\n }\n\n /**\n * Update preferences (merge update)\n */\n async updatePreferences(preferences: Partial<UserPreferences>): Promise<{ data: { preferences: UserPreferences }; message?: string }> {\n return this.request('/forge/users/me/preferences', {\n method: 'PATCH',\n body: JSON.stringify(preferences),\n });\n }\n\n /**\n * Replace all preferences\n */\n async replacePreferences(preferences: UserPreferences): Promise<{ data: { preferences: UserPreferences }; message?: string }> {\n return this.request('/forge/users/me/preferences', {\n method: 'PUT',\n body: JSON.stringify(preferences),\n });\n }\n\n /**\n * Step 1: Prepare avatar upload - get signed URL for GCS\n */\n async prepareAvatarUpload(filename: string, mimeType: string): Promise<PrepareAvatarUploadResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n filename,\n mime_type: mimeType,\n }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to prepare upload' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on prepareAvatarUpload, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n filename,\n mime_type: mimeType,\n }),\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to prepare avatar upload');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to prepare avatar upload');\n }\n\n return data.data;\n }\n\n /**\n * Step 2: Upload file directly to GCS\n */\n async uploadToGCS(uploadUrl: string, file: File): Promise<void> {\n const response = await fetch(uploadUrl, {\n method: 'PUT',\n headers: {\n 'Content-Type': file.type,\n },\n body: file,\n });\n\n if (!response.ok) {\n throw new Error('Failed to upload file to storage');\n }\n }\n\n /**\n * Step 3: Confirm avatar upload\n */\n async confirmAvatarUpload(storagePath: string): Promise<ConfirmAvatarUploadResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n storage_path: storagePath,\n }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to confirm upload' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on confirmAvatarUpload, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n storage_path: storagePath,\n }),\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to confirm avatar upload');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to confirm avatar upload');\n }\n\n return data.data;\n }\n\n /**\n * Get current user's avatar\n */\n async getMyAvatar(): Promise<MyAvatarResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/my-avatar`, {\n headers: {\n 'Authorization': `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to fetch avatar' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on getMyAvatar, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/my-avatar`, {\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n },\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to fetch avatar');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to fetch avatar');\n }\n\n return data.data;\n }\n\n /**\n * Get any user's avatar\n */\n async getUserAvatar(userId: string): Promise<MyAvatarResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {\n headers: {\n 'Authorization': `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to fetch avatar' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on getUserAvatar, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n },\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to fetch avatar');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to fetch avatar');\n }\n\n return data.data;\n }\n\n /**\n * Complete avatar upload flow (3-step process)\n */\n async uploadAvatar(file: File): Promise<string> {\n // Validate file type\n const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];\n if (!validTypes.includes(file.type)) {\n throw new Error('Please select a PNG, JPEG, or WebP image');\n }\n\n // Validate file size (5MB max)\n const maxSize = 5 * 1024 * 1024;\n if (file.size > maxSize) {\n throw new Error('Image must be less than 5MB');\n }\n\n // Step 1: Prepare upload\n const prepareData = await this.prepareAvatarUpload(file.name, file.type);\n\n // Step 2: Upload to GCS\n await this.uploadToGCS(prepareData.upload_url, file);\n\n // Step 3: Confirm upload\n const confirmData = await this.confirmAvatarUpload(prepareData.storage_path);\n\n return confirmData.avatar_url;\n }\n}\n","/**\n * In-memory cache for avatar URLs\n * Shared across all components using the preferences context\n */\n\ninterface CacheEntry {\n url: string;\n cachedAt: number;\n}\n\n// Cache TTL: 1 day (as per API guidance)\nexport const AVATAR_CACHE_TTL = 24 * 60 * 60 * 1000;\n\n// Global in-memory cache\nconst avatarCache = new Map<string, CacheEntry>();\n\n/**\n * Get cached avatar URL if valid\n */\nexport function getCachedAvatar(userId: string): string | null {\n const cached = avatarCache.get(userId);\n if (cached && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL) {\n return cached.url;\n }\n return null;\n}\n\n/**\n * Set avatar URL in cache\n */\nexport function setCachedAvatar(userId: string, url: string): void {\n avatarCache.set(userId, {\n url,\n cachedAt: Date.now(),\n });\n}\n\n/**\n * Clear cached avatar for a specific user or all users\n */\nexport function clearAvatarCache(userId?: string): void {\n if (userId) {\n avatarCache.delete(userId);\n } else {\n avatarCache.clear();\n }\n}\n\n/**\n * Check if cache entry exists and is valid\n */\nexport function hasCachedAvatar(userId: string): boolean {\n const cached = avatarCache.get(userId);\n return cached !== undefined && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL;\n}\n"],"mappings":"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","names":["data","clearAvatarCache"]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@clarityops/preferences",
3
+ "version": "0.1.1",
4
+ "description": "Shared user preferences context for ClarityOps platforms",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": ">=18.0.0",
21
+ "react-dom": ">=18.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/react": "^18.2.0",
25
+ "react": "^18.2.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.3.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "directory": "packages/preferences"
35
+ },
36
+ "license": "MIT",
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "dev": "tsup --watch",
40
+ "typecheck": "tsc --noEmit"
41
+ }
42
+ }
@@ -0,0 +1,309 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useEffect,
6
+ useCallback,
7
+ useRef,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import type {
11
+ UserPreferences,
12
+ AvatarData,
13
+ PreferencesConfig,
14
+ PreferencesContextValue,
15
+ } from './types';
16
+ import { PreferencesService } from './PreferencesService';
17
+ import {
18
+ getCachedAvatar,
19
+ setCachedAvatar,
20
+ clearAvatarCache as clearCache,
21
+ } from './avatar-cache';
22
+
23
+ const PreferencesContext = createContext<PreferencesContextValue | null>(null);
24
+
25
+ export interface PreferencesProviderProps {
26
+ children: ReactNode;
27
+ config: PreferencesConfig;
28
+ /** Optional initial preferences to avoid loading state */
29
+ initialPreferences?: UserPreferences;
30
+ }
31
+
32
+ export function PreferencesProvider({
33
+ children,
34
+ config,
35
+ initialPreferences,
36
+ }: PreferencesProviderProps) {
37
+ const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || {});
38
+ const [loading, setLoading] = useState(!initialPreferences);
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ // Avatar state
42
+ const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
43
+ const [hasAvatar, setHasAvatar] = useState(false);
44
+ const [avatarLoading, setAvatarLoading] = useState(true);
45
+ const [avatarError, setAvatarError] = useState<string | null>(null);
46
+ const refreshAttemptRef = useRef(0);
47
+
48
+ // Store config in a ref to avoid dependency issues
49
+ const configRef = useRef(config);
50
+ configRef.current = config;
51
+
52
+ // Service instance
53
+ const serviceRef = useRef<PreferencesService | null>(null);
54
+ if (!serviceRef.current) {
55
+ serviceRef.current = new PreferencesService(config);
56
+ }
57
+
58
+ // Update service config when getToken or onTokenInvalid changes
59
+ useEffect(() => {
60
+ if (serviceRef.current) {
61
+ serviceRef.current.updateConfig({
62
+ getToken: config.getToken,
63
+ onTokenInvalid: config.onTokenInvalid,
64
+ });
65
+ }
66
+ }, [config.getToken, config.onTokenInvalid]);
67
+
68
+ // Fetch preferences
69
+ const refreshPreferences = useCallback(async () => {
70
+ setLoading(true);
71
+ setError(null);
72
+
73
+ try {
74
+ const service = serviceRef.current;
75
+ if (!service) throw new Error('Service not initialized');
76
+
77
+ const response = await service.getPreferences();
78
+ setPreferences(response.data.preferences);
79
+ } catch (err) {
80
+ const msg = err instanceof Error ? err.message : 'Failed to load preferences';
81
+ setError(msg);
82
+ console.error('Preferences fetch error:', err);
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ }, []);
87
+
88
+ // Fetch avatar
89
+ const fetchAvatar = useCallback(async (skipCache = false) => {
90
+ const cacheKey = 'me';
91
+
92
+ // Check cache first
93
+ if (!skipCache) {
94
+ const cached = getCachedAvatar(cacheKey);
95
+ if (cached) {
96
+ setAvatarUrl(cached);
97
+ setHasAvatar(true);
98
+ setAvatarLoading(false);
99
+ return;
100
+ }
101
+ }
102
+
103
+ setAvatarLoading(true);
104
+ setAvatarError(null);
105
+
106
+ try {
107
+ const service = serviceRef.current;
108
+ if (!service) throw new Error('Service not initialized');
109
+
110
+ const data = await service.getMyAvatar();
111
+
112
+ if (data.has_avatar && data.avatar_url) {
113
+ setAvatarUrl(data.avatar_url);
114
+ setHasAvatar(true);
115
+ setCachedAvatar(cacheKey, data.avatar_url);
116
+ refreshAttemptRef.current = 0;
117
+ } else {
118
+ setAvatarUrl(null);
119
+ setHasAvatar(false);
120
+ clearCache(cacheKey);
121
+ }
122
+ } catch (err) {
123
+ const msg = err instanceof Error ? err.message : 'Failed to load avatar';
124
+ setAvatarError(msg);
125
+ setAvatarUrl(null);
126
+ setHasAvatar(false);
127
+
128
+ // Don't log 404s as errors
129
+ if (!msg.includes('404') && !msg.includes('not found')) {
130
+ console.error('Avatar fetch error:', err);
131
+ }
132
+ } finally {
133
+ setAvatarLoading(false);
134
+ }
135
+ }, []);
136
+
137
+ // Refresh avatar (bypasses cache)
138
+ const refreshAvatar = useCallback(async () => {
139
+ await fetchAvatar(true);
140
+ }, [fetchAvatar]);
141
+
142
+ // Handle image error (403/expired)
143
+ const handleImageError = useCallback(() => {
144
+ if (refreshAttemptRef.current >= 2) {
145
+ console.warn('Avatar refresh limit reached');
146
+ setAvatarUrl(null);
147
+ setHasAvatar(false);
148
+ return;
149
+ }
150
+
151
+ refreshAttemptRef.current += 1;
152
+ console.log('Avatar image failed, refreshing (attempt', refreshAttemptRef.current, ')');
153
+
154
+ clearCache('me');
155
+ fetchAvatar(true);
156
+ }, [fetchAvatar]);
157
+
158
+ // Update preferences
159
+ const updatePreferences = useCallback(async (prefs: Partial<UserPreferences>) => {
160
+ try {
161
+ const service = serviceRef.current;
162
+ if (!service) throw new Error('Service not initialized');
163
+
164
+ const response = await service.updatePreferences(prefs);
165
+ setPreferences(response.data.preferences);
166
+ } catch (err) {
167
+ const msg = err instanceof Error ? err.message : 'Failed to update preferences';
168
+ setError(msg);
169
+ throw err;
170
+ }
171
+ }, []);
172
+
173
+ // Upload avatar
174
+ const uploadAvatar = useCallback(async (file: File): Promise<string> => {
175
+ const service = serviceRef.current;
176
+ if (!service) throw new Error('Service not initialized');
177
+
178
+ const url = await service.uploadAvatar(file);
179
+
180
+ // Clear cache and refresh
181
+ clearCache('me');
182
+ await fetchAvatar(true);
183
+
184
+ return url;
185
+ }, [fetchAvatar]);
186
+
187
+ // Clear avatar cache utility
188
+ const clearAvatarCache = useCallback((userId?: string) => {
189
+ clearCache(userId || 'me');
190
+ }, []);
191
+
192
+ // Apply theme to document
193
+ const applyTheme = useCallback((theme: 'light' | 'dark' | 'system' | undefined) => {
194
+ const root = document.documentElement;
195
+ if (theme === 'dark') {
196
+ root.classList.add('dark');
197
+ } else if (theme === 'light') {
198
+ root.classList.remove('dark');
199
+ } else {
200
+ // System preference
201
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
202
+ if (prefersDark) {
203
+ root.classList.add('dark');
204
+ } else {
205
+ root.classList.remove('dark');
206
+ }
207
+ }
208
+ }, []);
209
+
210
+ // Sync preferences on login - fetches and applies theme immediately
211
+ const syncOnLogin = useCallback(async () => {
212
+ try {
213
+ const service = serviceRef.current;
214
+ if (!service) return;
215
+
216
+ const response = await service.getPreferences();
217
+ const prefs = response.data.preferences;
218
+ setPreferences(prefs);
219
+
220
+ // Apply theme immediately
221
+ applyTheme(prefs.theme);
222
+
223
+ // Also fetch avatar
224
+ await fetchAvatar(true);
225
+ } catch (err) {
226
+ console.error('Failed to sync preferences on login:', err);
227
+ }
228
+ }, [applyTheme, fetchAvatar]);
229
+
230
+ // Initial fetch - only if we have a token (run once on mount)
231
+ useEffect(() => {
232
+ const hasToken = !!configRef.current.getToken();
233
+ if (!hasToken) {
234
+ // No token yet, skip initial fetch - will be triggered by syncOnLogin
235
+ setLoading(false);
236
+ setAvatarLoading(false);
237
+ return;
238
+ }
239
+
240
+ if (!initialPreferences) {
241
+ refreshPreferences();
242
+ }
243
+ fetchAvatar();
244
+ // eslint-disable-next-line react-hooks/exhaustive-deps
245
+ }, []); // Run only on mount - config is accessed via ref
246
+
247
+ // Apply theme when preferences change (only if we have a token)
248
+ useEffect(() => {
249
+ const hasToken = !!configRef.current.getToken();
250
+ if (hasToken && preferences.theme) {
251
+ applyTheme(preferences.theme);
252
+ }
253
+ }, [preferences.theme, applyTheme]);
254
+
255
+ const avatar: AvatarData = {
256
+ avatarUrl,
257
+ hasAvatar,
258
+ loading: avatarLoading,
259
+ error: avatarError,
260
+ refresh: refreshAvatar,
261
+ handleImageError,
262
+ };
263
+
264
+ const value: PreferencesContextValue = {
265
+ preferences,
266
+ loading,
267
+ error,
268
+ avatar,
269
+ updatePreferences,
270
+ refreshPreferences,
271
+ uploadAvatar,
272
+ clearAvatarCache,
273
+ syncOnLogin,
274
+ };
275
+
276
+ return (
277
+ <PreferencesContext.Provider value={value}>
278
+ {children}
279
+ </PreferencesContext.Provider>
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Hook to access preferences context
285
+ * Throws if used outside PreferencesProvider
286
+ */
287
+ export function usePreferences(): PreferencesContextValue {
288
+ const context = useContext(PreferencesContext);
289
+ if (!context) {
290
+ throw new Error('usePreferences must be used within a PreferencesProvider');
291
+ }
292
+ return context;
293
+ }
294
+
295
+ /**
296
+ * Hook to safely access preferences context
297
+ * Returns null if used outside PreferencesProvider (useful during hot reload)
298
+ */
299
+ export function usePreferencesSafe(): PreferencesContextValue | null {
300
+ return useContext(PreferencesContext);
301
+ }
302
+
303
+ /**
304
+ * Hook to access just avatar data
305
+ */
306
+ export function useAvatar(): AvatarData {
307
+ const { avatar } = usePreferences();
308
+ return avatar;
309
+ }