Skip to content

Crash on Android on App Start up (Hibob) #8138

@oferRounds

Description

@oferRounds

What happened?

During app launch, our app crashes on android from time to time. This had started after we upgraded to the version 8.*, with RN 0.77.3 with new arch enabled
The current version we use is the latest – 8.4.2

Image Image

This is our current App.tsx:

import { applyMiddleware, type Store } from 'redux'
import ReduxThunk from 'redux-thunk'
import promiseMiddleware from 'redux-promise-middleware'
import {
	initializeApp,
	logOut,
	performNeededActionsOnAppBackgrounding,
	type RenewSessionCompletionData,
	renewUserSessionIfNeeded
} from './actions/AppInitializationActions'
import 'react-native-reanimated'
// prevent a crash on gesture-handler component after react-native moved to inline requires
import 'react-native-gesture-handler'
import type { BobState } from './reducers'
import reducers from './reducers'
import {
	APP_LAUNCHER_LOADER,
	PEOPLE_LIST_SCREEN_ID,
	APP_MENU_TAB_SCREEN,
	HOME_SCREEN_ID,
	LOGIN_SCREEN_ID,
	SCREEN_TITLE_COMPONENT_ID,
	STORYBOOK_SCREEN_ID
} from './screens/ScreensIds'
import { registerScreens } from './screens/screens'
import {
	UNDETERMINED_LOGIN_STATE,
	type UNDETERMINED_LOGIN_STATE_CASE,
	NOT_LOGGED_IN_STATE,
	LOGGED_IN_STATE,
	type NOT_LOGGED_IN_STATE_CASE,
	type LOGGED_IN_STATE_CASE,
	HOMEPAGE_WHOSOUT,
	HOMEPAGE_NEWJOINERS,
	HOMEPAGE_BIRTHDAYS,
	HOMEPAGE_ANNIVERSARIES,
	HOMEPAGE_SHOUTOUTS,
	DIRECTORY_FEATURE,
	ORG_CHART_FEATURE,
	TRY_DEMO_PREFIX,
	type BobDispatchReturn,
	HIBOB_COMPANY_ID
} from './consts'
import { screenWidth, getSafeTheme, clearThemeCache } from './services/AppearanceService'
import { AppState, type AppStateStatus, Platform, LogBox, Linking, InteractionManager, Appearance } from 'react-native'
import SoundsService from './services/SoundsService'
import MainStoreService from './services/MainStoreService'
import AppService from './services/AppService'
import TimeServiceHelper from './components/TimeOff/TimeServiceHelper'
import CallKitExtensionService from './services/CallKitExtensionService'
import AppTabs from './screens/AppTabs'
import ShoutoutHelper from './components/Shoutouts/ShoutoutHelper'
import {
	Navigation,
	type LayoutTabsChildren,
	type LayoutRoot,
	type AnimationOptions,
	type Options
} from 'react-native-navigation'
import { initPushNotifications } from './services/PushNotificationsService'
import { getValueOfParamInURL, IS_IOS } from './services/BobUtils'
import { translate, TranslationPaths } from '@b-localizationService'
import { BobColorsService, ColorRole } from '@b-colors'
import { GOTHAM_BOOK } from '@b-typography'
import * as Sentry from '@sentry/react-native'
import UserHelper from './models/UserHelper'
import EnvironmentService from './services/EnvironmentService'
import ErrorReportService from './services/ErrorReportService'
import MAMConfigurationService from './services/MAMConfigurationService'
import { get } from '@b-lodash'
import { dismissHUD, showLoadingHUD, showResultHUD } from './components/common/Components/BobBasicComponents'
import selectIsStageEnvironment from './duckers/debugMode/selectors/selectIsStageEnvironment'
import type { ErrorEvent, EventHint } from '@sentry/browser'
import './unistyles'
import { UnistylesRuntime, type UnistylesTheme } from 'react-native-unistyles'
import {
	initPropsWhichNeedsRootComponent,
	getNavigationBarOptions,
	topBarTitleStyle,
	topBarSubtitleStyle,
	backButtonPayload
} from './services/AppearanceService'
import EnvironmentConsts from './shared/constants/EnvironmentConsts'
import DeviceConstant from './shared/constants/DeviceConstant'

