import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import apiClient from '@app/services/apiClient'; /** * Sleep utility for delays */ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } export interface AppConfigRetryOptions { maxRetries?: number; initialDelay?: number; } export interface AppConfig { baseUrl?: string; contextPath?: string; serverPort?: number; appNameNavbar?: string; languages?: string[]; logoStyle?: 'modern' | 'classic'; enableLogin?: boolean; enableEmailInvites?: boolean; isAdmin?: boolean; enableAlphaFunctionality?: boolean; enableAnalytics?: boolean | null; enablePosthog?: boolean | null; enableScarf?: boolean | null; premiumEnabled?: boolean; premiumKey?: string; termsAndConditions?: string; privacyPolicy?: string; cookiePolicy?: string; impressum?: string; accessibilityStatement?: string; runningProOrHigher?: boolean; runningEE?: boolean; license?: string; SSOAutoLogin?: boolean; serverCertificateEnabled?: boolean; appVersion?: string; machineType?: string; activeSecurity?: boolean; error?: string; } export type AppConfigBootstrapMode = 'blocking' | 'non-blocking'; interface AppConfigContextValue { config: AppConfig | null; loading: boolean; error: string | null; refetch: () => Promise; } const AppConfigContext = createContext({ config: null, loading: true, error: null, refetch: async () => {}, }); /** * Provider component that fetches and provides app configuration * Should be placed at the top level of the app, before any components that need config */ export interface AppConfigProviderProps { children: ReactNode; retryOptions?: AppConfigRetryOptions; initialConfig?: AppConfig | null; bootstrapMode?: AppConfigBootstrapMode; autoFetch?: boolean; } export const AppConfigProvider: React.FC = ({ children, retryOptions, initialConfig = null, bootstrapMode = 'blocking', autoFetch = true, }) => { const isBlockingMode = bootstrapMode === 'blocking'; const [config, setConfig] = useState(initialConfig); const [error, setError] = useState(null); const [fetchCount, setFetchCount] = useState(0); const [hasResolvedConfig, setHasResolvedConfig] = useState(Boolean(initialConfig) && !isBlockingMode); const [loading, setLoading] = useState(!hasResolvedConfig); const maxRetries = retryOptions?.maxRetries ?? 0; const initialDelay = retryOptions?.initialDelay ?? 1000; const fetchConfig = useCallback(async (force = false) => { // Prevent duplicate fetches unless forced if (!force && fetchCount > 0) { console.debug('[AppConfig] Already fetched, skipping'); return; } const shouldBlockUI = !hasResolvedConfig || isBlockingMode; if (shouldBlockUI) { setLoading(true); } setError(null); for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { const delay = initialDelay * Math.pow(2, attempt - 1); console.log(`[AppConfig] Retry attempt ${attempt}/${maxRetries} after ${delay}ms delay...`); await sleep(delay); } else { console.log('[AppConfig] Fetching app config...'); } // apiClient automatically adds JWT header if available via interceptors const response = await apiClient.get('/api/v1/config/app-config', !isBlockingMode ? { suppressErrorToast: true } : undefined); const data = response.data; console.debug('[AppConfig] Config fetched successfully:', data); setConfig(data); setFetchCount(prev => prev + 1); setHasResolvedConfig(true); setLoading(false); return; // Success - exit function } catch (err: any) { const status = err?.response?.status; // On 401 (not authenticated), use default config with login enabled // This allows the app to work even without authentication if (status === 401) { console.debug('[AppConfig] 401 error - using default config (login enabled)'); setConfig({ enableLogin: true }); setHasResolvedConfig(true); setLoading(false); return; } // Check if we should retry (network errors or 5xx errors) const shouldRetry = (!status || status >= 500) && attempt < maxRetries; if (shouldRetry) { console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, err.message, '- will retry...'); continue; } // Final attempt failed or non-retryable error (4xx) const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred'; setError(errorMessage); console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err); // Preserve existing config (initial default or previous fetch). If nothing is set, assume login enabled. setConfig((current) => current ?? { enableLogin: true }); setHasResolvedConfig(true); break; } } setLoading(false); }, [fetchCount, hasResolvedConfig, isBlockingMode, maxRetries, initialDelay]); useEffect(() => { // Always try to fetch config to check if login is disabled // The endpoint should be public and return proper JSON if (autoFetch) { fetchConfig(); } }, [autoFetch, fetchConfig]); // Listen for JWT availability (triggered on login/signup) useEffect(() => { const handleJwtAvailable = () => { console.debug('[AppConfig] JWT available event - refetching config'); // Force refetch with JWT fetchConfig(true); }; window.addEventListener('jwt-available', handleJwtAvailable); return () => window.removeEventListener('jwt-available', handleJwtAvailable); }, [fetchConfig]); const value: AppConfigContextValue = { config, loading, error, refetch: () => fetchConfig(true), }; return ( {children} ); }; /** * Hook to access application configuration * Must be used within AppConfigProvider */ export function useAppConfig(): AppConfigContextValue { const context = useContext(AppConfigContext); if (context === undefined) { throw new Error('useAppConfig must be used within AppConfigProvider'); } return context; }