template_0205
This commit is contained in:
103
RN_TEMPLATE/app/navigators/AppNavigator.tsx
Normal file
103
RN_TEMPLATE/app/navigators/AppNavigator.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* The app navigator (formerly "AppNavigator" and "MainNavigator") is used for the primary
|
||||
* navigation flows of your app.
|
||||
* Generally speaking, it will contain an auth flow (registration, login, forgot password)
|
||||
* and a "main" flow which the user will use once logged in.
|
||||
*/
|
||||
import { NavigationContainer } from "@react-navigation/native"
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack"
|
||||
|
||||
import Config from "@/config"
|
||||
import { useAuth } from "@/context/AuthContext"
|
||||
import { AboutScreen } from "@/screens/AboutScreen"
|
||||
import { AuthWelcomeScreen } from "@/screens/AuthWelcomeScreen"
|
||||
import { ChangeEmailScreen } from "@/screens/ChangeEmailScreen"
|
||||
import { ChangePasswordScreen } from "@/screens/ChangePasswordScreen"
|
||||
import { ErrorBoundary } from "@/screens/ErrorScreen/ErrorBoundary"
|
||||
import { ForgotPasswordScreen } from "@/screens/ForgotPasswordScreen"
|
||||
import { LanguageScreen } from "@/screens/LanguageScreen"
|
||||
import { LoginScreen } from "@/screens/LoginScreen"
|
||||
import { ProfileScreen } from "@/screens/ProfileScreen"
|
||||
import { RegisterScreen } from "@/screens/RegisterScreen"
|
||||
import { SecurityScreen } from "@/screens/SecurityScreen"
|
||||
import { SessionManagementScreen } from "@/screens/SessionManagementScreen"
|
||||
import { SettingsScreen } from "@/screens/SettingsScreen"
|
||||
import { ThemeScreen } from "@/screens/ThemeScreen"
|
||||
import { WelcomeScreen } from "@/screens/WelcomeScreen"
|
||||
import { useAppTheme } from "@/theme/context"
|
||||
|
||||
import { MainNavigator } from "./MainNavigator"
|
||||
import type { AppStackParamList, NavigationProps } from "./navigationTypes"
|
||||
import { navigationRef, useBackButtonHandler } from "./navigationUtilities"
|
||||
|
||||
/**
|
||||
* This is a list of all the route names that will exit the app if the back button
|
||||
* is pressed while in that screen. Only affects Android.
|
||||
*/
|
||||
const exitRoutes = Config.exitRoutes
|
||||
|
||||
// Documentation: https://reactnavigation.org/docs/stack-navigator/
|
||||
const Stack = createNativeStackNavigator<AppStackParamList>()
|
||||
|
||||
const AppStack = () => {
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
const {
|
||||
theme: { colors },
|
||||
} = useAppTheme()
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
navigationBarColor: colors.background,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
}}
|
||||
initialRouteName={isAuthenticated ? "Welcome" : "AuthWelcome"}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Stack.Screen name="Welcome" component={WelcomeScreen} />
|
||||
|
||||
<Stack.Screen name="Main" component={MainNavigator} />
|
||||
|
||||
<Stack.Screen name="Profile" component={ProfileScreen} />
|
||||
<Stack.Screen name="Settings" component={SettingsScreen} />
|
||||
<Stack.Screen name="Security" component={SecurityScreen} />
|
||||
<Stack.Screen name="SessionManagement" component={SessionManagementScreen} />
|
||||
<Stack.Screen name="ChangePassword" component={ChangePasswordScreen} />
|
||||
<Stack.Screen name="ChangeEmail" component={ChangeEmailScreen} />
|
||||
<Stack.Screen name="About" component={AboutScreen} />
|
||||
<Stack.Screen name="Language" component={LanguageScreen} />
|
||||
<Stack.Screen name="Theme" component={ThemeScreen} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Stack.Screen name="AuthWelcome" component={AuthWelcomeScreen} />
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/** 🔥 Your screens go here */}
|
||||
{/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
export const AppNavigator = (props: NavigationProps) => {
|
||||
const { navigationTheme } = useAppTheme()
|
||||
|
||||
useBackButtonHandler((routeName) => exitRoutes.includes(routeName))
|
||||
|
||||
return (
|
||||
<NavigationContainer ref={navigationRef} theme={navigationTheme} {...props}>
|
||||
<ErrorBoundary catchErrors={Config.catchErrors}>
|
||||
<AppStack />
|
||||
</ErrorBoundary>
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
350
RN_TEMPLATE/app/navigators/MainNavigator.tsx
Normal file
350
RN_TEMPLATE/app/navigators/MainNavigator.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Platform,
|
||||
Pressable,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from "react-native"
|
||||
import { BottomTabBarProps, createBottomTabNavigator } from "@react-navigation/bottom-tabs"
|
||||
import { useNavigation } from "@react-navigation/native"
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
|
||||
import {
|
||||
PanGestureHandler,
|
||||
PanGestureHandlerStateChangeEvent,
|
||||
State,
|
||||
} from "react-native-gesture-handler"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
|
||||
import { Avatar } from "@/components/Avatar"
|
||||
import { Icon, IconTypes } from "@/components/Icon"
|
||||
import { Text } from "@/components/Text"
|
||||
import { useAuth } from "@/context/AuthContext"
|
||||
import { translate } from "@/i18n/translate"
|
||||
import { CommunityScreen } from "@/screens/CommunityScreen"
|
||||
import { ShowroomScreen } from "@/screens/ShowroomScreen/ShowroomScreen"
|
||||
import { useAppTheme } from "@/theme/context"
|
||||
import type { ThemedStyle } from "@/theme/types"
|
||||
import { s, fs } from "@/utils/responsive"
|
||||
|
||||
import type { AppStackParamList, MainTabParamList } from "./navigationTypes"
|
||||
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>()
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get("window")
|
||||
|
||||
// Tab configuration
|
||||
const TAB_CONFIG: { name: keyof MainTabParamList; icon: IconTypes }[] = [
|
||||
{ name: "Showroom", icon: "home" },
|
||||
{ name: "Community", icon: "barChart" },
|
||||
]
|
||||
|
||||
/**
|
||||
* Header left component with profile avatar button
|
||||
*/
|
||||
function HeaderProfileButton() {
|
||||
const { user } = useAuth()
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>()
|
||||
|
||||
const handlePress = () => {
|
||||
navigation.navigate("Profile")
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
|
||||
<Avatar
|
||||
uri={user?.profile?.avatar}
|
||||
fallback={user?.profile?.nickname || user?.username || "U"}
|
||||
size={s(36)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom floating capsule tab bar with glass effect and gesture support
|
||||
*/
|
||||
function FloatingTabBar({ state, navigation }: BottomTabBarProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const { themed, theme } = useAppTheme()
|
||||
|
||||
const tabIndex = state.index
|
||||
const tabCount = TAB_CONFIG.length
|
||||
const containerWidth = SCREEN_WIDTH - theme.spacing.xl * 2
|
||||
const buttonWidth = (containerWidth - theme.spacing.xs * 2) / tabCount
|
||||
|
||||
// 动画值
|
||||
const basePos = useRef(new Animated.Value(tabIndex)).current
|
||||
const gestureX = useRef(new Animated.Value(0)).current
|
||||
|
||||
// 视觉上的活跃索引,用于控制图标颜色
|
||||
const [visualIndex, setVisualIndex] = useState(tabIndex)
|
||||
|
||||
// 同步系统索引到视觉索引
|
||||
useEffect(() => {
|
||||
setVisualIndex(tabIndex)
|
||||
Animated.spring(basePos, {
|
||||
toValue: tabIndex,
|
||||
useNativeDriver: true,
|
||||
tension: 100,
|
||||
friction: 12,
|
||||
}).start()
|
||||
}, [tabIndex, basePos])
|
||||
|
||||
// 监听手势位移,实时更新图标视觉状态
|
||||
useEffect(() => {
|
||||
const listenerId = gestureX.addListener(({ value }) => {
|
||||
// 计算当前指示器中心位置对应的索引
|
||||
const currentIndicatorPos = tabIndex * buttonWidth + value
|
||||
const newVisualIndex = Math.max(
|
||||
0,
|
||||
Math.min(TAB_CONFIG.length - 1, Math.round(currentIndicatorPos / buttonWidth)),
|
||||
)
|
||||
|
||||
if (newVisualIndex !== visualIndex) {
|
||||
setVisualIndex(newVisualIndex)
|
||||
}
|
||||
})
|
||||
return () => gestureX.removeListener(listenerId)
|
||||
}, [tabIndex, visualIndex, buttonWidth, gestureX])
|
||||
|
||||
// 手势结束处理
|
||||
const onHandlerStateChange = (event: PanGestureHandlerStateChangeEvent) => {
|
||||
if (event.nativeEvent.state === State.END) {
|
||||
const { translationX, velocityX } = event.nativeEvent
|
||||
const movedTabs = Math.round(translationX / buttonWidth)
|
||||
|
||||
// 快速滑动检测
|
||||
let finalMovedTabs = movedTabs
|
||||
if (Math.abs(velocityX) > 500 && Math.abs(translationX) > 20) {
|
||||
finalMovedTabs = velocityX > 0 ? -1 : 1 // 注意方向:向右滑是上一个,向左滑是下一个
|
||||
}
|
||||
|
||||
// 计算新索引
|
||||
let newIndex = tabIndex + finalMovedTabs
|
||||
newIndex = Math.max(0, Math.min(TAB_CONFIG.length - 1, newIndex))
|
||||
|
||||
if (newIndex !== tabIndex) {
|
||||
gestureX.flattenOffset()
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate(TAB_CONFIG[newIndex].name)
|
||||
})
|
||||
}
|
||||
|
||||
// 弹簧动画复位
|
||||
Animated.spring(gestureX, {
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
tension: 100,
|
||||
friction: 15,
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
// 指示器位置 = 基准位置 + 手势偏移
|
||||
const indicatorTranslateX = Animated.add(
|
||||
basePos.interpolate({
|
||||
inputRange: TAB_CONFIG.map((_, i) => i),
|
||||
outputRange: TAB_CONFIG.map((_, i) => theme.spacing.xs + i * buttonWidth),
|
||||
}),
|
||||
gestureX,
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={$tabBarOverlay}>
|
||||
<View style={[themed($tabBarContainer), { bottom: insets.bottom + theme.spacing.sm }]}>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={Animated.event([{ nativeEvent: { translationX: gestureX } }], {
|
||||
useNativeDriver: true,
|
||||
})}
|
||||
onHandlerStateChange={onHandlerStateChange}
|
||||
activeOffsetX={[-15, 15]}
|
||||
>
|
||||
<Animated.View style={themed($tabBarGlass)}>
|
||||
{/* 滑动指示器 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
themed($tabIndicator),
|
||||
{
|
||||
width: buttonWidth,
|
||||
transform: [{ translateX: indicatorTranslateX }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Tab 按钮 */}
|
||||
{TAB_CONFIG.map((tab, index) => {
|
||||
// 图标颜色取决于 visualIndex (手势实时计算) 而不是 state.index
|
||||
const isActive = visualIndex === index
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={tab.name}
|
||||
style={$tabButton}
|
||||
onPress={() => {
|
||||
if (tabIndex !== index) {
|
||||
navigation.navigate(tab.name)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={tab.icon}
|
||||
size={s(24)}
|
||||
color={isActive ? theme.colors.tint : theme.colors.textDim}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom header component with absolutely centered title
|
||||
*/
|
||||
function CustomHeader({ title }: { title: string }) {
|
||||
const { themed } = useAppTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[themed($headerContainer), { paddingTop: insets.top, height: s(56) + insets.top }]}
|
||||
>
|
||||
{/* Absolutely centered title */}
|
||||
<View style={[$headerTitleWrapper, { top: insets.top, height: s(56) }]}>
|
||||
<Text style={themed($headerTitle)}>{title}</Text>
|
||||
</View>
|
||||
{/* Left side - avatar */}
|
||||
<View style={$headerLeft}>
|
||||
<HeaderProfileButton />
|
||||
</View>
|
||||
{/* Right side - future icons go here */}
|
||||
<View style={$headerRight} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main navigator with a floating capsule tab bar.
|
||||
*/
|
||||
export function MainNavigator() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
header: ({ options }) => <CustomHeader title={options.title || ""} />,
|
||||
tabBarHideOnKeyboard: true,
|
||||
tabBarStyle: { display: "none" },
|
||||
}}
|
||||
tabBar={(props) => <FloatingTabBar {...props} />}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Showroom"
|
||||
component={ShowroomScreen}
|
||||
options={{
|
||||
title: translate("navigator:componentsTab"),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Community"
|
||||
component={CommunityScreen}
|
||||
options={{
|
||||
title: translate("navigator:communityTab"),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
// Styles
|
||||
const $headerContainer: ThemedStyle<ViewStyle> = ({ colors, spacing }) => ({
|
||||
backgroundColor: colors.background,
|
||||
height: s(56),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: spacing.lg,
|
||||
})
|
||||
|
||||
const $headerTitleWrapper: ViewStyle = {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
pointerEvents: "none",
|
||||
}
|
||||
|
||||
const $headerTitle: ThemedStyle<TextStyle> = ({ colors, typography }) => ({
|
||||
color: colors.text,
|
||||
fontFamily: typography.primary.medium,
|
||||
fontSize: fs(17),
|
||||
})
|
||||
|
||||
const $headerLeft: ViewStyle = {
|
||||
zIndex: 1,
|
||||
}
|
||||
|
||||
const $headerRight: ViewStyle = {
|
||||
marginLeft: "auto",
|
||||
zIndex: 1,
|
||||
}
|
||||
|
||||
const $tabBarOverlay: ViewStyle = {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: "center",
|
||||
}
|
||||
|
||||
const $tabBarContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
width: SCREEN_WIDTH - spacing.xl * 2,
|
||||
alignItems: "center",
|
||||
})
|
||||
|
||||
const $tabBarGlass: ThemedStyle<ViewStyle> = ({ colors, spacing, isDark }) => ({
|
||||
flexDirection: "row",
|
||||
// 半透明背景模拟毛玻璃效果
|
||||
backgroundColor: isDark ? "rgba(30, 30, 30, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||
borderRadius: s(28),
|
||||
height: s(56),
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: spacing.xs,
|
||||
// iOS 阴影
|
||||
...(Platform.OS === "ios" && {
|
||||
shadowColor: colors.palette.neutral800,
|
||||
shadowOffset: { width: 0, height: s(4) },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: s(12),
|
||||
}),
|
||||
// Android 阴影
|
||||
elevation: 8,
|
||||
// 边框增加玻璃感
|
||||
borderWidth: 1,
|
||||
borderColor: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
|
||||
})
|
||||
|
||||
const $tabIndicator: ThemedStyle<ViewStyle> = ({ isDark }) => ({
|
||||
position: "absolute",
|
||||
height: s(48),
|
||||
// 选择器半透明
|
||||
backgroundColor: isDark ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.08)",
|
||||
borderRadius: s(24),
|
||||
top: s(4),
|
||||
left: 0,
|
||||
})
|
||||
|
||||
const $tabButton: ViewStyle = {
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1,
|
||||
}
|
||||
49
RN_TEMPLATE/app/navigators/navigationTypes.ts
Normal file
49
RN_TEMPLATE/app/navigators/navigationTypes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ComponentProps } from "react"
|
||||
import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"
|
||||
import {
|
||||
CompositeScreenProps,
|
||||
NavigationContainer,
|
||||
NavigatorScreenParams,
|
||||
} from "@react-navigation/native"
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack"
|
||||
|
||||
// Main Tab Navigator types
|
||||
export type MainTabParamList = {
|
||||
Showroom: { queryIndex?: string; itemIndex?: string }
|
||||
Community: undefined
|
||||
}
|
||||
|
||||
// App Stack Navigator types
|
||||
export type AppStackParamList = {
|
||||
Welcome: undefined
|
||||
AuthWelcome: undefined
|
||||
Login: undefined
|
||||
Register: undefined
|
||||
ForgotPassword: undefined
|
||||
Main: NavigatorScreenParams<MainTabParamList>
|
||||
Profile: undefined
|
||||
Settings: undefined
|
||||
Security: undefined
|
||||
ChangePassword: undefined
|
||||
ChangeEmail: undefined
|
||||
SessionManagement: undefined
|
||||
About: undefined
|
||||
Language: undefined
|
||||
Theme: undefined
|
||||
// 🔥 Your screens go here
|
||||
// IGNITE_GENERATOR_ANCHOR_APP_STACK_PARAM_LIST
|
||||
}
|
||||
|
||||
export type AppStackScreenProps<T extends keyof AppStackParamList> = NativeStackScreenProps<
|
||||
AppStackParamList,
|
||||
T
|
||||
>
|
||||
|
||||
export type MainTabScreenProps<T extends keyof MainTabParamList> = CompositeScreenProps<
|
||||
BottomTabScreenProps<MainTabParamList, T>,
|
||||
AppStackScreenProps<keyof AppStackParamList>
|
||||
>
|
||||
|
||||
export interface NavigationProps extends Partial<
|
||||
ComponentProps<typeof NavigationContainer<AppStackParamList>>
|
||||
> {}
|
||||
208
RN_TEMPLATE/app/navigators/navigationUtilities.ts
Normal file
208
RN_TEMPLATE/app/navigators/navigationUtilities.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user