if (__DEV__) {
	LogBox.ignoreAllLogs(true)
}

const navigationAnimationOptions: AnimationOptions =
	Platform.OS === 'ios'
		? {}
		: {
				push: {
					content: {
						translationX: {
							from: screenWidth(),
							to: 0,
							duration: 300
						}
					}
				},
				pop: {
					content: {
						translationX: {
							from: 0,
							to: screenWidth(),
							duration: 300
						}
					}
				}
			}

function buildNavigationDefaultOptions(theme: UnistylesTheme): Options {
	return {
		layout: { orientation: ['portrait'] },
		topBar: {
			title: topBarTitleStyle(undefined, theme),
			subtitle: topBarSubtitleStyle(undefined, theme),
			backButton: backButtonPayload(theme)
		},
		animations: navigationAnimationOptions,
		statusBar: {
			drawBehind: true,
			backgroundColor: 'transparent',
			style: BobColorsService.isDarkMode() ? 'light' : 'dark'
		},
		navigationBar: getNavigationBarOptions()
	}
}

let warnedDefaultOptionsError = false
function setNavigationDefaultOptions() {
	try {
		if (!Navigation || typeof Navigation.setDefaultOptions !== 'function') {
			if (__DEV__ && !warnedDefaultOptionsError) {
				warnedDefaultOptionsError = true
				console.warn('Navigation.setDefaultOptions is not available yet.')
			}
			return
		}
		const theme = getSafeTheme()
		Navigation.setDefaultOptions(buildNavigationDefaultOptions(theme))
	} catch (error) {
		if (__DEV__ && !warnedDefaultOptionsError) {
			warnedDefaultOptionsError = true
			console.warn('Failed to set default navigation options:', error)
		}
	}
}

// Public function to reapply navigation defaults when theme changes
export function reapplyNavigationDefaults() {
	setNavigationDefaultOptions()
}

export const FIRST_APP_LAUNCH_KEY: string = 'FIRST_APP_LAUNCH_KEY'

type AppRootViewType = NOT_LOGGED_IN_STATE_CASE | LOGGED_IN_STATE_CASE | UNDETERMINED_LOGIN_STATE_CASE
const middlewares = [promiseMiddleware]

export const kStore: Store<BobState> = MainStoreService.initialize(
	reducers,
	applyMiddleware(...middlewares, ReduxThunk)
)
registerScreens(kStore)

let kAppState: AppStateStatus
export default class App {
	currentRoot: AppRootViewType
	isBridgeInitialled = false
	firstTime = true
	companyId: number | null | undefined = null // needed for Sentry, see "beforeSend" below
	theme: UnistylesTheme | undefined
	private static savedBottomTabs: LayoutTabsChildren[] = []
	private static activeComponentIds: Set<string> = new Set<string>()

