refactor(navigation): consolidate navigation with href helpers and extract push device hook

- Migrate all navigation calls from magic strings to centralized href helpers
- Extract inline device registration logic into useRegisterPushDevice hook
- Remove unused terms and privacy policy routes from profile stack
- Add hrefTradeDetail helper for trade detail navigation
- Restore routePayloadCache.stashSystemMessage for group request/invite handling
- Change desktop shell tab navigation from replace to push
This commit is contained in:
lafay
2026-06-12 23:42:09 +08:00
parent 1f7e25349f
commit 1e05d2bd54
19 changed files with 312 additions and 473 deletions

View File

@@ -1,6 +1,6 @@
import { useMemo, useCallback } from 'react';
import { Platform, Pressable, useWindowDimensions } from 'react-native';
import { Tabs, usePathname } from 'expo-router';
import { Tabs, usePathname, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { PressableProps } from 'react-native';
@@ -8,6 +8,7 @@ import type { PressableProps } from 'react-native';
import { useAppColors, shadows } from '../../../src/theme';
import { BREAKPOINTS } from '../../../src/hooks';
import { useHomeTabBarVisibilityStore, useTotalUnreadCount, useHomeTabPressStore } from '../../../src/stores';
import { hrefHome } from '../../../src/navigation/hrefs';
const TAB_BAR_HEIGHT = 56;
const TAB_BAR_FLOATING_MARGIN = 12;
@@ -28,6 +29,7 @@ export default function TabsLayout() {
const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
const pathname = usePathname();
const router = useRouter();
const unreadCount = useTotalUnreadCount();
const scrollHideTabBar = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
const triggerHomeTabPress = useHomeTabPressStore((s) => s.triggerPress);
@@ -35,11 +37,18 @@ export default function TabsLayout() {
const isHomeStackRoute = pathname === '/home' || pathname.startsWith('/home/');
const handleHomeTabPress = useCallback(() => {
if (pathname === '/home') {
triggerHomeTabPress();
}
}, [pathname, triggerHomeTabPress]);
const handleHomeTabPress = useCallback(
(e: { preventDefault: () => void }) => {
if (!isHomeStackRoute) return;
e.preventDefault();
if (pathname === '/home') {
triggerHomeTabPress();
} else {
router.navigate(hrefHome());
}
},
[isHomeStackRoute, pathname, triggerHomeTabPress, router]
);
const tabBarStyle = useMemo(() => {
if (hideTabBar) {

View File

@@ -17,8 +17,6 @@ export default function ProfileStackLayout() {
<Stack.Screen name="blocked-users" />
<Stack.Screen name="chat-settings" />
<Stack.Screen name="about" />
<Stack.Screen name="terms" />
<Stack.Screen name="privacy" />
<Stack.Screen name="verification" />
<Stack.Screen name="data-storage" />
<Stack.Screen name="privacy-settings" />

View File

@@ -1,5 +0,0 @@
import { PrivacyPolicyScreen } from '../../../../src/screens/profile/PrivacyPolicyScreen';
export default function PrivacyPolicyRoute() {
return <PrivacyPolicyScreen />;
}

View File

@@ -1,5 +0,0 @@
import { TermsOfServiceScreen } from '../../../../src/screens/profile/TermsOfServiceScreen';
export default function TermsOfServiceRoute() {
return <TermsOfServiceScreen />;
}

View File

@@ -1,83 +1,24 @@
import { useEffect, useRef } from 'react';
import { Platform } from 'react-native';
import { useEffect } from 'react';
import { Redirect } from 'expo-router';
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
import { messageManager, useAuthStore } from '../../src/stores';
import { jpushService } from '../../src/services/notification/jpushService';
function getDeviceType(): 'ios' | 'android' | 'web' {
switch (Platform.OS) {
case 'ios': return 'ios';
case 'android': return 'android';
default: return 'web';
}
}
async function getOrCreateDeviceID(): Promise<string> {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
const DEVICE_ID_KEY = 'withyou_device_id';
let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
if (deviceId) return deviceId;
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 8);
deviceId = `${Platform.OS}_${timestamp}_${random}`;
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
return deviceId;
} catch {
return `${Platform.OS}_${Date.now()}`;
}
}
import { useRegisterPushDevice } from '../../src/hooks';
import { hrefAuthLogin } from '../../src/navigation/hrefs';
export default function AppLayout() {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const userID = useAuthStore((s) => s.currentUser?.id);
const deviceRegistered = useRef(false);
useEffect(() => {
messageManager.initialize();
}, []);
useEffect(() => {
if (!isAuthenticated || !userID) return;
if (!deviceRegistered.current) {
deviceRegistered.current = true;
if (Platform.OS === 'web') {
// Web: register device without push_token (JPush not available on web)
getOrCreateDeviceID().then((deviceId) => {
const { pushService } = require('../../src/services/notification/pushService');
pushService.registerDevice({
device_id: deviceId,
device_type: 'web',
device_name: 'Web Browser',
}).catch((err: any) => {
console.warn('[Push] web device register failed:', err?.message);
});
});
} else {
// Mobile: JPush handles registration internally in initialize()
jpushService.initialize().then((ok) => {
if (ok && userID) {
jpushService.setAliasForUser(userID);
}
});
}
}
return () => {
if (!isAuthenticated) {
deviceRegistered.current = false;
jpushService.cleanup();
}
};
}, [isAuthenticated, userID]);
useRegisterPushDevice(isAuthenticated, userID);
if (!isAuthenticated) {
return <Redirect href="/login" />;
return <Redirect href={hrefAuthLogin()} />;
}
return <AppRouteStack />;
}
}

View File

@@ -6,6 +6,7 @@ import { AppDesktopShell } from '../../src/app-navigation/AppDesktopShell';
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
import { BREAKPOINTS } from '../../src/hooks';
import { messageManager, useAuthStore } from '../../src/stores';
import { hrefAuthLogin } from '../../src/navigation/hrefs';
export default function AppLayoutWeb() {
const { width } = useWindowDimensions();
@@ -17,7 +18,7 @@ export default function AppLayoutWeb() {
}, []);
if (!isAuthenticated) {
return <Redirect href="/login" />;
return <Redirect href={hrefAuthLogin()} />;
}
if (useDesktopShell) {

View File

@@ -30,6 +30,7 @@ import { CallScreen, IncomingCallModal, FloatingCallWindow } from '../src/compon
import { jpushService } from '@/services/notification/jpushService';
import { callKeepService } from '@/services/callkeep';
import { callStore } from '@/stores/call';
import * as hrefs from '../src/navigation/hrefs';
registerNotificationPresentationHandler();
@@ -172,22 +173,21 @@ function NotificationBootstrap() {
if (conversationId) {
const isGroup = conversationType === 'group';
const q = new URLSearchParams();
if (isGroup) {
q.set('isGroupChat', '1');
if (groupId) q.set('groupId', groupId);
if (groupName) q.set('groupName', groupName);
if (groupAvatar) q.set('groupAvatar', groupAvatar);
} else if (senderId) {
q.set('userId', senderId);
}
const qs = q.toString();
router.push(`/chat/${encodeURIComponent(conversationId)}${qs ? `?${qs}` : ''}`);
router.push(
hrefs.hrefChat({
conversationId,
userId: isGroup ? undefined : senderId,
isGroupChat: isGroup,
groupId: isGroup ? groupId : undefined,
groupName: isGroup ? groupName : undefined,
groupAvatar: isGroup ? groupAvatar : undefined,
})
);
} else {
router.push('/messages/notifications');
router.push(hrefs.hrefNotifications());
}
} else {
router.push('/messages/notifications');
router.push(hrefs.hrefNotifications());
}
});
@@ -287,30 +287,6 @@ function ThemedStack() {
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(app)" options={{ headerShown: false }} />
<Stack.Screen
name="terms"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="privacy"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="post/[postId]"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="user/[userId]"
options={{
headerShown: false,
}}
/>
</Stack>
</SessionGate>
</>

View File

@@ -1,11 +1,12 @@
import { Redirect } from 'expo-router';
import { useAuthStore } from '../src/stores';
import { hrefAuthWelcome, hrefHome } from '../src/navigation/hrefs';
export default function Index() {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
if (isAuthenticated) {
return <Redirect href="/home" />;
return <Redirect href={hrefHome()} />;
}
return <Redirect href="/welcome" />;
return <Redirect href={hrefAuthWelcome()} />;
}