import { useState, useEffect, useRef } from "react" import { BackHandler, Linking, Platform } from "react-native" import { NavigationState, PartialState, createNavigationContainerRef, } from "@react-navigation/native" import Config from "@/config" import type { PersistNavigationConfig } from "@/config/config.base" import * as storage from "@/utils/storage" import { useIsMounted } from "@/utils/useIsMounted" import type { AppStackParamList, NavigationProps } from "./navigationTypes" type Storage = typeof storage /** * Reference to the root App Navigator. * * If needed, you can use this to access the navigation object outside of a * `NavigationContainer` context. However, it's recommended to use the `useNavigation` hook whenever possible. * @see [Navigating Without Navigation Prop]{@link https://reactnavigation.org/docs/navigating-without-navigation-prop/} * * The types on this reference will only let you reference top level navigators. If you have * nested navigators, you'll need to use the `useNavigation` with the stack navigator's ParamList type. */ export const navigationRef = createNavigationContainerRef() /** * Gets the current screen from any navigation state. * @param {NavigationState | PartialState} state - The navigation state to traverse. * @returns {string} - The name of the current screen. */ export function getActiveRouteName(state: NavigationState | PartialState): string { const route = state.routes[state.index ?? 0] // Found the active route -- return the name if (!route.state) return route.name as keyof AppStackParamList // Recursive call to deal with nested routers return getActiveRouteName(route.state as NavigationState) } const iosExit = () => false /** * Hook that handles Android back button presses and forwards those on to * the navigation or allows exiting the app. * @see [BackHandler]{@link https://reactnative.dev/docs/backhandler} * @param {(routeName: string) => boolean} canExit - Function that returns whether we can exit the app. * @returns {void} */ export function useBackButtonHandler(canExit: (routeName: string) => boolean) { // The reason we're using a ref here is because we need to be able // to update the canExit function without re-setting up all the listeners const canExitRef = useRef(Platform.OS !== "android" ? iosExit : canExit) useEffect(() => { canExitRef.current = canExit }, [canExit]) useEffect(() => { // We'll fire this when the back button is pressed on Android. const onBackPress = () => { if (!navigationRef.isReady()) { return false } // grab the current route const routeName = getActiveRouteName(navigationRef.getRootState()) // are we allowed to exit? if (canExitRef.current(routeName)) { // exit and let the system know we've handled the event BackHandler.exitApp() return true } // we can't exit, so let's turn this into a back action if (navigationRef.canGoBack()) { navigationRef.goBack() return true } return false } // Subscribe when we come to life const subscription = BackHandler.addEventListener("hardwareBackPress", onBackPress) // Unsubscribe when we're done return () => subscription.remove() }, []) } /** * This helper function will determine whether we should enable navigation persistence * based on a config setting and the __DEV__ environment (dev or prod). * @param {PersistNavigationConfig} persistNavigation - The config setting for navigation persistence. * @returns {boolean} - Whether to restore navigation state by default. */ function navigationRestoredDefaultState(persistNavigation: PersistNavigationConfig) { if (persistNavigation === "always") return false if (persistNavigation === "dev" && __DEV__) return false if (persistNavigation === "prod" && !__DEV__) return false // all other cases, disable restoration by returning true return true } /** * Custom hook for persisting navigation state. * @param {Storage} storage - The storage utility to use. * @param {string} persistenceKey - The key to use for storing the navigation state. * @returns {object} - The navigation state and persistence functions. */ export function useNavigationPersistence(storage: Storage, persistenceKey: string) { const [initialNavigationState, setInitialNavigationState] = useState() const isMounted = useIsMounted() const initNavState = navigationRestoredDefaultState(Config.persistNavigation) const [isRestored, setIsRestored] = useState(initNavState) const routeNameRef = useRef(undefined) const onNavigationStateChange = (state: NavigationState | undefined) => { const previousRouteName = routeNameRef.current if (state !== undefined) { const currentRouteName = getActiveRouteName(state) if (previousRouteName !== currentRouteName) { // track screens. if (__DEV__) { console.log(currentRouteName) } } // Save the current route name for later comparison routeNameRef.current = currentRouteName as keyof AppStackParamList // Persist state to storage storage.save(persistenceKey, state) } } const restoreState = async () => { try { const initialUrl = await Linking.getInitialURL() // Only restore the state if app has not started from a deep link if (!initialUrl) { const state = (await storage.load(persistenceKey)) as NavigationProps["initialState"] | null if (state) setInitialNavigationState(state) } } finally { if (isMounted()) setIsRestored(true) } } useEffect(() => { if (!isRestored) restoreState() // runs once on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return { onNavigationStateChange, restoreState, isRestored, initialNavigationState } } /** * use this to navigate without the navigation * prop. If you have access to the navigation prop, do not use this. * @see {@link https://reactnavigation.org/docs/navigating-without-navigation-prop/} * @param {unknown} name - The name of the route to navigate to. * @param {unknown} params - The params to pass to the route. */ export function navigate(name: unknown, params?: unknown) { if (navigationRef.isReady()) { // @ts-expect-error navigationRef.navigate(name as never, params as never) } } /** * This function is used to go back in a navigation stack, if it's possible to go back. * If the navigation stack can't go back, nothing happens. * The navigationRef variable is a React ref that references a navigation object. * The navigationRef variable is set in the App component. */ export function goBack() { if (navigationRef.isReady() && navigationRef.canGoBack()) { navigationRef.goBack() } } /** * resetRoot will reset the root navigation state to the given params. * @param {Parameters[0]} state - The state to reset the root to. * @returns {void} */ export function resetRoot( state: Parameters[0] = { index: 0, routes: [] }, ) { if (navigationRef.isReady()) { navigationRef.resetRoot(state) } }