	private static getScreenSpecificOptions(
		screenId: string | undefined,
		companyName: string | undefined,
		theme: UnistylesTheme
	): Options {
		const baseOptions: Options = {
			layout: {
				backgroundColor: theme.colors[ColorRole.backgroundGeneral]
			},
			statusBar: {
				backgroundColor: theme.colors[ColorRole.backgroundGeneral]
			},
			navigationBar: getNavigationBarOptions(),
			topBar: {
				background: {
					color: theme.colors[ColorRole.backgroundGeneral]
				},
				borderColor: theme.colors[ColorRole.border300],
				title: {
					color: theme.colors[ColorRole.text800]
				},
				backButton: {
					color: theme.colors[ColorRole.text700]
				}
			}
		}

		switch (screenId) {
			case HOME_SCREEN_ID:
				return {
					...baseOptions,
					topBar: {
						...baseOptions.topBar,
						leftButtons: [
							{
								id: SCREEN_TITLE_COMPONENT_ID,
								component: {
									id: SCREEN_TITLE_COMPONENT_ID + HOME_SCREEN_ID,
									name: SCREEN_TITLE_COMPONENT_ID,
									passProps: {
										text: companyName
									}
								}
							}
						]
					}
				}
			case PEOPLE_LIST_SCREEN_ID:
				return {
					...baseOptions,
					layout: {
						...baseOptions.layout,
						orientation: IS_IOS ? ['portrait', 'landscape'] : undefined
					},
					topBar: {
						...baseOptions.topBar,
						leftButtons: [
							{
								id: SCREEN_TITLE_COMPONENT_ID,
								component: {
									id: SCREEN_TITLE_COMPONENT_ID + PEOPLE_LIST_SCREEN_ID,
									name: SCREEN_TITLE_COMPONENT_ID,
									passProps: {
										text: translate(TranslationPaths.peopleTabTitle)
									}
								}
							}
						],
						noBorder: true,
						scrollEdgeAppearance: {
							active: true,
							noBorder: true
						},
						borderColor: theme.colors[ColorRole.backgroundGeneral],
						elevation: -1
					}
				}
			case APP_MENU_TAB_SCREEN:
				return {
					layout: {
						backgroundColor: theme.colors[ColorRole.grey100BackgroundOfWhite]
					},
					statusBar: {
						backgroundColor: theme.colors[ColorRole.grey100BackgroundOfWhite]
					},
					navigationBar: getNavigationBarOptions(),
					topBar: {
						background: {
							color: theme.colors[ColorRole.grey100BackgroundOfWhite]
						},
						leftButtons: [
							{
								id: SCREEN_TITLE_COMPONENT_ID,
								component: {
									id: SCREEN_TITLE_COMPONENT_ID + APP_MENU_TAB_SCREEN,
									name: SCREEN_TITLE_COMPONENT_ID,
									passProps: {
										text: translate(TranslationPaths.appMenuScreen_title)
									}
								}
							}
						],
						noBorder: true,
						scrollEdgeAppearance: {
							active: true,
							noBorder: true
						},
						borderColor: theme.colors[ColorRole.divider200],
						elevation: -1
					}
				}
			default:
				return baseOptions
		}
	}

	private static getBottomTabOptions(screenId: string | undefined, theme: UnistylesTheme) {
		const baseOptions = {
			fontFamily: GOTHAM_BOOK,
			fontSize: 12,
			selectedFontSize: 12,
			textColor: theme.colors[ColorRole.text800],
			selectedTextColor: theme.colors[ColorRole.text800],
			selectedIconColor: null,
			iconColor: theme.colors[ColorRole.text800]
		}

		switch (screenId) {
			case HOME_SCREEN_ID:
				return {
					...baseOptions,
					icon: require('../images/homeTabIcon.png'),
					selectedIcon: BobColorsService.isDarkMode()
						? require('../images/selectedHomeTabIconDark.png')
						: require('../images/selectedHomeTabIcon.png'),
					text: translate(TranslationPaths.homeTabTitle)
				}
			case PEOPLE_LIST_SCREEN_ID:
				return {
					...baseOptions,
					icon: require('../images/peopleTabIcon.png'),
					selectedIcon: BobColorsService.isDarkMode()
						? require('../images/selectedPeopleTabIconDark.png')
						: require('../images/selectedPeopleTabIcon.png'),
					text: translate(TranslationPaths.peopleTabTitle)
				}
			case APP_MENU_TAB_SCREEN:
				return {
					...baseOptions,
					icon: require('../images/appMenuTabIcon.png'),
					selectedIcon: BobColorsService.isDarkMode()
						? require('../images/selectedAppMenuTabIconDark.png')
						: require('../images/selectedAppMenuTabIcon.png'),
					text: translate(TranslationPaths.appMenuTabTitle)
				}
			default:
				return baseOptions
		}
	}

