209 lines
7.2 KiB
TypeScript
209 lines
7.2 KiB
TypeScript
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<AppStackParamList>()
|
|
|
|
/**
|
|
* Gets the current screen from any navigation state.
|
|
* @param {NavigationState | PartialState<NavigationState>} state - The navigation state to traverse.
|
|
* @returns {string} - The name of the current screen.
|
|
*/
|
|
export function getActiveRouteName(state: NavigationState | PartialState<NavigationState>): 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<AppStackParamList>)
|
|
}
|
|
|
|
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<NavigationProps["initialState"]>()
|
|
const isMounted = useIsMounted()
|
|
|
|
const initNavState = navigationRestoredDefaultState(Config.persistNavigation)
|
|
const [isRestored, setIsRestored] = useState(initNavState)
|
|
|
|
const routeNameRef = useRef<keyof AppStackParamList | undefined>(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<typeof navigationRef.resetRoot>[0]} state - The state to reset the root to.
|
|
* @returns {void}
|
|
*/
|
|
export function resetRoot(
|
|
state: Parameters<typeof navigationRef.resetRoot>[0] = { index: 0, routes: [] },
|
|
) {
|
|
if (navigationRef.isReady()) {
|
|
navigationRef.resetRoot(state)
|
|
}
|
|
}
|