import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Linking } from "react-native"; import { NavigationContainer, type LinkingOptions, useNavigationContainerRef, } from "@react-navigation/native"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { StatusBar } from "expo-status-bar"; import * as Notifications from "expo-notifications"; import AppNavigator from "./navigation/AppNavigator"; import type { RootStackParamList } from "./navigation/types"; import { SessionProvider, useSession } from "./state/session-context"; import { getNavigationTheme, useTheme } from "./theme"; import { parseNotificationTarget, type NotificationTarget } from "./notifications"; function extractGameId(url: string): string | null { try { const parsed = new URL(url); const path = parsed.pathname || ""; if (parsed.protocol === "https:" || parsed.protocol === "http:") { const match = path.match(/^\/play\/?([^/]+)?/); const id = match?.[1]?.trim(); return id ? id : null; } if (parsed.host === "play") { const id = path.replace(/^\//, "").trim(); return id ? id : null; } const fallbackMatch = path.match(/^\/play\/?([^/]+)?/); const fallbackId = fallbackMatch?.[1]?.trim(); return fallbackId ? fallbackId : null; } catch { return null; } } function logDeepLink(url: string) { if (!__DEV__) return; const gameId = extractGameId(url); console.log(`[deep-link] url=${url} gameId=${gameId ?? "invalid"}`); } function RootNavigationGate() { const manager = useSession(); const navigationRef = useNavigationContainerRef(); const [navReady, setNavReady] = useState(false); const lastTargetRef = useRef(null); const lastLinkRef = useRef(null); const pendingNotificationRef = useRef(null); const lastNotificationIdRef = useRef(null); const theme = useTheme(); const navigationTheme = getNavigationTheme(theme); const linking = useMemo>( () => ({ prefixes: ["negopoly://", "https://negopoly.fr"], config: { screens: { Entry: "play/:gameId", }, }, getInitialURL: async () => { const url = await Linking.getInitialURL(); if (url) { lastLinkRef.current = url; logDeepLink(url); } return url; }, subscribe: (listener) => { const onReceiveURL = ({ url }: { url: string }) => { if (!url) return; if (lastLinkRef.current === url) return; lastLinkRef.current = url; logDeepLink(url); listener(url); }; const subscription = Linking.addEventListener("url", onReceiveURL); return () => subscription.remove(); }, }), [], ); const processPendingNotification = useCallback(() => { const pending = pendingNotificationRef.current; if (!pending) return; if (!navReady || !navigationRef.isReady()) return; if (manager.sessionId !== pending.sessionId) return; if (!manager.session || manager.session.status !== "active") return; pendingNotificationRef.current = null; if (pending.type === "chat") { const targetStack = manager.isBanker ? "BankerTabs" : "PlayerTabs"; const targetTab = manager.isBanker ? "BankerChat" : "PlayerChat"; navigationRef.navigate( targetStack as never, { screen: targetTab, params: { screen: "ChatThread", params: { chatId: pending.chatId }, }, } as never, ); return; } const targetStack = manager.isBanker ? "BankerTabs" : "PlayerTabs"; const targetTab = manager.isBanker ? "BankerDashboard" : "PlayerHome"; navigationRef.navigate( targetStack as never, { screen: targetTab } as never, ); }, [manager.isBanker, manager.session, manager.sessionId, navReady, navigationRef]); const handleNotificationResponse = useCallback( (response: Notifications.NotificationResponse | null) => { if (!response) return; const identifier = response.notification.request.identifier; if (identifier && lastNotificationIdRef.current === identifier) return; if (identifier) { lastNotificationIdRef.current = identifier; } const target = parseNotificationTarget( response.notification.request.content.data, ); if (!target) return; if (__DEV__) { console.log( `[push] target=${target.type} session=${target.sessionId}` + (target.type === "chat" ? ` chat=${target.chatId}` : ""), ); } pendingNotificationRef.current = target; processPendingNotification(); }, [processPendingNotification], ); useEffect(() => { if (!navReady || !navigationRef.isReady()) return; let target: keyof RootStackParamList; if (!manager.sessionId) { target = "Entry"; } else if (!manager.session) { target = manager.connectionState === "error" ? "Entry" : "Lobby"; } else if (manager.session.status === "lobby") { target = "Lobby"; } else if (manager.isBanker) { target = "BankerTabs"; } else { target = "PlayerTabs"; } const currentRoute = navigationRef.getCurrentRoute(); if (currentRoute?.name === target || lastTargetRef.current === target) { return; } navigationRef.reset({ index: 0, routes: [{ name: target }], }); lastTargetRef.current = target; }, [ manager.sessionId, manager.session, manager.isBanker, manager.connectionState, navReady, navigationRef, ]); useEffect(() => { processPendingNotification(); }, [processPendingNotification]); useEffect(() => { let mounted = true; Notifications.getLastNotificationResponseAsync().then((response) => { if (!mounted) return; handleNotificationResponse(response); }); const subscription = Notifications.addNotificationResponseReceivedListener( (response) => { handleNotificationResponse(response); }, ); return () => { mounted = false; subscription.remove(); }; }, [handleNotificationResponse]); return ( setNavReady(true)} linking={linking} theme={navigationTheme} > ); } export default function App() { const theme = useTheme(); return ( ); }