	private static bottomTabs(
		_userDisplayName: string,
		companyName: string | undefined,
		shouldShowHomeTab: boolean,
		shouldDisplayPeopleTab: boolean,
		passProps: object
	): LayoutTabsChildren[] {
		const children: LayoutTabsChildren[] = []
		let currentTabIndex = -1
		const theme = UnistylesRuntime.getTheme() as UnistylesTheme

		if (shouldShowHomeTab) {
			currentTabIndex++
			children.push({
				stack: {
					children: [
						{
							component: {
								id: HOME_SCREEN_ID,
								name: HOME_SCREEN_ID,
								options: App.getScreenSpecificOptions(HOME_SCREEN_ID, companyName, theme),
								passProps: passProps
							}
						}
					],
					options: {
						bottomTab: App.getBottomTabOptions(HOME_SCREEN_ID, theme)
					}
				}
			})
		}

		if (shouldDisplayPeopleTab) {
			currentTabIndex++
			children.push({
				stack: {
					children: [
						{
							component: {
								id: PEOPLE_LIST_SCREEN_ID,
								name: PEOPLE_LIST_SCREEN_ID,
								options: App.getScreenSpecificOptions(PEOPLE_LIST_SCREEN_ID, companyName, theme),
								passProps: passProps
							}
						}
					],
					options: {
						bottomTab: App.getBottomTabOptions(PEOPLE_LIST_SCREEN_ID, theme)
					}
				}
			})
		}

		currentTabIndex++
		AppTabs.TAB_BAR_APP_MENU_INDEX = currentTabIndex

		children.push({
			stack: {
				children: [
					{
						component: {
							id: APP_MENU_TAB_SCREEN,
							name: APP_MENU_TAB_SCREEN,
							options: App.getScreenSpecificOptions(APP_MENU_TAB_SCREEN, companyName, theme),
							passProps: passProps
						}
					}
				],
				options: {
					bottomTab: App.getBottomTabOptions(APP_MENU_TAB_SCREEN, theme)
				}
			}
		})

		return children
	}

	constructor() {
		App.activeComponentIds = new Set<string>()

		Navigation.events().registerComponentDidAppearListener(({ componentId }) => {
			App.activeComponentIds.add(componentId)
		})

		BobColorsService.initTheme()

		// Set up theme change listener to handle navigation updates
		Appearance.addChangeListener(({ colorScheme }) => {
			try {
				BobColorsService.setUserTheme(colorScheme)
				// Clear theme cache to ensure fresh theme data on next access
				clearThemeCache()
				// Reapply navigation defaults first to ensure consistency
				reapplyNavigationDefaults()
				// Then refresh all screen-specific navigation elements
				App.refreshUserTopBarAndBottomBar()
			} catch (_error) {
				// Fallback: ensure navigation defaults are applied even if other updates fail
				try {
					clearThemeCache()
					reapplyNavigationDefaults()
				} catch (_fallbackError) {
					// Silent fail in extreme cases to prevent crash
				}
			}
		})

		EnvironmentService.initialize()
		this.initSentry()

		// CRITICAL FIX for HIBOB-MOBILE-51J: Apply MAM configuration early in app lifecycle
		// This ensures enrollment cancellation tracking is configured before any MSAL operations
		MAMConfigurationService.applyConfigurationToNativeModules()

		Navigation.events().registerAppLaunchedListener(() => {
			this.isBridgeInitialled = true
			// Apply defaults first to avoid setRoot using old defaults
			setNavigationDefaultOptions()
			initPropsWhichNeedsRootComponent()
			const forceResetRoot = true
			this.onStoreUpdate(forceResetRoot)
		})
		AppService.initialize()

		const store = MainStoreService.store()
		// since react-redux only works on components, we need to subscribe this class manually
		store.subscribe(this.onStoreUpdate.bind(this))
		store.dispatch(initializeApp(this.onAppInitializationCompletion))
		SoundsService.initialize()
		ShoutoutHelper.initialize(store)
		TimeServiceHelper.initialize(store)
		CallKitExtensionService.initialize().catch((error) => {
			ErrorReportService.logNonFatalErrorWithContext(error, 'Failed to initialize CallKitExtensionService')
		})

		kAppState = AppState.currentState

		if (Platform.OS === 'android') {
			// android bug – getting false changes on start
			setTimeout(() => {
				kAppState = AppState.currentState
				AppState.addEventListener('change', this.onAppStateChange)
			}, 5000)
		} else {
			AppState.addEventListener('change', this.onAppStateChange)
		}
	}

	private onDeepLinkingLaunch = (urlOrEvent?: string | { url: string }) => {
		const url = typeof urlOrEvent === 'string' ? urlOrEvent : get(urlOrEvent, 'url')
		const isOpenApp = typeof urlOrEvent === 'string'
		this.handleDeepLinking(url, isOpenApp)
	}

