diff --git a/src/hooks/useRealtimeConfig.ts b/src/hooks/useRealtimeConfig.ts new file mode 100644 index 0000000..e569b06 --- /dev/null +++ b/src/hooks/useRealtimeConfig.ts @@ -0,0 +1,30 @@ +import { useState, useCallback } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +import { configService, SettingsConfig } from '../services/configService'; + +export const useRealtimeConfig = () => { + const [config, setConfig] = useState(null); + + const loadConfig = useCallback(async () => { + try { + const fetchedConfig = await configService.getSettingsConfig(); + + // Deep compare to avoid unnecessary re-renders + setConfig(prev => { + const prevStr = JSON.stringify(prev); + const newStr = JSON.stringify(fetchedConfig); + return prevStr === newStr ? prev : fetchedConfig; + }); + } catch (error) { + if (__DEV__) console.warn('Config fetch failed', error); + } + }, []); + + useFocusEffect( + useCallback(() => { + loadConfig(); // Fetch on focus (will use memory cache if available) + }, [loadConfig]) + ); + + return config; +}; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index b54612f..29893ac 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,4 +1,8 @@ +import { useFocusEffect } from '@react-navigation/native'; import React, { useCallback, useState, useEffect, useRef } from 'react'; +import { useRealtimeConfig } from '../hooks/useRealtimeConfig'; + + import { View, Text, @@ -316,11 +320,13 @@ const SettingsScreen: React.FC = () => { const [catalogCount, setCatalogCount] = useState(0); const [mdblistKeySet, setMdblistKeySet] = useState(false); const [openRouterKeySet, setOpenRouterKeySet] = useState(false); - const [initialLoadComplete, setInitialLoadComplete] = useState(false); - const [totalDownloads, setTotalDownloads] = useState(null); + const [totalDownloads, setTotalDownloads] = useState(0); const [displayDownloads, setDisplayDownloads] = useState(null); const [isCountingUp, setIsCountingUp] = useState(false); + // Use Realtime Config Hook + const settingsConfig = useRealtimeConfig(); + // Scroll to top ref and handler const mobileScrollViewRef = useRef(null); const tabletScrollViewRef = useRef(null); @@ -354,7 +360,6 @@ const SettingsScreen: React.FC = () => { // Load addon count and get their catalogs const addons = await stremioService.getInstalledAddonsAsync(); setAddonCount(addons.length); - setInitialLoadComplete(true); // Count total available catalogs let totalCatalogs = 0; @@ -525,8 +530,17 @@ const SettingsScreen: React.FC = () => { /> ); + // Helper to check item visibility + const isItemVisible = (itemId: string) => { + if (!settingsConfig?.items) return true; + const item = settingsConfig.items[itemId]; + if (item && item.visible === false) return false; + return true; + }; + // Filter categories based on conditions const visibleCategories = SETTINGS_CATEGORIES.filter(category => { + if (settingsConfig?.categories?.[category.id]?.visible === false) return false; if (category.id === 'developer' && !__DEV__) return false; if (category.id === 'cache' && !mdblistKeySet) return false; return true; @@ -539,110 +553,130 @@ const SettingsScreen: React.FC = () => { case 'account': return ( - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('TraktSettings')} - isLast={true} - isTablet={isTablet} - /> + {isItemVisible('trakt') && ( + } + renderControl={ChevronRight} + onPress={() => navigation.navigate('TraktSettings')} + isLast={true} + isTablet={isTablet} + /> + )} ); case 'content': return ( - navigation.navigate('Addons')} - isTablet={isTablet} - /> - navigation.navigate('DebridIntegration')} - isTablet={isTablet} - /> - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('ScraperSettings')} - isTablet={isTablet} - /> - navigation.navigate('CatalogSettings')} - isTablet={isTablet} - /> - navigation.navigate('HomeScreenSettings')} - isTablet={isTablet} - /> - ( - updateSetting('showDiscover', value)} - /> - )} - isTablet={isTablet} - /> - navigation.navigate('ContinueWatchingSettings')} - isLast={true} - isTablet={isTablet} - /> + {isItemVisible('addons') && ( + navigation.navigate('Addons')} + isTablet={isTablet} + /> + )} + {isItemVisible('debrid') && ( + navigation.navigate('DebridIntegration')} + isTablet={isTablet} + /> + )} + {isItemVisible('plugins') && ( + } + renderControl={ChevronRight} + onPress={() => navigation.navigate('ScraperSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('catalogs') && ( + navigation.navigate('CatalogSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('home_screen') && ( + navigation.navigate('HomeScreenSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('show_discover') && ( + ( + updateSetting('showDiscover', value)} + /> + )} + isTablet={isTablet} + /> + )} + {isItemVisible('continue_watching') && ( + navigation.navigate('ContinueWatchingSettings')} + isLast={true} + isTablet={isTablet} + /> + )} ); case 'appearance': return ( - navigation.navigate('ThemeSettings')} - isTablet={isTablet} - /> - ( - updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} - /> - )} - isLast={isTablet} - isTablet={isTablet} - /> - {!isTablet && ( + {isItemVisible('theme') && ( + navigation.navigate('ThemeSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('episode_layout') && ( + ( + updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} + /> + )} + isLast={isTablet} + isTablet={isTablet} + /> + )} + {!isTablet && isItemVisible('streams_backdrop') && ( { case 'integrations': return ( - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('MDBListSettings')} - isTablet={isTablet} - /> - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('TMDBSettings')} - isLast={true} - isTablet={isTablet} - /> + {isItemVisible('mdblist') && ( + } + renderControl={ChevronRight} + onPress={() => navigation.navigate('MDBListSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('tmdb') && ( + } + renderControl={ChevronRight} + onPress={() => navigation.navigate('TMDBSettings')} + isLast={true} + isTablet={isTablet} + /> + )} ); case 'ai': return ( - navigation.navigate('AISettings')} - isLast={true} - isTablet={isTablet} - /> + {isItemVisible('openrouter') && ( + navigation.navigate('AISettings')} + isLast={true} + isTablet={isTablet} + /> + )} ); case 'playback': return ( - navigation.navigate('PlayerSettings')} - isTablet={isTablet} - /> - ( - updateSetting('showTrailers', value)} - trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} - thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'} - /> - )} - isTablet={isTablet} - /> - ( - updateSetting('enableDownloads', value)} - trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} - thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} - /> - )} - isTablet={isTablet} - /> - navigation.navigate('NotificationSettings')} - isLast={true} - isTablet={isTablet} - /> + {isItemVisible('video_player') && ( + navigation.navigate('PlayerSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('show_trailers') && ( + ( + updateSetting('showTrailers', value)} + trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} + thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'} + /> + )} + isTablet={isTablet} + /> + )} + {isItemVisible('enable_downloads') && ( + ( + updateSetting('enableDownloads', value)} + trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} + thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} + /> + )} + isTablet={isTablet} + /> + )} + {isItemVisible('notifications') && ( + navigation.navigate('NotificationSettings')} + isLast={true} + isTablet={isTablet} + /> + )} ); @@ -1079,75 +1127,103 @@ const SettingsScreen: React.FC = () => { contentContainerStyle={styles.scrollContent} > {/* Account */} - - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('TraktSettings')} - isLast - /> - + {(settingsConfig?.categories?.['account']?.visible !== false) && isItemVisible('trakt') && ( + + {isItemVisible('trakt') && ( + } + renderControl={ChevronRight} + onPress={() => navigation.navigate('TraktSettings')} + isLast + /> + )} + + )} {/* General Settings */} - - navigation.navigate('ContentDiscoverySettings')} - /> - navigation.navigate('AppearanceSettings')} - /> - navigation.navigate('IntegrationsSettings')} - /> - navigation.navigate('PlaybackSettings')} - isLast - /> - + {( + (settingsConfig?.categories?.['content']?.visible !== false) || + (settingsConfig?.categories?.['appearance']?.visible !== false) || + (settingsConfig?.categories?.['integrations']?.visible !== false) || + (settingsConfig?.categories?.['playback']?.visible !== false) + ) && ( + + {(settingsConfig?.categories?.['content']?.visible !== false) && ( + navigation.navigate('ContentDiscoverySettings')} + /> + )} + {(settingsConfig?.categories?.['appearance']?.visible !== false) && ( + navigation.navigate('AppearanceSettings')} + /> + )} + {(settingsConfig?.categories?.['integrations']?.visible !== false) && ( + navigation.navigate('IntegrationsSettings')} + /> + )} + {(settingsConfig?.categories?.['playback']?.visible !== false) && ( + navigation.navigate('PlaybackSettings')} + isLast + /> + )} + + )} {/* Data */} - - navigation.navigate('Backup')} - /> - { - if (Platform.OS === 'android') { - try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } - setHasUpdateBadge(false); - } - navigation.navigate('Update'); - }} - isLast - /> - + {( + (settingsConfig?.categories?.['backup']?.visible !== false) || + (settingsConfig?.categories?.['updates']?.visible !== false) + ) && ( + + {(settingsConfig?.categories?.['backup']?.visible !== false) && ( + navigation.navigate('Backup')} + /> + )} + {(settingsConfig?.categories?.['updates']?.visible !== false) && ( + { + if (Platform.OS === 'android') { + try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } + setHasUpdateBadge(false); + } + navigation.navigate('Update'); + }} + isLast + /> + )} + + )} {/* Cache - only if MDBList is set */} {mdblistKeySet && ( @@ -1188,7 +1264,7 @@ const SettingsScreen: React.FC = () => { )} {/* Downloads Counter */} - {displayDownloads !== null && ( + {settingsConfig?.items?.['downloads_counter']?.visible !== false && displayDownloads !== null && ( {displayDownloads.toLocaleString()} diff --git a/src/screens/settings/AppearanceSettingsScreen.tsx b/src/screens/settings/AppearanceSettingsScreen.tsx index 37e57d0..5a67e02 100644 --- a/src/screens/settings/AppearanceSettingsScreen.tsx +++ b/src/screens/settings/AppearanceSettingsScreen.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, useCallback } from 'react'; import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../contexts/ThemeContext'; @@ -8,6 +8,7 @@ import { useSettings } from '../../hooks/useSettings'; import { RootStackParamList } from '../../navigation/AppNavigator'; import ScreenHeader from '../../components/common/ScreenHeader'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; +import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; const { width } = Dimensions.get('window'); const isTablet = width >= 768; @@ -17,6 +18,21 @@ const AppearanceSettingsScreen: React.FC = () => { const { currentTheme } = useTheme(); const { settings, updateSetting } = useSettings(); const insets = useSafeAreaInsets(); + const config = useRealtimeConfig(); + + const isItemVisible = (itemId: string) => { + if (!config?.items) return true; + const item = config.items[itemId]; + if (item && item.visible === false) return false; + return true; + }; + + const hasVisibleItems = (itemIds: string[]) => { + return itemIds.some(id => { + if (id === 'streams_backdrop' && isTablet) return false; + return isItemVisible(id); + }); + }; return ( @@ -28,45 +44,53 @@ const AppearanceSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - - } - onPress={() => navigation.navigate('ThemeSettings')} - isLast - /> - - - - ( - updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} + {hasVisibleItems(['theme']) && ( + + {isItemVisible('theme') && ( + } + onPress={() => navigation.navigate('ThemeSettings')} + isLast /> )} - isLast={isTablet} - /> - {!isTablet && ( - ( - updateSetting('enableStreamsBackdrop', value)} - /> - )} - isLast - /> - )} - + + )} + + {hasVisibleItems(['episode_layout', 'streams_backdrop']) && ( + + {isItemVisible('episode_layout') && ( + ( + updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} + /> + )} + isLast={isTablet || !isItemVisible('streams_backdrop')} + /> + )} + {!isTablet && isItemVisible('streams_backdrop') && ( + ( + updateSetting('enableStreamsBackdrop', value)} + /> + )} + isLast + /> + )} + + )} ); diff --git a/src/screens/settings/ContentDiscoverySettingsScreen.tsx b/src/screens/settings/ContentDiscoverySettingsScreen.tsx index 09ee996..a928799 100644 --- a/src/screens/settings/ContentDiscoverySettingsScreen.tsx +++ b/src/screens/settings/ContentDiscoverySettingsScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { View, StyleSheet, ScrollView, StatusBar, Platform } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../contexts/ThemeContext'; @@ -11,6 +11,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator'; import ScreenHeader from '../../components/common/ScreenHeader'; import PluginIcon from '../../components/icons/PluginIcon'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; +import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; const ContentDiscoverySettingsScreen: React.FC = () => { const navigation = useNavigation>(); @@ -20,6 +21,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => { const [addonCount, setAddonCount] = useState(0); const [catalogCount, setCatalogCount] = useState(0); + const config = useRealtimeConfig(); const loadData = useCallback(async () => { try { @@ -48,16 +50,22 @@ const ContentDiscoverySettingsScreen: React.FC = () => { } }, []); - useEffect(() => { - loadData(); - }, [loadData]); - - useEffect(() => { - const unsubscribe = navigation.addListener('focus', () => { + useFocusEffect( + useCallback(() => { loadData(); - }); - return unsubscribe; - }, [navigation, loadData]); + }, [loadData]) + ); + + const isItemVisible = (itemId: string) => { + if (!config?.items) return true; + const item = config.items[itemId]; + if (item && item.visible === false) return false; + return true; + }; + + const hasVisibleItems = (itemIds: string[]) => { + return itemIds.some(id => isItemVisible(id)); + }; return ( @@ -69,70 +77,90 @@ const ContentDiscoverySettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - - } - onPress={() => navigation.navigate('Addons')} - /> - } - onPress={() => navigation.navigate('DebridIntegration')} - /> - } - renderControl={() => } - onPress={() => navigation.navigate('ScraperSettings')} - isLast - /> - - - - } - onPress={() => navigation.navigate('CatalogSettings')} - /> - } - onPress={() => navigation.navigate('HomeScreenSettings')} - /> - } - onPress={() => navigation.navigate('ContinueWatchingSettings')} - isLast - /> - - - - ( - updateSetting('showDiscover', value)} + {hasVisibleItems(['addons', 'debrid', 'plugins']) && ( + + {isItemVisible('addons') && ( + } + onPress={() => navigation.navigate('Addons')} /> )} - isLast - /> - + {isItemVisible('debrid') && ( + } + onPress={() => navigation.navigate('DebridIntegration')} + /> + )} + {isItemVisible('plugins') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('ScraperSettings')} + isLast + /> + )} + + )} + + {hasVisibleItems(['catalogs', 'home_screen', 'continue_watching']) && ( + + {isItemVisible('catalogs') && ( + } + onPress={() => navigation.navigate('CatalogSettings')} + /> + )} + {isItemVisible('home_screen') && ( + } + onPress={() => navigation.navigate('HomeScreenSettings')} + /> + )} + {isItemVisible('continue_watching') && ( + } + onPress={() => navigation.navigate('ContinueWatchingSettings')} + isLast + /> + )} + + )} + + {hasVisibleItems(['show_discover']) && ( + + {isItemVisible('show_discover') && ( + ( + updateSetting('showDiscover', value)} + /> + )} + isLast + /> + )} + + )} ); diff --git a/src/screens/settings/IntegrationsSettingsScreen.tsx b/src/screens/settings/IntegrationsSettingsScreen.tsx index a7c10aa..887caee 100644 --- a/src/screens/settings/IntegrationsSettingsScreen.tsx +++ b/src/screens/settings/IntegrationsSettingsScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../contexts/ThemeContext'; @@ -10,6 +10,7 @@ import ScreenHeader from '../../components/common/ScreenHeader'; import MDBListIcon from '../../components/icons/MDBListIcon'; import TMDBIcon from '../../components/icons/TMDBIcon'; import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents'; +import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; const IntegrationsSettingsScreen: React.FC = () => { const navigation = useNavigation>(); @@ -18,6 +19,7 @@ const IntegrationsSettingsScreen: React.FC = () => { const [mdblistKeySet, setMdblistKeySet] = useState(false); const [openRouterKeySet, setOpenRouterKeySet] = useState(false); + const config = useRealtimeConfig(); const loadData = useCallback(async () => { try { @@ -31,16 +33,22 @@ const IntegrationsSettingsScreen: React.FC = () => { } }, []); - useEffect(() => { - loadData(); - }, [loadData]); - - useEffect(() => { - const unsubscribe = navigation.addListener('focus', () => { + useFocusEffect( + useCallback(() => { loadData(); - }); - return unsubscribe; - }, [navigation, loadData]); + }, [loadData]) + ); + + const isItemVisible = (itemId: string) => { + if (!config?.items) return true; + const item = config.items[itemId]; + if (item && item.visible === false) return false; + return true; + }; + + const hasVisibleItems = (itemIds: string[]) => { + return itemIds.some(id => isItemVisible(id)); + }; return ( @@ -52,34 +60,44 @@ const IntegrationsSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - - } - renderControl={() => } - onPress={() => navigation.navigate('MDBListSettings')} - /> - } - renderControl={() => } - onPress={() => navigation.navigate('TMDBSettings')} - isLast - /> - + {hasVisibleItems(['mdblist', 'tmdb']) && ( + + {isItemVisible('mdblist') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('MDBListSettings')} + /> + )} + {isItemVisible('tmdb') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('TMDBSettings')} + isLast + /> + )} + + )} - - } - onPress={() => navigation.navigate('AISettings')} - isLast - /> - + {hasVisibleItems(['openrouter']) && ( + + {isItemVisible('openrouter') && ( + } + onPress={() => navigation.navigate('AISettings')} + isLast + /> + )} + + )} ); diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index a82b3d5..db02bc7 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, useCallback } from 'react'; import { View, StyleSheet, ScrollView, StatusBar, Platform } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../contexts/ThemeContext'; @@ -8,12 +8,25 @@ import { useSettings } from '../../hooks/useSettings'; import { RootStackParamList } from '../../navigation/AppNavigator'; import ScreenHeader from '../../components/common/ScreenHeader'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; +import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; const PlaybackSettingsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings, updateSetting } = useSettings(); const insets = useSafeAreaInsets(); + const config = useRealtimeConfig(); + + const isItemVisible = (itemId: string) => { + if (!config?.items) return true; + const item = config.items[itemId]; + if (item && item.visible === false) return false; + return true; + }; + + const hasVisibleItems = (itemIds: string[]) => { + return itemIds.some(id => isItemVisible(id)); + }; return ( @@ -25,56 +38,70 @@ const PlaybackSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - - } - onPress={() => navigation.navigate('PlayerSettings')} - isLast - /> - - - - ( - updateSetting('showTrailers', value)} + {hasVisibleItems(['video_player']) && ( + + {isItemVisible('video_player') && ( + } + onPress={() => navigation.navigate('PlayerSettings')} + isLast /> )} - /> - ( - updateSetting('enableDownloads', value)} + + )} + + {hasVisibleItems(['show_trailers', 'enable_downloads']) && ( + + {isItemVisible('show_trailers') && ( + ( + updateSetting('showTrailers', value)} + /> + )} /> )} - isLast - /> - + {isItemVisible('enable_downloads') && ( + ( + updateSetting('enableDownloads', value)} + /> + )} + isLast + /> + )} + + )} - - } - onPress={() => navigation.navigate('NotificationSettings')} - isLast - /> - + {hasVisibleItems(['notifications']) && ( + + {isItemVisible('notifications') && ( + } + onPress={() => navigation.navigate('NotificationSettings')} + isLast + /> + )} + + )} ); diff --git a/src/services/configService.ts b/src/services/configService.ts new file mode 100644 index 0000000..44b819c --- /dev/null +++ b/src/services/configService.ts @@ -0,0 +1,59 @@ +import { Platform } from 'react-native'; + +// Reuse the same base URL as campaign service +const CAMPAIGN_API_URL = process.env.EXPO_PUBLIC_CAMPAIGN_API_URL || 'http://localhost:3000'; + +export interface SettingsConfig { + categories?: { + [key: string]: { + visible?: boolean; + order?: number; + title?: string; + } + }; + items?: { + [key: string]: { + visible?: boolean; + [key: string]: any; + } + }; +} + +class ConfigService { + private configCache: Record = {}; + + async getConfig(key: string): Promise { + // Return memory cache if available (fetch once per session) + if (this.configCache[key]) { + return this.configCache[key] as T; + } + + try { + console.log(`[ConfigService] Fetching config for key: ${key}`); + const response = await fetch(`${CAMPAIGN_API_URL}/api/config?key=${key}`); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + + // If data is empty object, return null + if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) { + return null; + } + + this.configCache[key] = data; + return data as T; + } catch (error) { + console.warn('[ConfigService] Error fetching config:', error); + return null; + } + } + + async getSettingsConfig(): Promise { + return this.getConfig('settings_screen'); + } +} + +export const configService = new ConfigService();