import type { ReactElement, ReactElementFiber } from '../element/types'; import navigationStack, { getRoute } from './navigationStack'; const MarkParentKey = '__bd_parent'; let currentScreen = ''; let Screens: any; try { Screens = require('react-native-screens'); } catch (e) { // Ignore } function findChild( node: ReactElementFiber, filter: (node: ReactElementFiber) => boolean ): ReactElementFiber | undefined { if (!node) { return undefined; } if (filter(node)) { return node; } return findChild(node.child, filter); } function findParent( node: ReactElementFiber, filter: (node: ReactElementFiber) => boolean ): ReactElementFiber | undefined { if (!node) { return undefined; } if (filter(node)) { return node; } return findParent(node.return, filter); } function markParent(route: any, navigation: any) { if (route[MarkParentKey]) { // console.info(route); return; } const parentNavigation = navigation.getParent(); if (!parentNavigation) { return; } const parentRoute = getRoute(parentNavigation.getState()); route[MarkParentKey] = parentRoute.key; } function checkRoute(key: string, navigation: any): boolean { if (!key || !navigation) { return true; } const currentRoute = getRoute(navigation.getState()); if (key !== currentRoute.key) { return false; } return checkRoute(currentRoute[MarkParentKey], navigation.getParent()); } function handleNativeStack(node: ReactElementFiber) { let { route, options, navigation } = node.memoizedProps.descriptor; // console.info(currentScreen, route); if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { return; } currentScreen = route.key; // console.info(currentScreen); navigationStack.add(node, navigation, route, options); } function handleStack(node: ReactElementFiber) { let route, navigation, options; const { state, descriptors } = node.memoizedProps; route = getRoute(state); if (!descriptors || !route) { return; } const descriptor = descriptors[route.key]; if (!descriptor) { console.warn('failed to find tab descriptor in react navigation'); return; } navigation = descriptor.navigation; options = descriptor.options; if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { // console.info(route); return; } currentScreen = route.key; // console.info(currentScreen); navigationStack.add(node, navigation, route, options); } function handleTab(node: ReactElementFiber) { let route, navigation, options; const childNode = findChild(node, (n) => n.memoizedProps?.hasOwnProperty('route') ); if ( !childNode || !childNode.memoizedProps || !childNode.memoizedProps.route ) { return; } route = childNode.memoizedProps.route; navigation = childNode.memoizedProps.navigation; options = childNode.memoizedProps.options; if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { // console.info(route); return; } currentScreen = route.key; // console.info(currentScreen); navigationStack.add(node, navigation, route, options); } const taroNavigationEventSet = new Set(); const taroPageMap = new Map(); function handleTaro(node: ReactElementFiber) { let route, navigation, pageConfig; const childNode = findChild(node, (n) => n.memoizedProps?.hasOwnProperty('pageConfig') ); if ( !childNode || !childNode.memoizedProps || !childNode.memoizedProps.route ) { return; } route = childNode.memoizedProps.route; navigation = childNode.memoizedProps.navigation; pageConfig = childNode.memoizedProps.pageConfig; if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { // console.info(route); return; } currentScreen = route.key; // console.info(currentScreen); const options = { path: pageConfig?.pagePath, title: pageConfig?.navigationBarTitleText, }; taroPageMap.set(route.key, [node, navigation, options]); // console.info('cache', route.key); navigationStack.add(node, navigation, route, options); if (!taroNavigationEventSet.has(navigation)) { navigation.addListener('state', taroStateListener); taroNavigationEventSet.add(navigation); } } function taroStateListener(event: any) { const state = event?.data?.state; // console.info(state); if (!state) { return; } const route = getRoute(state); // console.info(route); if (!taroPageMap.has(route.key)) { return; } currentScreen = route.key; const [node, navigation, options] = taroPageMap.get(route.key); navigationStack.add(node, navigation, route, options); const routes = state.routes ?? []; if (routes.length < taroPageMap.size) { const keySet = new Set(routes.map((route: any) => route.key)); for (const key of taroPageMap.keys()) { if (!keySet.has(key)) { taroPageMap.delete(key); } } } // console.info('map', [...taroPageMap.keys()]); } let screensEnabled: any; let _lastNavigatorNode: ReactElementFiber; let _lastTabNode: ReactElementFiber; let _lastNativeStackNode: ReactElementFiber; function handleWithScreens(node: ReactElementFiber) { const memoizedProps = node.memoizedProps; if (memoizedProps.hasOwnProperty('activityState')) { if (memoizedProps.hasOwnProperty('enabled')) { const targetNode = memoizedProps.children?._owner; if ( targetNode && targetNode.memoizedProps && targetNode.memoizedProps.hasOwnProperty('state') && targetNode.memoizedProps.hasOwnProperty('navigation') && targetNode.memoizedProps.hasOwnProperty('descriptors') ) { handleStack(targetNode); } else { handleTab(node); } _lastNavigatorNode = node; return; } const targetNode = findParent(node, (n) => n.memoizedProps?.hasOwnProperty('descriptor') ); if (targetNode) { handleNativeStack(targetNode); _lastNativeStackNode = node; } return; } if ( memoizedProps.hasOwnProperty('navigation') && memoizedProps.hasOwnProperty('route') ) { handleTaro(node); return; } const childNode = findChild( node, (n) => n.memoizedProps.hasOwnProperty('route') && n.memoizedProps.hasOwnProperty('navigation') ); if (childNode) { handleJsxCompile(childNode); } } function handleWithoutScreens(node: ReactElementFiber) { const memoizedProps = node.memoizedProps; // console.info(getComponentName(node.elementType), '->', Object.keys(memoizedProps ?? {})) if ( memoizedProps.hasOwnProperty('pointerEvents') && memoizedProps.hasOwnProperty('onLayout') ) { const tabNode = findParent( node, (n) => n.memoizedProps?.hasOwnProperty('state') && n.memoizedProps?.hasOwnProperty('descriptors') ); if (tabNode) { // console.info(getComponentName(tabNode.elementType), '->', Object.keys(tabNode.memoizedProps ?? {})) handleStack(tabNode); _lastTabNode = node; } return; } if ( memoizedProps.hasOwnProperty('activityState') && memoizedProps.hasOwnProperty('enabled') ) { const targetNode = memoizedProps.children?._owner; if ( targetNode && targetNode.memoizedProps && targetNode.memoizedProps.hasOwnProperty('state') && targetNode.memoizedProps.hasOwnProperty('navigation') && targetNode.memoizedProps.hasOwnProperty('descriptors') ) { handleStack(targetNode); _lastNavigatorNode = node; } } } export default function handleReactNavigation(element: ReactElement) { const node = element._owner; if (screensEnabled === undefined) { screensEnabled = !!Screens?.screensEnabled?.(); } if (!node) { if (screensEnabled) { tryHandleJsxCompileProd(element); } return; } if (_lastNavigatorNode === node) { return; } if (_lastTabNode === node) { return; } if (_lastNativeStackNode === node) { return; } if (!node.memoizedProps) { return; } if (screensEnabled === undefined) { screensEnabled = !!Screens?.screensEnabled?.(); } if (screensEnabled) { handleWithScreens(node); } else { handleWithoutScreens(node); } } const jsxNavigationEventSet = new Set(); const jsxPageMap = new Map(); const handleJsxCompile = (node: ReactElementFiber) => { let route: any, navigation: any; if (!node?.memoizedProps?.route) { return; } route = node.memoizedProps.route; navigation = node.memoizedProps.navigation; if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { return; } currentScreen = route.key; jsxPageMap.set(route.key, [ navigation, { path: route.name, title: route.name, }, ]); navigationStack.add(node, navigation, route, {}); if (!jsxNavigationEventSet.has(navigation)) { navigation.addListener('state', (event: any) => { jsxStateListener(event, navigation); }); jsxNavigationEventSet.add(navigation); } }; const tryHandleJsxCompileProd = (element: ReactElement) => { if (process.env.NODE_ENV === 'production') { handleJsxCompileProd(element); return; } }; const handleJsxCompileProd = (element: ReactElement) => { let route: any, navigation: any; if (!element?.props?.route) { return; } route = element.props.route; navigation = element.props.navigation; if (currentScreen === route.key) { return; } markParent(route, navigation); if (!checkRoute(route.key, navigation)) { return; } currentScreen = route.key; jsxPageMap.set(route.key, [ navigation, { path: route.name, title: route.name, }, ]); navigationStack.add(element._owner, navigation, route, {}); if (!jsxNavigationEventSet.has(navigation)) { navigation.addListener('state', (event: any) => { jsxStateListener(event, navigation); }); jsxNavigationEventSet.add(navigation); } }; function jsxStateListener(event: any, navigation: any) { const state = event?.data?.state; if (!state) { return; } for (let i = 0; i < state.routes.length; i++) { const route = state.routes[i]; if (!jsxPageMap.has(route.key)) { jsxPageMap.set(route.key, [ null, navigation, { path: route.name, title: route.name, }, ]); } } const route = getRoute(state); if (!jsxPageMap.has(route.key)) { return; } currentScreen = route.key; const [node, options] = jsxPageMap.get(currentScreen); navigationStack.add(node, navigation, route, options); const routes = state.routes ?? []; if (routes.length < jsxPageMap.size) { const keySet = new Set(routes.map((route: any) => route.key)); jsxPageMap.forEach((key) => { if (!keySet.has(key)) { jsxPageMap.delete(key); } }); } }