From 606df826e93e5fae80536227633392b9ddb32b98 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 19 Oct 2026 16:44:42 +0200 Subject: [PATCH 1/6] fix: wrap inbox and insights with screens marker --- .../LHNOptionsList/LHNOptionsList.tsx | 2 + .../TabRootScrollViewMarker/index.native.tsx | 37 +++++++ .../TabRootScrollViewMarker/index.tsx | 12 +++ .../TabRootScrollViewMarker/types.ts | 8 ++ src/pages/Insights/InsightsDashboard.tsx | 97 ++++++++++--------- 5 files changed, 110 insertions(+), 46 deletions(-) create mode 100644 src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.native.tsx create mode 100644 src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.tsx create mode 100644 src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/types.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 7ca6a3d66b80..d03f84a0954c 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,3 +1,4 @@ +import {renderTabRootScrollComponent} from '@components/Navigation/TabBarBottomContent/TabRootScrollViewMarker'; import useTabRootScrollProps from '@components/Navigation/TabBarBottomContent/useTabRootScrollProps'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; @@ -180,6 +181,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio maintainVisibleContentPosition={{disabled: true}} drawDistance={250} removeClippedSubviews + renderScrollComponent={renderTabRootScrollComponent} /> diff --git a/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.native.tsx b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.native.tsx new file mode 100644 index 000000000000..ac215d209045 --- /dev/null +++ b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.native.tsx @@ -0,0 +1,37 @@ +import useThemeStyles from '@hooks/useThemeStyles'; + +import type {ScrollViewProps} from 'react-native'; + +import React from 'react'; +// FlashList's default scroll component is React Native's Animated.ScrollView, not Reanimated's. +// eslint-disable-next-line no-restricted-imports +import {Animated} from 'react-native'; +import {ScrollViewMarker} from 'react-native-screens/experimental'; + +import type TabRootScrollViewMarkerProps from './types'; + +/** + * A second tap on the focused native tab scrolls the tab's content to the top. Without a marker, react-native-screens + * scrolls the first scroll view it finds by following each view's first child, which on a screen with a horizontal + * selector above its list is the selector. The marker has to be the scroll view's direct native parent. + */ +function TabRootScrollViewMarker({children}: TabRootScrollViewMarkerProps) { + const styles = useThemeStyles(); + + return {children}; +} + +/** + * For a FlashList's `renderScrollComponent`, which replaces the list's own ScrollView rather than wrapping it. It renders + * the list's default scroll component, Animated.ScrollView. FlashList passes the ref among the props, so it reaches it. + */ +function renderTabRootScrollComponent(props: ScrollViewProps) { + return ( + + + + ); +} + +export default TabRootScrollViewMarker; +export {renderTabRootScrollComponent}; diff --git a/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.tsx b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.tsx new file mode 100644 index 000000000000..f7bed5e1a838 --- /dev/null +++ b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/index.tsx @@ -0,0 +1,12 @@ +import type TabRootScrollViewMarkerProps from './types'; + +/** On web the JS tab bar has no scroll to top on a second tap, so there is nothing to mark. */ +function TabRootScrollViewMarker({children}: TabRootScrollViewMarkerProps) { + return children; +} + +/** Lists keep their default scroll component. */ +const renderTabRootScrollComponent = undefined; + +export default TabRootScrollViewMarker; +export {renderTabRootScrollComponent}; diff --git a/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/types.ts b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/types.ts new file mode 100644 index 000000000000..41f7c4f850aa --- /dev/null +++ b/src/components/Navigation/TabBarBottomContent/TabRootScrollViewMarker/types.ts @@ -0,0 +1,8 @@ +import type {ReactElement} from 'react'; + +type TabRootScrollViewMarkerProps = { + /** The tab's vertical scroll view, rendered as the marker's only child */ + children: ReactElement; +}; + +export default TabRootScrollViewMarkerProps; diff --git a/src/pages/Insights/InsightsDashboard.tsx b/src/pages/Insights/InsightsDashboard.tsx index fe515d980c51..d5641e838efd 100644 --- a/src/pages/Insights/InsightsDashboard.tsx +++ b/src/pages/Insights/InsightsDashboard.tsx @@ -1,6 +1,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; +import TabRootScrollViewMarker from '@components/Navigation/TabBarBottomContent/TabRootScrollViewMarker'; import useTabRootScreenWrapperProps from '@components/Navigation/TabBarBottomContent/useTabRootScreenWrapperProps'; import useTabRootScrollProps from '@components/Navigation/TabBarBottomContent/useTabRootScrollProps'; import TopBar from '@components/Navigation/TopBar'; @@ -119,12 +120,14 @@ function InsightsDashboardContent({state, headlineChart, supportingCharts, filte if (isEmptyState) { return ( - - {state === INSIGHTS_DASHBOARD_STATE.NO_EXPENSES ? : } - + + + {state === INSIGHTS_DASHBOARD_STATE.NO_EXPENSES ? : } + + ); } @@ -132,47 +135,49 @@ function InsightsDashboardContent({state, headlineChart, supportingCharts, filte const columns = shouldUseNarrowLayout ? [supportingCharts] : [supportingCharts.filter((chart, index) => index % 2 === 0), supportingCharts.filter((chart, index) => index % 2 === 1)]; return ( - - - - - {columns.map((columnCharts, columnIndex) => ( - - {columnCharts.map(({chart, queryJSON, snapshot, averageQueryJSON, averageSnapshot}) => ( - - ))} - - ))} + + + + + + {columns.map((columnCharts, columnIndex) => ( + + {columnCharts.map(({chart, queryJSON, snapshot, averageQueryJSON, averageSnapshot}) => ( + + ))} + + ))} + - - + + ); } From 5340620d8871938fd23aa3cd493ae6e47bd35736 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 19 Oct 2026 16:46:48 +0200 Subject: [PATCH 2/6] fix: rerender nav bar when enabling insights beta --- .../NativeTabNavigator/NativeTabLayout.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NativeTabLayout.tsx b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NativeTabLayout.tsx index 777c74986afd..6ba91ad61c5a 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NativeTabLayout.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NativeTabLayout.tsx @@ -16,12 +16,24 @@ type NativeTabLayoutProps = Parameters descriptors[route.key]?.options.tabBarItemHidden) + .map((route) => route.name) + .join(); // The tabs stay at the same place in the tree in both layouts, so crossing the breakpoint does not remount them. return ( {!shouldUseNarrowLayout && } - {children} + + {children} + Date: Mon, 19 Oct 2026 17:12:03 +0200 Subject: [PATCH 3/6] fix: replace `REPLACE` with reset so back button on Account tab works after relaunch --- src/pages/settings/InitialSettingsPage.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8baaac6f2884..3ddc8918d50b 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -25,6 +25,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; import Navigation from '@libs/Navigation/Navigation'; +import type {TabNavigatorParamList} from '@libs/Navigation/types'; import {openInitialSettingsPage} from '@userActions/Wallet'; @@ -32,13 +33,15 @@ import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {NativeBottomTabNavigationProp} from '@react-navigation/bottom-tabs/unstable'; import type {ComponentRef} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, ScrollViewProps, StyleProp, ViewStyle} from 'react-native'; -import {findFocusedRoute, useNavigation, useNavigationState, useRoute} from '@react-navigation/native'; +import {CommonActions, findFocusedRoute, useNavigation, useNavigationState, useRoute} from '@react-navigation/native'; import {canSwitchAccountsSelector} from '@selectors/Account'; import React, {useContext, useEffect, useLayoutEffect, useRef} from 'react'; import {View} from 'react-native'; @@ -63,14 +66,23 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const isDrawnOverTabs = useIsSettingsDrawnOverTabs(); // The tab navigator keeps the full tab history, so going back returns to the tab the user opened Account from. - // With nothing behind it, as after a deep link, Home replaces Account so that going back does not reopen it. + // With nothing behind it, as after a deep link or a relaunch, the tab history is reset to Home alone so that going + // back does not reopen Account. The tab router drops REPLACE, so `navigate(HOME, {forceReplace: true})` can't do it. const goBackFromAccount = () => { if (navigation.canGoBack()) { navigation.goBack(); return; } - Navigation.navigate(ROUTES.HOME, {forceReplace: true}); + const tabNavigation = navigation.getParent>(); + const tabState = tabNavigation?.getState(); + const homeRoute = tabState?.routes.find((tabRoute) => tabRoute.name === SCREENS.HOME); + if (!tabNavigation || !tabState || !homeRoute) { + Navigation.navigate(ROUTES.HOME); + return; + } + + tabNavigation.dispatch(CommonActions.reset({...tabState, index: tabState.routes.indexOf(homeRoute), history: [{type: 'route', key: homeRoute.key}]})); }; useAndroidBackButtonHandler(() => { From 0c410eab8d8fe13f91fc79f6a51e6f043d10eeef Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 20 Oct 2026 12:12:53 +0200 Subject: [PATCH 4/6] fix: crash on iOS when using marker --- ios/Podfile | 3 +++ ios/Podfile.lock | 4 ++-- patches/react-native-screens/details.md | 7 +++++++ ...scroll-to-top-uses-scroll-view-marker.patch | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 patches/react-native-screens/react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch diff --git a/ios/Podfile b/ios/Podfile index 12d17f06cc44..d9fef99b568f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -3,6 +3,9 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json # This value is used by $RNMapboxMaps $RNMapboxMapsImpl = 'mapbox' $VCDisableFrameProcessors = true +# Compiles react-native-screens' experimental (gamma) iOS code, which ScrollViewMarker needs. +# Without it, rendering ScrollViewMarker crashes on iOS. +ENV['RNS_GAMMA_ENABLED'] = '1' def node_require(script) # Resolve script with node to allow for hoisting diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b1f52d4f86b6..b02b59ac794b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4116,7 +4116,7 @@ SPEC CHECKSUMS: RNPermissions: 32bd3b977f28442af0900b102f4458f0f9f9b77c RNReactNativeHapticFeedback: 6c8677db48f62a635e6a2065f5d2bd02498d7a73 RNReanimated: e75c988a6527ec651ea2957b93891ddb64d530d4 - RNScreens: 8708989f700bdc3dc5abc3c77aa3b5f3e73ce2b2 + RNScreens: 433b8c24dd1dfeaa3b61f72d55052cb64fd19c5f RNSentry: 3dcc26572ed12d8ee7edbe80d198dda70ac10d4c RNShare: eb378c57de30a83b0253eed74762ee99249f47d7 RNSVG: 394cfd0518613b144c65ffe69045f827b78a7a7c @@ -4134,6 +4134,6 @@ SPEC CHECKSUMS: VisionCamera: 30b358b807324c692064f78385e9a732ce1bebfe Yoga: c30c859b7e9b75e547b600b486fe33165f60de00 -PODFILE CHECKSUM: 64b30fca02abc208520075c2e1275b24533fa805 +PODFILE CHECKSUM: c937d1b7b774ce019c0c848e4ad1e850f08c0e90 COCOAPODS: 1.16.2 diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md index e053770b6b88..5d002d0f04cc 100644 --- a/patches/react-native-screens/details.md +++ b/patches/react-native-screens/details.md @@ -97,3 +97,10 @@ - Upstream PR/issue: TBD - E/App issue: [#104226](https://github.com/Expensify/App/issues/104226) - PR introducing patch: [#104238](https://github.com/Expensify/App/pull/104238) + +### [react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch](react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch) + +- Reason: On iOS, a second tap on a native tab scrolls the tab's content to the top. When the tab's root is a native stack, as with the Inbox tab's split navigator, `RNSScreenStack` handles that tap and picks the scroll view by following each view's first native subview. On the Inbox the first one it reaches is the horizontal tab selector above the list. That selector is already at the top, so nothing scrolls. A `ScrollViewMarker` around the list registers the list's scroll view with the tabs screen, but the stack never reads it. The patch makes the stack scroll the marked scroll view while it shows a single screen, and fall back to the first-subview search otherwise, since the marker can sit on any screen of the stack. The marker only exists on iOS when `RNS_GAMMA_ENABLED=1` is set at `pod install`, which both Podfiles do. +- Upstream PR/issue: TBD. [#4985](https://github.com/software-mansion/react-native-screens/pull/4985) makes Stack v4 screens register a marked scroll view as their content scroll view, but does not change the stack's handling of a repeated tab tap, so it does not fix this. +- E/App issue: TBD +- PR introducing patch: [#104368](https://github.com/Expensify/App/pull/104368) diff --git a/patches/react-native-screens/react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch b/patches/react-native-screens/react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch new file mode 100644 index 000000000000..833db323d6bf --- /dev/null +++ b/patches/react-native-screens/react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.mm b/node_modules/react-native-screens/ios/RNSScreenStack.mm +--- a/node_modules/react-native-screens/ios/RNSScreenStack.mm ++++ b/node_modules/react-native-screens/ios/RNSScreenStack.mm +@@ -143,8 +143,12 @@ namespace react = facebook::react; + tabScreenController.tabScreenComponentView.shouldUseRepeatedTabSelectionPopToRootSpecialEffect) { + return [[self popToRootViewControllerAnimated:true] count] > 0; + } else if (tabScreenController.tabScreenComponentView.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect) { +- UIScrollView *scrollView = +- [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[[self topViewController] view]]; ++ // A ScrollViewMarker registers its scroll view with the tabs screen, not with this stack, so it can be on any ++ // screen of the stack. It is used only while the stack shows a single screen. ++ UIScrollView *markedScrollView = ++ [[self viewControllers] count] == 1 ? [tabScreenController.tabScreenComponentView cachedContentScrollView] : nil; ++ UIScrollView *scrollView = markedScrollView ++ ?: [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[[self topViewController] view]]; + return [scrollView rnscreens_scrollToTop]; + } + From 1260bb962b607bb5ee69f0958fd3f57a0a15c4da Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 20 Oct 2026 13:00:52 +0200 Subject: [PATCH 5/6] fix: dont close the account tab immediately after open --- ...tom-tabs+7.19.2+003+hidden-tab-items.patch | 34 +++++++++++++++---- ....19.2+004+stable-native-tab-identity.patch | 14 ++++---- patches/react-navigation/details.md | 2 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch b/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch index 9dd8d44e6bd1..8ed497d4f84d 100644 --- a/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch +++ b/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js -index e6ea55b..4d577fe 100644 +index b087433de3b..f729d3ecbf1 100644 --- a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js +++ b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js @@ -4,11 +4,11 @@ import { getLabel, SafeAreaProviderCompat, Screen as ScreenContent } from '@reac @@ -39,7 +39,29 @@ index e6ea55b..4d577fe 100644 provenance: 0 } }); -@@ -189,17 +201,17 @@ export function NativeBottomTabView({ +@@ -88,7 +100,9 @@ export function NativeBottomTabView({ + target: route.key + }); + } +- if (actionOrigin === 'programmatic-js' || focusedRouteKey === route.key) { ++ // While a hidden route is focused, native keeps the tab underneath it selected, so native reporting that tab is ++ // not a request to leave the hidden route. ++ if (actionOrigin === 'programmatic-js' || nativeSelectedRouteKey === route.key) { + dispatch({ + type: 'CONFIRM_STATE', + confirmed +@@ -114,7 +128,9 @@ export function NativeBottomTabView({ + console.error(`Received 'tabSelectionRejected' for route that doesn't exist: ${selectedScreenKey}`); + return; + } +- if (focusedRouteKey === route.key) { ++ // Android re-sends an unchanged request on a re-render and rejects it as repeated, which reports the tab under a ++ // focused hidden route. Following it would close the hidden route. ++ if (nativeSelectedRouteKey === route.key) { + dispatch({ + type: 'CONFIRM_STATE', + confirmed +@@ -189,17 +205,17 @@ export function NativeBottomTabView({ const tabBarMinimizeBehavior = currentOptions.tabBarMinimizeBehavior === 'auto' ? 'automatic' : currentOptions.tabBarMinimizeBehavior === 'none' ? 'never' : currentOptions.tabBarMinimizeBehavior; const shouldHideTabBar = currentOptions.tabBarStyle?.display === 'none'; const bottomAccessory = currentOptions.bottomAccessory; @@ -61,7 +83,7 @@ index e6ea55b..4d577fe 100644 colorScheme: dark ? 'dark' : 'light', nativeContainerStyle: { backgroundColor: colors.background -@@ -213,6 +225,9 @@ export function NativeBottomTabView({ +@@ -213,6 +229,9 @@ export function NativeBottomTabView({ tabBarTintColor: activeTintColor }, children: state.routes.map((route, index) => { @@ -71,7 +93,7 @@ index e6ea55b..4d577fe 100644 const { options, render, -@@ -224,6 +239,7 @@ export function NativeBottomTabView({ +@@ -224,6 +243,7 @@ export function NativeBottomTabView({ title, lazy = false, tabBarLabel, @@ -79,7 +101,7 @@ index e6ea55b..4d577fe 100644 tabBarSelectionEnabled, tabBarBadgeStyle, tabBarIcon, -@@ -290,6 +306,7 @@ export function NativeBottomTabView({ +@@ -290,6 +310,7 @@ export function NativeBottomTabView({ preventNativeSelection: tabBarSelectionEnabled === false, badgeValue: tabBarBadge?.toString(), title: tabTitle, @@ -87,7 +109,7 @@ index e6ea55b..4d577fe 100644 nativeContainerStyle: { backgroundColor: colors.background }, -@@ -369,9 +386,40 @@ export function NativeBottomTabView({ +@@ -369,9 +390,40 @@ export function NativeBottomTabView({ }) }, route.key); }) diff --git a/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch b/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch index 71f8fa440f21..cb8623e23cac 100644 --- a/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch +++ b/patches/react-navigation/@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js -index 5a152a3d182..d2d69feda98 100644 +index f729d3ecbf1..8f349432b40 100644 --- a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js +++ b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js @@ -39,6 +39,10 @@ export function NativeBottomTabView({ @@ -31,7 +31,7 @@ index 5a152a3d182..d2d69feda98 100644 if (!route) { console.error(`Received 'tabSelected' for route that doesn't exist: ${selectedScreenKey}`); return; -@@ -121,7 +125,7 @@ export function NativeBottomTabView({ +@@ -123,7 +127,7 @@ export function NativeBottomTabView({ routeKey: selectedScreenKey, provenance }; @@ -40,7 +40,7 @@ index 5a152a3d182..d2d69feda98 100644 if (!route) { console.error(`Received 'tabSelectionRejected' for route that doesn't exist: ${selectedScreenKey}`); return; -@@ -150,7 +154,7 @@ export function NativeBottomTabView({ +@@ -154,7 +158,7 @@ export function NativeBottomTabView({ }); navigation.emit({ type: 'tabPress', @@ -49,7 +49,7 @@ index 5a152a3d182..d2d69feda98 100644 }); }; const currentOptions = descriptors[state.routes[state.index].key]?.options; -@@ -204,7 +208,7 @@ export function NativeBottomTabView({ +@@ -208,7 +212,7 @@ export function NativeBottomTabView({ return /*#__PURE__*/_jsxs(SafeAreaProviderCompat, { children: [/*#__PURE__*/_jsx(Tabs.Host, { navStateRequest: { @@ -58,7 +58,7 @@ index 5a152a3d182..d2d69feda98 100644 baseProvenance: nativeState.confirmed.provenance }, rejectStaleNavStateUpdates: true, -@@ -302,7 +306,7 @@ export function NativeBottomTabView({ +@@ -306,7 +310,7 @@ export function NativeBottomTabView({ closing: true, route }), @@ -67,7 +67,7 @@ index 5a152a3d182..d2d69feda98 100644 preventNativeSelection: tabBarSelectionEnabled === false, badgeValue: tabBarBadge?.toString(), title: tabTitle, -@@ -370,6 +374,7 @@ export function NativeBottomTabView({ +@@ -374,6 +378,7 @@ export function NativeBottomTabView({ }, overrideScrollViewContentInsetAdjustmentBehavior }, @@ -75,7 +75,7 @@ index 5a152a3d182..d2d69feda98 100644 children: /*#__PURE__*/_jsx(Deferred, { lazy: lazy, visible: isFocused || isPreloaded, -@@ -383,8 +388,8 @@ export function NativeBottomTabView({ +@@ -387,8 +392,8 @@ export function NativeBottomTabView({ children: render() }) }) diff --git a/patches/react-navigation/details.md b/patches/react-navigation/details.md index bb00259da721..7d55986533fa 100644 --- a/patches/react-navigation/details.md +++ b/patches/react-navigation/details.md @@ -73,7 +73,7 @@ ### [@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch](@react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch) -- Reason: Adds a `tabBarItemHidden` option to the native bottom tabs. The App registers six tabs but the native bar can show at most five (Material's `BottomNavigationView` throws past five, and `UITabBar` folds the rest into a "More" tab), and which five depends on the Insights beta: Insights with it, Account without it. A native tab bar cannot hide one of its items, so a route with `tabBarItemHidden` gets no `Tabs.Screen` at all. When such a route is focused, it is drawn in JS over the native host with the tab bar hidden, while the host keeps the last focused visible tab selected underneath. A hidden route stays mounted once visited. Nothing native changes. The host's selection is derived from the tab history, skipping routes that are hidden by then, so a beta change that hides the selected route cannot leave the host pointing at a removed tab. A route that moves between hidden and visible is remounted, and a hidden route ignores preloading and emits no transition events. The patch also forwards a `tabBarAccessibilityLabel` option to RNScreens' `tabBarItemAccessibilityLabel`. The iOS tab labels are drawn into the icon images, so without it VoiceOver announces the tabs with no name. **This part can be removed on the upgrade to React Navigation v8**, which forwards the same option. The `tabBarItemHidden` part has no upstream counterpart and stays. +- Reason: Adds a `tabBarItemHidden` option to the native bottom tabs. The App registers six tabs but the native bar can show at most five (Material's `BottomNavigationView` throws past five, and `UITabBar` folds the rest into a "More" tab), and which five depends on the Insights beta: Insights with it, Account without it. A native tab bar cannot hide one of its items, so a route with `tabBarItemHidden` gets no `Tabs.Screen` at all. When such a route is focused, it is drawn in JS over the native host with the tab bar hidden, while the host keeps the last focused visible tab selected underneath. A hidden route stays mounted once visited. Nothing native changes. The host's selection is derived from the tab history, skipping routes that are hidden by then, so a beta change that hides the selected route cannot leave the host pointing at a removed tab. A route that moves between hidden and visible is remounted, and a hidden route ignores preloading and emits no transition events. Native reporting the tab that sits under a focused hidden route, as Android does when it rejects an unchanged request as repeated on a re-render, confirms the state instead of moving JS to that tab, so the hidden route stays open. The patch also forwards a `tabBarAccessibilityLabel` option to RNScreens' `tabBarItemAccessibilityLabel`. The iOS tab labels are drawn into the icon images, so without it VoiceOver announces the tabs with no name. **This part can be removed on the upgrade to React Navigation v8**, which forwards the same option. The `tabBarItemHidden` part has no upstream counterpart and stays. - Upstream PR/issue: not reported, because hiding an item from a native tab bar has no native counterpart and this works around it in JS for the App's tab set; the accessibility label part is already in v8. - E/App issue: [#101169](https://github.com/Expensify/App/issues/101169) - PR Introducing Patch: [#101339](https://github.com/Expensify/App/pull/101339) From 33aeeb232176b623aa192f02babb320a1eb4db31 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 20 Oct 2026 13:04:43 +0200 Subject: [PATCH 6/6] chore: update details --- patches/react-native-screens/details.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md index 5d002d0f04cc..d903512dcac5 100644 --- a/patches/react-native-screens/details.md +++ b/patches/react-native-screens/details.md @@ -101,6 +101,6 @@ ### [react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch](react-native-screens+4.28.0+006+stack-scroll-to-top-uses-scroll-view-marker.patch) - Reason: On iOS, a second tap on a native tab scrolls the tab's content to the top. When the tab's root is a native stack, as with the Inbox tab's split navigator, `RNSScreenStack` handles that tap and picks the scroll view by following each view's first native subview. On the Inbox the first one it reaches is the horizontal tab selector above the list. That selector is already at the top, so nothing scrolls. A `ScrollViewMarker` around the list registers the list's scroll view with the tabs screen, but the stack never reads it. The patch makes the stack scroll the marked scroll view while it shows a single screen, and fall back to the first-subview search otherwise, since the marker can sit on any screen of the stack. The marker only exists on iOS when `RNS_GAMMA_ENABLED=1` is set at `pod install`, which both Podfiles do. -- Upstream PR/issue: TBD. [#4985](https://github.com/software-mansion/react-native-screens/pull/4985) makes Stack v4 screens register a marked scroll view as their content scroll view, but does not change the stack's handling of a repeated tab tap, so it does not fix this. -- E/App issue: TBD +- Upstream PR/issue: [#4985](https://github.com/software-mansion/react-native-screens/pull/4985) +- E/App issue: https://github.com/Expensify/App/issues/104284 - PR introducing patch: [#104368](https://github.com/Expensify/App/pull/104368)