	private showLoadingHUD = (url: string, shouldWaitNavigation: boolean = false) => {
		const companyName = getValueOfParamInURL(url, 'companyName')

		if (shouldWaitNavigation) {
			const screenEventListener = Navigation.events().registerComponentDidAppearListener(({ componentName }) => {
				if (componentName === LOGIN_SCREEN_ID) {
					setTimeout(() => {
						showLoadingHUD(
							translate(TranslationPaths.try_out_environment_hub_loading_message, {
								companyName: companyName
							})
						)
					}, 0)
					screenEventListener.remove()
				}
			})
		} else {
			InteractionManager.runAfterInteractions(() => {
				showLoadingHUD(
					translate(TranslationPaths.try_out_environment_hub_loading_message, {
						companyName: companyName
					})
				)
			})
		}
	}

	private setEnvironment = (url: string) => {
		const domain = getValueOfParamInURL(url, 'domain')
		if (!domain) {
			return
		}

		EnvironmentService.bobDomain = domain
		setTimeout(() => {
			dismissHUD()
			showResultHUD(true, translate(TranslationPaths.try_out_environment_hub_success_message))
		}, 3000)
	}

	private handleDeepLinking = (url: string | undefined | null, isOpenApp: boolean) => {
		if (!url) {
			return
		}

		if (url.startsWith(TRY_DEMO_PREFIX)) {
			const domain = getValueOfParamInURL(url, 'domain')
			if (!domain) {
				showResultHUD(false, translate(TranslationPaths.try_out_environment_hub_error_message))
				return
			}

			const storeState = kStore.getState() as BobState
			const root: AppRootViewType = storeState.app.root

			if (LOGGED_IN_STATE === root) {
				const dispatch = kStore.dispatch as BobDispatchReturn
				this.showLoadingHUD(url, true)
				dispatch(
					logOut(() => {
						this.setEnvironment(url)
					})
				)
			} else {
				this.showLoadingHUD(url, isOpenApp)
				this.setEnvironment(url)
			}
		}
	}

	private setupDeepLinkingHandling = () => {
		Linking.getInitialURL()
			.then((url) => {
				if (url) {
					this.onDeepLinkingLaunch(url)
				}
			})
			.catch((error) => {
				ErrorReportService.logNonFatalError(error)
			})

		Linking.addEventListener('url', this.onDeepLinkingLaunch)
	}

	private initSentry() {
		Sentry.init({
			dsn: 'https://a7fb0a5844e4a0509273455aaf5c30d6@o4507759802712064.ingest.de.sentry.io/4507759853568080',
			tracesSampleRate: 0.1,
			profilesSampleRate: 0.1,
			enableAppStartTracking: true,
			enableNativeFramesTracking: true,
			enableStallTracking: true,
			enableUserInteractionTracing: true,
			enableAutoSessionTracking: true,
			autoInitializeNativeSdk: !EnvironmentConsts.isLocal || DeviceConstant.isIOS,
			enableLogs: true,
			integrations: [Sentry.reactNativeNavigationIntegration({ navigation: Navigation })],
			beforeSend: (event: ErrorEvent, _hint: EventHint) => {
				if (__DEV__) {
					return null
				}

				let retVal: ErrorEvent | null = null

				const shouldAlwaysSend = this.companyId === HIBOB_COMPANY_ID

				if (shouldAlwaysSend) {
					retVal = event
				} else {
					retVal = Math.random() > 0.9 ? event : null
				}

				return retVal
			}
		})
	}

	private onStoreUpdate(forceResetRoot?: boolean) {
		if (!this.isBridgeInitialled) {
			return
		}

		const storeState = kStore.getState() as BobState
		const root: AppRootViewType = storeState.app.root

		if (this.firstTime && root !== UNDETERMINED_LOGIN_STATE) {
			this.setupDeepLinkingHandling()
			this.firstTime = false
		}

		if (this.currentRoot !== root || forceResetRoot) {
			this.currentRoot = root
			this.startApp(root)
		}
	}

	private startApp(root: AppRootViewType) {
		switch (root) {
			case UNDETERMINED_LOGIN_STATE:
				this.handleUndeterminedLoginState()
				break
			case LOGGED_IN_STATE:
				this.handleLoggedInState()
				break
			case NOT_LOGGED_IN_STATE:
				this.handleNotLoggedInState()
				break
			default:
				console.error('Unknown app root')
		}
	}

