import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; import { Platform, AppState, AppStateStatus } from 'react-native'; import { storage } from '../utils/storage'; import { ApiService } from '../services/api'; import NetInfo from '@react-native-community/netinfo'; import { DebugLogger } from '../utils/logger'; interface AppConfig { branding: { app_name?: string; app_tagline?: string; app_icon_url?: string; logo_url?: string; splash_image_url?: string; brand_color?: string; theme_color_primary?: string; theme_color_secondary?: string; primary_font_family?: string; }; control_center: { kill_switch_active?: boolean; kill_switch_message?: string; maintenance_start_at?: string; maintenance_end_at?: string; maintenance_bypass_ips?: string; announcement_enabled?: boolean; announcement_text?: string; announcement_type?: 'info' | 'warning' | 'danger'; }; app_updates: { app_version?: string; min_app_version?: string; onboarding_version?: string; store_url_android?: string; store_url_ios?: string; store_url_huawei?: string; }; features: { enable_registration?: boolean; enable_guest_mode?: boolean; require_otp_registration?: boolean; enable_biometrics?: boolean; enable_remember_me?: boolean; review_prompt_enabled?: boolean; min_actions_before_review?: number; region_lock_enabled?: boolean; dashboard_categories?: string; }; security_auth: { login_title?: string; login_subtitle?: string; token_ttl_minutes?: number; session_max_age?: number; login_max_attempts?: number; biometric_auth_type?: string; oauth_google_enabled?: boolean; oauth_apple_enabled?: boolean; oauth_facebook_enabled?: boolean; }; connectivity: { api_base_url?: string; api_version?: string; api_timeout_ms?: number; api_retry_count?: number; request_cache_ttl?: number; sync_interval_ms?: number; enable_ssl_pinning?: boolean; ssl_pinning_hash?: string; environment_selector?: string; }; notifications: { enable_push_notifications?: boolean; fcm_topic_default?: string; default_channel_id?: string; notification_sound_enabled?: boolean; badge_count_enabled?: boolean; priority_level?: string; }; support_social: { support_email?: string; support_whatsapp?: string; live_chat_url?: string; faq_url?: string; privacy_policy_url?: string; social_instagram_url?: string; social_twitter_url?: string; social_facebook_url?: string; social_youtube_url?: string; faq_json?: any[]; help_topics_json?: any[]; }; analytics_system: { crashlytics_enabled?: boolean; log_level?: string; event_sampling_rate?: string; google_analytics_id?: string; gdpr_compliance_enabled?: boolean; target_sdk_version?: string; system_timezone?: string; default_locale?: string; }; localization?: { [lang: string]: Record; }; } interface ConfigContextType { config: AppConfig | null; isLoading: boolean; isSyncing: boolean; isConnected: boolean; syncConfig: () => Promise; } const ConfigContext = createContext(undefined); const STORAGE_KEY = 'cached_mobile_config'; const ETAG_KEY = 'cached_config_etag'; // Minimum sync interval: 3 seconds to feel "instant" while avoiding excessive load const MIN_SYNC_INTERVAL_MS = 3_000; const DEFAULT_SYNC_INTERVAL_MS = 5_000; export function ConfigProvider({ children }: { children: React.ReactNode }) { const [config, setConfig] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isConnected, setIsConnected] = useState(true); const [isSyncing, setIsSyncing] = useState(false); const isSyncingRef = useRef(false); const intervalRef = useRef | null>(null); const prevConnected = useRef(null); useEffect(() => { // 1. Monitor network connection const unsubscribeNet = NetInfo.addEventListener(state => { const connected = !!state.isConnected; if (prevConnected.current !== null && prevConnected.current !== connected) { DebugLogger.log(`Network status changed: ${connected ? 'Online' : 'Offline'}`, connected ? 'success' : 'error'); if (connected) syncConfig(); } setIsConnected(connected); prevConnected.current = connected; }); // 2. Monitor AppState (Sync on foreground) const subscription = AppState.addEventListener('change', (nextAppState) => { if (nextAppState === 'active') { syncConfig(); } }); loadInitialConfig(); return () => { unsubscribeNet(); subscription.remove(); if (intervalRef.current) clearInterval(intervalRef.current); }; }, []); // Re-setup interval when config changes (for dynamic sync_interval_ms) useEffect(() => { if (intervalRef.current) clearInterval(intervalRef.current); const rawInterval = config?.connectivity?.sync_interval_ms ?? DEFAULT_SYNC_INTERVAL_MS; // Clamp to minimum to avoid hammering server const safeInterval = Math.max(rawInterval, MIN_SYNC_INTERVAL_MS); intervalRef.current = setInterval(() => { syncConfig(); }, safeInterval); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [config?.connectivity?.sync_interval_ms]); const loadInitialConfig = async () => { try { // 1. Load from cache first (offline-first approach) const str = await storage.get(STORAGE_KEY); if (str) { try { const cached = JSON.parse(str); setConfig(cached); setIsLoading(false); } catch { // Cache is corrupted, remove it await storage.remove(STORAGE_KEY); } } // 2. Fetch fresh config in background (non-blocking) await syncConfig(); } catch (error) { console.warn('[Config] Initialization error:', error); } finally { setIsLoading(false); } }; const syncConfig = async () => { if (isSyncingRef.current) return; isSyncingRef.current = true; setIsSyncing(true); try { const etag = await storage.get(ETAG_KEY); const response = await ApiService.syncMobileConfig(etag); if (response?.status === 'not_modified') { DebugLogger.log('Config is up to date (ETag match)', 'sync'); return; } if (response?.status === 'success' && response?.data) { const freshConfig = response.data; setConfig(freshConfig); DebugLogger.log(`Config updated with ETag: ${response.etag?.substring(0, 8)}...`, 'sync'); // Persist to cache await storage.save(STORAGE_KEY, JSON.stringify(freshConfig)); if (response.etag) { await storage.save(ETAG_KEY, response.etag); } } } catch (error: any) { DebugLogger.log(`Sync error: ${error.message}`, 'error'); console.warn('[Config] Sync failed (offline?):', error); } finally { isSyncingRef.current = false; setIsSyncing(false); setIsLoading(false); } }; return ( {children} ); } export function useAppConfig() { const context = useContext(ConfigContext); if (context === undefined) { return { config: { branding: { app_name: 'biiproject' }, control_center: {}, app_updates: {}, features: {}, security_auth: {}, connectivity: {}, notifications: {}, support_social: {}, analytics_system: {} }, isLoading: false, isSyncing: false, isConnected: true, syncConfig: async () => {} } as any; } return context; }