From 3291478e27c6ef5e2cb3809aba36096888660913 Mon Sep 17 00:00:00 2001 From: Najuna Date: Tue, 24 Feb 2026 15:18:56 +0300 Subject: [PATCH] formulus: add app-config driven native tab visibility --- formulus/src/navigation/MainTabNavigator.tsx | 140 +++++++++++++------ formulus/src/types/AppConfig.ts | 21 +++ formulus/src/types/NavigationTypes.ts | 10 ++ 3 files changed, 126 insertions(+), 45 deletions(-) diff --git a/formulus/src/navigation/MainTabNavigator.tsx b/formulus/src/navigation/MainTabNavigator.tsx index 34287c22b..ea7d85e8b 100644 --- a/formulus/src/navigation/MainTabNavigator.tsx +++ b/formulus/src/navigation/MainTabNavigator.tsx @@ -11,7 +11,12 @@ import AboutScreen from '../screens/AboutScreen'; import HelpScreen from '../screens/HelpScreen'; import MoreScreen from '../screens/MoreScreen'; import { colors } from '../theme/colors'; -import { MainTabParamList } from '../types/NavigationTypes'; +import AppConfigService from '../services/AppConfigService'; +import { + MainTabParamList, + VisibleMainTab, + VISIBLE_MAIN_TABS, +} from '../types/NavigationTypes'; import { useAppTheme } from '../contexts/AppThemeContext'; const Tab = createBottomTabNavigator(); @@ -41,6 +46,64 @@ const renderMoreIcon = ({ color, size }: TabBarIconProps) => ( ); +const TAB_COMPONENTS: Record< + VisibleMainTab, + React.ComponentType> +> = { + Home: HomeScreen as React.ComponentType>, + Forms: FormsScreen as React.ComponentType>, + Observations: ObservationsScreen as React.ComponentType< + Record + >, + Sync: SyncScreen as React.ComponentType>, + More: MoreScreen as React.ComponentType>, +}; + +const TAB_ICONS: Record< + VisibleMainTab, + (props: TabBarIconProps) => React.ReactElement +> = { + Home: renderHomeIcon, + Forms: renderFormsIcon, + Observations: renderObservationsIcon, + Sync: renderSyncIcon, + More: renderMoreIcon, +}; + +const isVisibleMainTab = (value: string): value is VisibleMainTab => + (VISIBLE_MAIN_TABS as readonly string[]).includes(value); + +const getConfiguredTabs = (): { + tabs: VisibleMainTab[]; + tabConfigPresent: boolean; +} => { + const config = AppConfigService.getInstance().getConfig(); + const requestedTabs = config?.navigation?.tabs; + + if (requestedTabs === undefined) { + return { tabs: [...VISIBLE_MAIN_TABS], tabConfigPresent: false }; + } + + if (!Array.isArray(requestedTabs)) { + console.warn( + '[Navigation] app.config.json navigation.tabs must be an array. Falling back to defaults.', + ); + return { tabs: [...VISIBLE_MAIN_TABS], tabConfigPresent: true }; + } + + const tabs = requestedTabs.filter(tab => { + const isValid = isVisibleMainTab(tab); + if (!isValid) { + console.warn( + `[Navigation] Invalid tab "${tab}" in app.config.json. Skipping.`, + ); + } + return isValid; + }); + + return { tabs, tabConfigPresent: true }; +}; + const MainTabNavigator: React.FC = () => { const insets = useSafeAreaInsets(); const baseTabBarHeight = 60; @@ -49,6 +112,9 @@ const MainTabNavigator: React.FC = () => { // Theme colors come from AppThemeContext — they update automatically // when the custom app's config is loaded or the color scheme changes. const { themeColors } = useAppTheme(); + const { tabs: configuredTabs, tabConfigPresent } = getConfiguredTabs(); + const hideTabBar = tabConfigPresent && configuredTabs.length === 0; + const tabsToRender = hideTabBar ? (['Home'] as const) : configuredTabs; return ( { tabBarActiveTintColor: themeColors.primary, tabBarInactiveTintColor: colors.neutral[500], tabBarStyle: { + display: hideTabBar ? 'none' : 'flex', backgroundColor: themeColors.surface, borderTopWidth: 1, borderTopColor: themeColors.divider, @@ -65,34 +132,33 @@ const MainTabNavigator: React.FC = () => { height: tabBarHeight, }, }}> - - - - + {tabsToRender.map(tabName => ( + ({ + tabPress: () => { + const state = navigation.getState(); + const currentRoute = state.routes[state.index]; + if (currentRoute?.name === 'More') { + ( + navigation as { setParams: (params: object) => void } + ).setParams({ + openDrawer: Date.now(), + }); + } + }, + }) + : undefined + } + /> + ))} { tabBarButton: () => null, }} /> - ({ - tabPress: () => { - const state = navigation.getState(); - const currentRoute = state.routes[state.index]; - if (currentRoute?.name === 'More') { - navigation.setParams({ openDrawer: Date.now() }); - } - }, - })} - /> ); }; diff --git a/formulus/src/types/AppConfig.ts b/formulus/src/types/AppConfig.ts index e2bbcaa41..785ef370a 100644 --- a/formulus/src/types/AppConfig.ts +++ b/formulus/src/types/AppConfig.ts @@ -8,6 +8,7 @@ * (tab bar, headers, modals) and to forward theme colors to the Formplayer * WebView so that forms match the custom app's look and feel. */ +import { VisibleMainTab } from './NavigationTypes'; /** * Color tokens for a single mode (light or dark). @@ -72,6 +73,17 @@ export interface AppTheme { dark: ThemeColors; } +/** + * Native navigation configuration for the Formulus tab bar. + */ +export interface NavigationConfig { + /** + * Visible native tabs in display order. + * Accepts string values from app.config.json and is validated at runtime. + */ + tabs: string[]; +} + /** * Root shape of app.config.json. */ @@ -84,4 +96,13 @@ export interface AppConfig { version: string; /** Theme definition with light and dark palettes */ theme: AppTheme; + /** + * Optional native navigation settings. + * If omitted, Formulus shows all default native tabs. + */ + navigation?: NavigationConfig; } + +// Keep this alias exported so app-config consumers can strongly type +// validated tab selections after runtime filtering. +export type { VisibleMainTab }; diff --git a/formulus/src/types/NavigationTypes.ts b/formulus/src/types/NavigationTypes.ts index a74a406c8..02cfc5c84 100644 --- a/formulus/src/types/NavigationTypes.ts +++ b/formulus/src/types/NavigationTypes.ts @@ -9,6 +9,16 @@ export type MainTabParamList = { More: { openDrawer?: number } | undefined; }; +export const VISIBLE_MAIN_TABS = [ + 'Home', + 'Forms', + 'Observations', + 'Sync', + 'More', +] as const; + +export type VisibleMainTab = (typeof VISIBLE_MAIN_TABS)[number]; + export type MainAppStackParamList = { Welcome: undefined; MainApp: undefined;