	private handleUndeterminedLoginState() {
		const loadingRoot: LayoutRoot = {
			root: {
				component: {
					name: APP_LAUNCHER_LOADER
				}
			}
		}

		if (IS_IOS) {
			Navigation.setDefaultOptions({
				animations: {
					setRoot: {
						waitForRender: true
					}
				}
			})
		}

		Navigation.setRoot(loadingRoot)
	}

	private handleNotLoggedInState() {
		this.companyId = null
		const loginRoot: LayoutRoot = {
			root: {
				component: {
					name: LOGIN_SCREEN_ID,
					options: {
						topBar: {
							visible: false
						}
					},
					passProps: {
						onInitialLoginCompletion: this.onInitialLoginCompletion
					}
				}
			}
		}

		Navigation.setRoot(loginRoot)
	}

	private handleLoggedInState() {
		initPushNotifications(AppService.onReceivedPushNotification)
		const passProps = {
			addMethodToCallWhenRefreshDataNeeded: AppService.addMethodToCallWhenRefreshDataNeeded,
			removeMethodToCallWhenRefreshDataNeeded: AppService.removeMethodToCallWhenRefreshDataNeeded,
			addMethodToCallWhenAppReturnedFromBackground: AppService.addMethodToCallWhenAppReturnedFromBackground,
			removeMethodToCallWhenAppReturnedFromBackground: AppService.removeMethodToCallWhenAppReturnedFromBackground,
			addMethodToCallOnPushNotificationReceived: AppService.addMethodToCallWhenReceivedPushNotification,
			removeMethodToCallOnPushNotificationReceived: AppService.removeMethodToCallWhenReceivedPushNotification
		}

		const storeState = kStore.getState() as BobState
		const permissions = storeState.permissions.permissions
		const userDisplayName = UserHelper.getDisplayName(storeState.auth.user)
		this.companyId = storeState.auth.user?.companyId
		this.theme = UnistylesRuntime.getTheme() as UnistylesTheme
		const companyName = storeState.auth.user?.companyName
		const companyFeatures = storeState.app.companyFeatures
		const shouldShowHomeTab = Boolean(
			companyFeatures?.has(HOMEPAGE_WHOSOUT) ||
				companyFeatures?.has(HOMEPAGE_NEWJOINERS) ||
				companyFeatures?.has(HOMEPAGE_BIRTHDAYS) ||
				companyFeatures?.has(HOMEPAGE_ANNIVERSARIES) ||
				companyFeatures?.has(HOMEPAGE_SHOUTOUTS)
		)
		const shouldDisplayPeopleTab = Boolean(
			(permissions?.shouldDisplayPeopleDirectory && companyFeatures?.has(DIRECTORY_FEATURE)) ||
				(permissions?.shouldDisplayOrgChart && companyFeatures?.has(ORG_CHART_FEATURE))
		)
		App.savedBottomTabs = App.bottomTabs(
			userDisplayName,
			companyName,
			shouldShowHomeTab,
			shouldDisplayPeopleTab,
			passProps
		)

		const state = kStore.getState()
		const isStageEnvironment = selectIsStageEnvironment(state)
		const mainRoot: LayoutRoot = {
			root: {
				bottomTabs: {
					id: 'BOTTOM_TABS_COMPONENT_ID',
					children: App.savedBottomTabs,
					options: {
						bottomTabs: {
							backgroundColor: isStageEnvironment
								? (UnistylesRuntime.getTheme() as UnistylesTheme).colors[ColorRole.infoBackground]
								: (UnistylesRuntime.getTheme() as UnistylesTheme).colors[ColorRole.backgroundGeneral]
						}
					}
				}
			}
		}

		Navigation.setRoot(mainRoot)
	}

	private onAppStateChange = (nextAppState: AppStateStatus) => {
		if (kAppState) {
			if (kAppState === 'background' && nextAppState === 'active') {
				kStore.dispatch(renewUserSessionIfNeeded(AppService.onRenewSessionCompletion))
				AppService.refreshInstalledEmailApps()

				// Check for pending MSAL authentication that may have been interrupted
				if (Platform.OS === 'android') {
					this.checkPendingMSALAuth()
				}

				CallKitExtensionService.handleAppForeground(kStore.dispatch).catch((error) => {
					ErrorReportService.logNonFatalErrorWithContext(error, 'Failed to handle CallKit foreground state')
				})
			} else if (kAppState === 'active' && nextAppState === 'inactive') {
				kStore.dispatch(performNeededActionsOnAppBackgrounding())
				CallKitExtensionService.storeStatusOnBackground().catch((error) => {
					ErrorReportService.logNonFatalErrorWithContext(error, 'Failed to store CallKit background state')
				})
			}
		}

		kAppState = nextAppState
	}

