11import type { NavigatorScreenParams } from '@react-navigation/native' ;
22import { useFocusEffect } from '@react-navigation/native' ;
3- import React , { useCallback , useEffect , useMemo , useRef } from 'react' ;
3+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
44// eslint-disable-next-line no-restricted-imports
55import { Animated , DeviceEventEmitter , InteractionManager } from 'react-native' ;
66import { DialogLabelProvider } from '@components/DialogLabelContext' ;
@@ -61,13 +61,18 @@ function MissingPersonalDetailsWithPINContext(props: Record<string, unknown>) {
6161 ) ;
6262}
6363
64- function SecondaryOverlay ( ) {
64+ type SecondaryOverlayProps = {
65+ disabled : boolean ;
66+ } ;
67+
68+ function SecondaryOverlay ( { disabled} : SecondaryOverlayProps ) {
6569 const { shouldRenderSecondaryOverlayForWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForRHPOnSuperWideRHP} = useWideRHPState ( ) ;
6670 const { sidePanelOffset} = useSidePanelState ( ) ;
6771
6872 if ( shouldRenderSecondaryOverlayForWideRHP ) {
6973 return (
7074 < Overlay
75+ disabled = { disabled }
7176 progress = { secondOverlayWideRHPProgress }
7277 positionRightValue = { Animated . add ( sidePanelOffset . current , animatedWideRHPWidth ) }
7378 onPress = { ( ) => Navigation . closeRHPFlow ( ) }
@@ -78,6 +83,7 @@ function SecondaryOverlay() {
7883 if ( shouldRenderSecondaryOverlayForRHPOnWideRHP ) {
7984 return (
8085 < Overlay
86+ disabled = { disabled }
8187 progress = { secondOverlayRHPOnWideRHPProgress }
8288 positionRightValue = { Animated . add ( sidePanelOffset . current , variables . sideBarWidth ) }
8389 onPress = { Navigation . dismissToPreviousRHP }
@@ -88,6 +94,7 @@ function SecondaryOverlay() {
8894 if ( shouldRenderSecondaryOverlayForRHPOnSuperWideRHP ) {
8995 return (
9096 < Overlay
97+ disabled = { disabled }
9198 progress = { secondOverlayRHPOnSuperWideRHPProgress }
9299 positionRightValue = { Animated . add ( sidePanelOffset . current , variables . sideBarWidth ) }
93100 onPress = { Navigation . dismissToSuperWideRHP }
@@ -107,6 +114,14 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
107114 const { isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout ( ) ;
108115 const containerRef = useRef ( null ) ;
109116 const isExecutingRef = useRef < boolean > ( false ) ;
117+ // Tracks whether the inner RHP stack is currently running a non-closing push/pop animation.
118+ // Clicking the overlay mid-push leaves stale animation state (see issue #87174), so we
119+ // ignore the dismiss press until the in-flight transition finishes. The ref gates the
120+ // synchronous dispatchers; the state mirror drives the `disabled` prop on the overlays so
121+ // the Pressable itself no-ops (defense in depth against cases where the dispatcher path
122+ // bypasses the ref guard, e.g. inline Navigation.dismissToPreviousRHP).
123+ const isOverlayDismissEnabledRef = useRef < boolean > ( true ) ;
124+ const [ isOverlayDismissEnabled , setIsOverlayDismissEnabled ] = useState < boolean > ( true ) ;
110125 const screenOptions = useRHPScreenOptions ( ) ;
111126 const { superWideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState ( ) ;
112127 const { clearWideRHPKeys, syncRHPKeys} = useWideRHPActions ( ) ;
@@ -165,12 +180,31 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
165180 abandonReviewDuplicateTransactions ( ) ;
166181 } ) ;
167182 } ,
183+ transitionStart : ( event : { data ?: { closing ?: boolean } } ) => {
184+ // Only disable dismiss when a screen is starting to open. A starting close is
185+ // either the user's own dismiss (benign) or a cancellation of an in-flight open
186+ // (handled by transitionEnd below).
187+ if ( event . data ?. closing ) {
188+ return ;
189+ }
190+ isOverlayDismissEnabledRef . current = false ;
191+ setIsOverlayDismissEnabled ( false ) ;
192+ } ,
193+ transitionEnd : ( ) => {
194+ // Re-enable on ANY transitionEnd (opening or closing). If an opening transition
195+ // is interrupted by a pop, React Navigation cancels the open and only emits
196+ // transitionEnd with closing: true for the popped route — the opening's own
197+ // closing: false end never fires. Ignoring closing: true here would leave the
198+ // gate stuck false forever and swallow subsequent overlay dismiss clicks.
199+ isOverlayDismissEnabledRef . current = true ;
200+ setIsOverlayDismissEnabled ( true ) ;
201+ } ,
168202 } ) ,
169203 [ navigation , route . params ?. screen ] ,
170204 ) ;
171205
172206 const handleOverlayPress = useCallback ( ( ) => {
173- if ( isExecutingRef . current ) {
207+ if ( isExecutingRef . current || ! isOverlayDismissEnabledRef . current ) {
174208 return ;
175209 }
176210 isExecutingRef . current = true ;
@@ -215,6 +249,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
215249 < NoDropZone >
216250 { ! shouldUseNarrowLayout && (
217251 < Overlay
252+ disabled = { ! isOverlayDismissEnabled }
218253 positionLeftValue = { overlayPositionLeft }
219254 onPress = { handleOverlayPress }
220255 />
@@ -466,9 +501,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
466501 { /* The third and second overlays are displayed here to cover RHP screens wider than the currently focused screen. */ }
467502 { /* Clicking on these overlays redirects you to the RHP screen below them. */ }
468503 { /* The width of these overlays is equal to the width of the screen minus the width of the currently focused RHP screen (positionRightValue) */ }
469- { ! shouldUseNarrowLayout && < SecondaryOverlay /> }
504+ { ! shouldUseNarrowLayout && < SecondaryOverlay disabled = { ! isOverlayDismissEnabled } /> }
470505 { ! shouldUseNarrowLayout && shouldRenderTertiaryOverlay && (
471506 < Overlay
507+ disabled = { ! isOverlayDismissEnabled }
472508 progress = { thirdOverlayProgress }
473509 positionRightValue = { Animated . add ( sidePanelOffset . current , variables . sideBarWidth ) }
474510 onPress = { Navigation . dismissToPreviousRHP }
0 commit comments