	private async checkPendingMSALAuth() {
		try {
			const { NativeModules } = require('react-native')
			if (NativeModules.MSALNativeModule?.checkPendingAuth) {
				const recovered = await NativeModules.MSALNativeModule.checkPendingAuth()
				if (recovered) {
					ErrorReportService.logWithLevel('Successfully recovered pending MSAL authentication', 'info')
				}
			}
		} catch (error) {
			// Log the error for diagnostic purposes, but do not interrupt flow
			ErrorReportService.logNonFatalErrorWithContext(error, 'Failed to recover pending MSAL authentication')
		}
	}

	// tslint:disable-next-line:no-empty
	private onAppInitializationCompletion(_result: RenewSessionCompletionData | null | undefined) {
		const tabScreenIds = App.savedBottomTabs.map((tab) => tab.stack?.children?.[0]?.component?.id)
		const iconColor = (UnistylesRuntime.getTheme() as UnistylesTheme).colors[ColorRole.bottomTabIconColor]
		tabScreenIds.forEach((screenId) => {
			Navigation.mergeOptions(screenId as string, {
				bottomTab: {
					text: (() => {
						switch (screenId) {
							case HOME_SCREEN_ID:
								return translate(TranslationPaths.homeTabTitle)
							case PEOPLE_LIST_SCREEN_ID:
								return translate(TranslationPaths.peopleTabTitle)
							case APP_MENU_TAB_SCREEN:
								return translate(TranslationPaths.appMenuTabTitle)
							default:
								return ''
						}
					})(),
					iconColor: iconColor
				}
			})
		})
	}

	// tslint:disable-next-line:no-empty
	private onInitialLoginCompletion(_result: boolean, _error: Error) {}

	public static refreshUserTopBarAndBottomBar() {
		const tabScreenIds = App.savedBottomTabs.map((tab) => tab.stack?.children?.[0]?.component?.id)
		setNavigationDefaultOptions()
		const theme = UnistylesRuntime.getTheme() as UnistylesTheme
		const storeState = kStore.getState() as BobState
		const companyName = storeState.auth.user?.companyName ?? ''

		tabScreenIds.forEach((screenId) => {
			const screenOptions = App.getScreenSpecificOptions(screenId, companyName, theme)
			const bottomTabOptions = App.getBottomTabOptions(screenId, theme)

			Navigation.mergeOptions(screenId as string, {
				...screenOptions,
				bottomTab: bottomTabOptions
			})
		})

		App.activeComponentIds.forEach((componentId) => {
			const screenOptions = App.getScreenSpecificOptions(componentId, companyName, theme)
			Navigation.mergeOptions(componentId, {
				...screenOptions
			})
		})

		// Update the bottomTabs background color as well
		const isStageEnvironment = selectIsStageEnvironment(storeState)
		Navigation.mergeOptions('BOTTOM_TABS_COMPONENT_ID', {
			bottomTabs: {
				backgroundColor: isStageEnvironment
					? theme.colors[ColorRole.infoBackground]
					: theme.colors[ColorRole.backgroundGeneral]
			}
		})
	}

	public static initializeStorybook() {
		Navigation.events().registerAppLaunchedListener(() => {
			Navigation.setRoot({
				root: {
					stack: {
						id: 'storybook-root',
						children: [
							{
								component: {
									name: STORYBOOK_SCREEN_ID,
									options: {
										topBar: {
											visible: false
										}
									}
								}
							}
						]
					}
				}
			})
		})
	}
}

What was the expected behaviour?

To launch normally without a crash

Was it tested on latest react-native-navigation?

  • I have tested this issue on the latest react-native-navigation release and it still reproduces.

Help us reproduce this issue!

No response

In what environment did this happen?

React Native Navigation version: 8.4.2
React Native version: 0.77.3
Has Fabric (React Native's new rendering system) enabled: yes
Node version: 22.11.0
Device model: OnePlus 8T
Android version: 14

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions